When people start out as self-taught programmers, a lot of the times we think about creating an application that simply works. It works, perfect! ONTO THE NEXT VICTIM! However, once we try to scale up, all sorts of random issues start showing up. Poor coding practices, lack of documentation and tight coupling. To make life easier, I will go over a few tips to help you develop a more flexible, reusable and maintainable code base.
Classes should be open to extension but closed to modification.
What do I mean by this? Let’s imagine we have a specific code release. This specific class in the code release underwent all sorts of rigorous testing before being deployed into production. Once in production, you want the state of this class to remain unchanged (i.e. you don’t want some clumsy developer or even bug from another system mess up your nicely designed parameters). That being said, this class can be altered in the development stage (NOT production). While closed to editing, this class should be open to extension by inheritance (using a template till you hit the final concrete class) or polymorphism (using abstract classes).
Dependency Inversion Principle
Concrete classes should be dependent on flexible high level classes.
This specific principle is related to dependencies. There are 2 types of dependencies: Low Level Dependency and High Level Dependency. Low Level Dependency is to have concrete classes be dependent on other concrete classes. THIS SHOULD BE AVOIDED! Look at the diagram below:
We can see in the diagram that there are 3 subsystems: ClientSubsystem, EnterpriseSubsystem and BackendSubsystem. While this implementation is working perfectly, imagine down the line you needed to make changes to the EnterpriseSubsystem. Since BackendSubsystem is dependent on EnterpriseSubsystem, changes will also be needed on BackendSubsystem to make it compatible to EnterpriseSubsystem. We may even need to make changes to ClientSubsystem based on those changes. An alternative is to to use a High Level Dependency. High Level Dependency involves having concrete classes be dependent on flexible high level classes (such as interfaces and abstract classes). As seen in the diagram, if we only make changes to the EnterpriseSubsystem concrete class and not touch the interface that EnterpriseSubsystem inherits from, the other subsystems do not need alterations. With high level, the code base is generalized such that we can decouple the method calls in each subsystem. The dependency is on generalized behaviors instead of the implementation.
Composing Objects Principle
Aggregation allows concrete classes to delegate to other concrete classes while providing a looser level of coupling than inheritance.
This principle uses generalization, abstraction and polymorphism as means of indirection. In this principle, we argue that inheritance has the cost of coupling and should be replaced with aggregation. The coupling in inheritances can be seen below:
The child class at the bottom is dependent on the top parent class. This makes the entire system tightly coupled. Composition allows the user to dynamically add methods to the classes, without having to make the classes be tightly dependent. Common design patterns to implement this principle are Composite Design Pattern and Decorator Design Pattern. This allows concrete classes to delegate to other concrete classes while providing a looser level of coupling than inheritance. Another advantage of this principle is that the behavior of objects can be dynamically changed during runtime, while inheritance restricts changes to the compile time only.
However, a major disadvantage of this principle is that aggregation might cause very similar implementation of similar classes, while inheritance has the benefit of sharing code. This means that we need to allocate time and budget to provide implementation of all behavior. Hence, whether to use inheritance or aggregation depends on the specific use cases. Do you have a set of related classes or unrelated classes? What is a common behavior between them? Do you need special classes to handle specialized cases or do you just need a different implementation of the same behavior?
Interface Segregation Principle
No client should be forced to depend on methods it does not use.
This principle splits interfaces that are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. To explain this principle better, let’s imagine a grocery store. You need a system so that the grocery can get money quickly in an organized fashion, while clients wish to be served quickly. A solution to this is to create a system. Let’s call this ICashier. The ICashier can scan items, take payments and alot time for taking breaks. Grocery store managers are happy because money can be entered quickly onto this system using bar code scanners while clients are happy to be able to purchase items without having to stand in line for a long time. Let’s look at 2 implementations below:
For the implementation on the left, the ICashier is inherited by SelfServeMachine and HumanCashier. While the HumanCashier implements the ICashier interface effectively, the SelfServeMachine has 1 redundant method it needs to implement. Self server machines don’t take breaks like humans but it needs to implement the method as a dummy method (i.e. it doesn’t do anything) just to be able to implement the ICashier interface. An alternative is to break ICashier into 2 smaller interfaces, as seen on the right. By having an IHumanWorker interface to implement the human specific methods, we can effectively use a combination of both interface to effectively implement the specific needs of the SelfServeMachine and HumanCashier concrete classes.
While the code bases I have mentioned above may take some time and practice to get used to, trust me, it is COMPLETELY WORTH IT! During initial stages of your code development, the changes won’t be noticeable. If you are as impatient as me, IT’S ABSOLUTELY TEDIOUS AND FRUSTRATING! However, in the long run, these principles will allow you to flexibly develop your code base (especially if you end up going enterprise level with coders slacking off, poor documentation and 1 tiny bug can crash multiple systems that you could have never imagined were dependent on each other!).