This entry extends the one about object-oriented design. Unlike the broad focus of that article, this one focuses on a more specific topic – the Single Responsible Principle (SRP). However, SRP is too broad a topic to be discussed pithily. As a result, the subsequent article focuses on the more parochial topic, implementing SRP in a simple way. As a means for achieving that goal, tips are provided by the list below. The implementation of those tips is demonstrated by the accompanying code.
- One can use base classes to separate responsibilities. When a class extends another class, it inherits its base-class’s functionality. Consequently, the inheriting class necessarily avoids implementing its base-class’s functionality. Inheritance allows the functionality of the two classes to remain separated. The previous article on object-oriented design demonstrates this principle nicely.
- With the exception of library classes, most classes contain data. One can separate that data from its containing class to achieve SRP. However, achieving that separation can involve a myriad of strategies – a topic that rightly deserves its own article – but the means by which data can be separated from its containing class is not particularly germane to the topic at hand. The more relevant, and salient, point to take away from this tip is the concept that one should separate, when reasonable, a class’s data from the class itself.
- With the exception of data containers, classes have functionality. One can separate the handling of that functionality from the functionality’s containing class to achieve SRP. Similar to separating data, achieving the formerly described separation can involve a myriad of strategies, but the specifics of those strategies are ungermane to this article. The more relevant, and salient, point to take away from this tip is the concept that one should separate, when reasonable, a class’s handling of its functionality.
The aforementioned tips are not a formula. Occasionally, the preceding tips might not be prudent to use, so one must implement them judiciously. However, do not become too hung up on that caveat. If one uses a tip injudiciously, at some point the affects of that misuse should become apparent. Even if the point of that discovery is at an inopportune time, the windfall of such a discovery does teach valuable lessons – ones that are often not forgotten. As a result, feel free to make mistakes. However, one must still be mindful of the aforementioned caveat, as judiciousness must always be exercised when designing software.
Object-oriented design is a topic about which dozens of books have been written. I do not wish to replace those lengthy tomes with this blog entry. However, this blog entry does intend to provide a basic means to write better code. The provided means are the rules below. There are some exceptions to those rules, so they are not ironclad. However, following them generally will lead you to write better code. In addition, the code that implements these rules is available for download.
- All public elements, excluding constants, should be included in a class’s interface. Those public elements can exist anywhere in the interface’s inheritance structure. Fastidiously following this principle allows a developer to use the Dependency Inversion Principle (DIP), which seeks to make all class references point to either interfaces or abstract classes. Replacing concrete class references with references to abstract classes or interfaces allows a coder to use multiple implementations of an interface or an abstract class without necessarily having to change the code blocks where that object is being utilized. As a result, the code is much more maintainable and extensible.
- Any class that should not be instantiated should be declared abstract. Abstract classes cannot be instantiated. Any code that attempts to do so will generate a compile-time error. Such errors tell the programmer that he, wittingly or unwittingly, is using a given class incorrectly. Warning the programmer of incorrect usage can avoid unwanted behavior, which can result in bugs. If those bugs result from seemingly correct code, then they can be difficult to find. Such a mess can easily be obviated by declaring any class that should not be instantiated abstract.
- All classes that should not be extended should prevent coders from doing so. This rule addresses the same problem as the rule above. Unwanted behavior has the potential to introduce bugs that are difficult to root out. In the same manner that incorrect instantiation can lead to unwanted behavior, incorrect class extension can lead to unwanted behavior as well. As a result, a developer should prevent coders from extending classes that should not be extended.
- Any method that should not be overridden should also prevent coders from doing so. This rule is another variation on the same theme: unwanted behavior. Overriding a method that should not be overridden can introduce a raft of problems, especially if the overriding coder is unaware of the behavior of the method in the parent class. A coder should not be allowed to cause that much damage to the code base. Consequently, a coder should only be allowed to override certain methods and all other methods should block overrides.
- A class hierarchy can be built from a collection of small interfaces. The first rule discussed using interfaces to represent the public face of entities. However, those entity interfaces can be constructed from smaller more focused interfaces. This concept is called the Interface Segregation Principle (ISP). ISP allows entity interfaces to be expanded in a plug-and-play manner. An interface need only extend/implement a given interface to have that interface’s functionality. In addition, all interfaces that inherit from that interface can be updated by modifying that interface. As a result, code that makes use of ISP is more maintainable and extensible.
- Your code base should be constructed from a hierarchy of base classes. Interfaces represent similar outward facing functionality, but a class hierarchy has similarities beyond public functionality. They also have similar implementations. Those similarities of implementation should be represented through a hierarchy of base classes. Where possible, any class that can be instantiated should inherit from one of those base classes. Like ISP, a hierarchy of base classes allows a coder to change a class by simply changing its base class. Similarly, any class that has a given base class can be updated by updating that base class. Because of that capability, a code base that utilizes a collection of common base classes is more flexible and extensible.