S.O.L.I.D. Software Development, One Step at a Time (Cont.) Open-Closed Principle The Open-Closed Principle says that a class should be open for extension, but closed for modification. In other words, you should be able to easily change the behavior of the class in question without having to modify it. The next time you are at a hardware store, look at the power tools. You will notice that there are a wide range of saw blades that can attach to a single saw. One blade compared to another may not look very different at first, but a closer inspection may reveal some significant differences. Some blades are constructed with different metals, the number of teeth or edges may vary, and the material that is used for the teeth is often designed for special purposes. No matter what the difference, though, if you are comparing two blades that attach to the same type of saw, they will have one thing in common: how they attach to the saw-the interface between the saw and the blade. The individual differences of the blades are what make each type of blade unique. One blade may cut through wood extremely quickly, but leave the edges rough. Another blade may cut wood more slowly and leave the edges smooth. Still others may be suited for cutting metal or other materials. The wide variety of blades, combined with the common method of attaching them to the saw, allows you to change the behavior of the saw without having to modify the mechanical portion of the saw. So, how do you allow a class’s behavior to be modified without actually modifying the class? The answer is surprisingly simple and there are several methods for doing this. Have you ever implemented an interface in a class and then passed an instance of that class into another object? Perhaps you implemented the IPrincipal interface for custom security needs. Or, you may have written your own interface such as the classic example of IAnimal, and implemented a Cat and a Dog object from this interface. The ubiquitous nature of explicit interfaces in .NET, as well as abstract base classes, delegates, and other forms of abstraction, all provide different ways of allowing custom behavior to be supplied to existing classes and modules. You can use design patterns such as Strategy, Template, State, and others to facilitate the behavioral changes through the use of such abstractions. There are still other patterns and abstractions, and other methods of injecting behavior and altering the class at runtime. Chances are, if you have written an application that required even a small amount of flexibility, you have either provided a custom behavior implementation to an existing class, or have written a class that required a custom behavior to be supplied. Restructuring for Open-Closed Given the need for multiple, unknown file types to be parsed, you decide to supply an interface that can be implemented by any number of objects, from any number of third parties, including the network operations personnel. In addition to the actual file parsing, you will need the interface to tell you whether or not the specific implementation can handle the current file contents. Your resulting application structure looks more like Figure 7, with the IFileFormatReader interface defined as follows:  Figure 7: Enabling open/closed via IFileFormatReader.public interface IFileFormatReader { bool CanHandle(string fileContents); string GetMessageBody(string fileContents); }
Since you know that there are multiple file formats being read now, you also decide to move the existing code that reads the flat file and XML file formats into two separate objects. The flat file reader can handle any non-binary log file, so you decide that this handler does not need to determine if it can handle the file contents sent to it. It only needs to say that it can handle the format, and then send the original content back out. You rewrite the implementation of the flat file format reader as follows: class FlatFileFormatReader: IFileFormatReader { public bool CanHandle(string fileContents) { return true; }
public string GetMessageBody( string fileContents) { return fileContents; } }
The XML file format reader will contain a check to see if the XML is valid. The GetMessageBody method will then parse the XML for the content, as shown in Listing 3. Next, you want to introduce the FileReaderService class. This will use the various IFileFormatReader implementations and is where the behavioral change will occur when the various format readers are supplied. To support an unknown number of file format readers, you decide to store the list of registered format readers in a simple collection: IList<IFileFormatReader> _formatReaders = new List<IFileFormatReader>();
public void RegisterFormatReader( IFileFormatReader fileFormatReader) { _formatReaders.Add(fileFormatReader); }
The RegisterFormatReader method allows any code that calls the FileReaderService API to register as many format readers as they need. Then, when a file needs to be parsed, a call to a GetMessageBody method is made, passing in the contents of the file as a string. This method runs through the list of registered format readers and checks to see if the current one can handle the format. If it can, it calls the GetMessageBody method of the reader and returns the data. public string GetMessageBody(string fileContents) { string messageBody = string.Empty; foreach(IFileFormatReader formatReader in _formatReaders) { if (formatReader.CanHandle(fileContents)) { messageBody = formatReader .GetMessageBody(fileContents); break; } } return messageBody; }
At this point, if there is no registered reader that can handle the file contents, an empty string is returned. You realize that you need to add a default file reader. The intention is to ensure that all log files are handled, regardless of the content. If a file can’t be handled by any other reader, you will want to return all of the content through the flat file format reader. By adding a separate RegisterDefaultFileReader method, you can ensure that only one default exists. Listing 4 shows the resulting GetMessageBody implementation. Finally, you need to update the usage of the FormatReader object in your EmailSender. You need to register both the XML file format reader and the flat file format reader in the constructor of the email sender class. private readonly FileReaderService _fileReaderService = new FileReaderService();
public EmailSender() { _fileReaderService.RegisterFormatReader( new XmlFormatReader()); _fileReaderService. RegisterDefaultFormatReader( new FlatFileFormatReader()); }
Happy Consumers and More Requirements A few days after releasing this version of the application and API, you hear that the operations group loves your IFileFormatReader and the extensibility it brings to the table. They have successfully implemented several format readers and are planning on more. A short time later, a new request comes in that you were not expecting. One system the operations group must support logs all of its errors to a database, not a text file. Moreover, according the operations personnel, they cannot write code that hits the database in question. Apparently, that’s “above their pay grade.” They need someone on the development staff to do it, and are asking for your help. The most challenging part of this new requirement is the CTO being involved, again. Due to the high visibility of this project and the potential for lost revenue if errors are not proactively corrected, he wants your application updated to support reading from the database, immediately. According to your manager, when the CTO says “immediately” he usually means before the end of the day. It’s only a few hours before the day ends and you’ve been running on very little sleep for the last few days, but you think you can bang out a working version and get it to the operations group in time to make the CTO happy. |