torsdag den 7. juni 2018

SOLID principles, in layman's terms: Open/Closed

Raison d'être: I set out to write about the SOLID software development principles. Specifically, my aim was/is to make these things more understandable to other developers who, much in the way as yours truly, found it troublesome to have to decipher lengthy, complex articles and books on the matter. These principles are for everyone to learn and use, but I found that they were hard to fully grasp; quite possibly because I come from a non-English speaking background. So with this series of articles I'm setting out to try and de-mystify the principles, with only the best intentions in mind. The principles apply to many layers of software development. In my articles, I specifially aim to describe them as they relate to programming. I hope it'll be of use to you. Thank you for stopping by.

This will be a 5 article-series about SOLID. SOLID is all the rage; at least as far as the job-adds I'm reading are concerned; "you are expected to honor the SOLID principles" etc. So what exactly is SOLID about? Plain and simple, it's a set of guide-lines in developing object-oriented systems. They are a group of concepts that have proven themselves valuable for a great many people coding a great many pieces of software. A tale told by your elders in software engineering, if you will, that you will want to pay heed to so you can boast on your CV that you're into the SOLID principles - and you'll be a better developer for knowing them, I promise you that. Heck, you're a better developer for simply _wanting_ to know them!

S[O]LID - The Open/Closed principle

The open/closed principle is the second in the set of principles that make out the SOLID acronym. According to the wikipedia entry, it states that [quote] "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification" [/quote]. What is basically comes down to is this, that you'll want to write your classes in such a way that you won't have to change them once they're done.

And why is this a good thing? Because changes to code is an inheritantly bad thing. Or, to put it differently, if you find yourself having to make changes to your source code when it's gone into production, that's when this principle would come in handy. Why, because source code once it's there and it's been tested, it's been reviewed, it's been released, by then it doesn't lend itself well to changes. Add a feature, tweak a business-rule, and you might find yourself in a situation where something, somewhere breaks, and you could be in a heap of trouble.

So what to do about it? It's easy: program to abstractions (i.e. interfaces or inherit from abstract classes), not implementations. When your variables are interface types, you won't need to specify until run-time which kind of implementation will actually represent the interface type. Consider this basic example:

First here's doing it wrong:

public class Logger
    {
        private DiskLogLocation diskLogLocation;

        public Logger()
        {
            diskLogLocation = new DiskLogLocation(@"c:\logs");
        }

        public void LogMessage(string message)
        {
            diskLogLocation.Log(message);

        }
    }


Above we have a Logger-class, which news up a 'DiskLogLocation', and write a message to it - supposedly to the directory specified in the constructor. The problem with this lies in the fact that we've now limited ourselves to the DiskLogLocation - we haven't fulfilled the open/closed principle, in as much as we cannot easily extend the Logger-class to allow us to log to the event-viewer, for example. Then here's doing it right:


 public class Logger
 {
        // use an interface, and provide an implementation of 
        // this interface at runtime
        private ILogLocation logLocation;

        public Logger()
        {
        }

        public void LogMessage(string message)
        {
            logLocation.Log(message);

        }
 }



In the above, the logLocation acts as a placeholder for which ever implementation of ILogLocation we eventually choose to use (see "As an aside" below on how to do just that). That's basically it - the open/closed principle in a nut-shell. That's - basically - what's meant by "open for extension": that we program to interfaces, not implementations, so that we can pull in whichever code we desire at a later time by simply replacing the interface implementation - for example, an 'EventLogger' class.

There're more ways than one to go about extending a class; the above is just one - but a fine place to start non the less, if I may say so myself. You can do magical stuff with some so-called 'design patterns', for example the strategy pattern, but that's for a different article.

So now you know about the open/closed principle: that you should program to interfaces and thus leave your options open for extention. Easy to learn, difficult to master; it takes some experience, recognizing the internal workings of a class that should be left open for extention. And there's always the risk of going overboard and interfacing to everything, when the business requirements just don't call for it. These discussions can get right religious at times.

______________

As an aside; how do you then, at run-time, specify which implementation will be used? Well one way is to have a method on your utilizing class which offers the possibility of exchanging the implementation with another one. For example, like so:

public class Logger
 {
        private ILogLocation logLocation;

        public Logger()
        {
        }
        
        public void SetLogLocation(ILogLocation _logLocation)
        {
            logLocation = _logLocation;
        }

        public void LogMessage(string message)
        {
            logLocation.Log(message);
        }
 }


Another way is to allow for this in the utilizing class' constructor, like so:

public class Logger
 {
        private readonly ILogLocation logLocation;

        // allow the Logger instantiator to determine which
        // implementation to use
        public Logger(ILogLocation _logLocationDeterminedAtRunTime)
        {
            logLocation = _logLocationDeterminedAtRunTime;          
        }

        public void LogMessage(string message)
        {
            logLocation.Log(message);

        }
 }

Given the above example you might possibly determine which implementing class to use via a configuration file, which you run at your composition root - which is just a fancy way of saying 'where your app starts', typically main() or ApplicationStart() or what have you. You could do this, for a simplistic example:

static void Main(string[] args)
        {
            string logLocationType = ConfigurationSettings.AppSettings.Get("type_of_log_location");
           
            if (logLocationType == "Disk")
            {
                logLocation = new DiskLogLocation(@"c:\logs");
            }

            Logger logger = new Logger(logLocation);
            logger.LogMessage("hello log");

        }

Here we're asking a configuration-setting for know-how on which type of log-location we wish to use, and we instantiate that pass it to the logger on construction. Now you're well on your way to using something fancy called 'Dependency Injection', - which in turn is just a fancy way of saying 'we are giving our classes the variables they need in their constructor*'. A topic for a different article entirely.

*) Usually the case, but not always.


Hope this helps you in your further career. And good luck with it, too!

Buy me a coffeeBuy me a coffee

Ingen kommentarer:

Send en kommentar