No, this isn’t a word of God.
It isn’t the word of Jon Skeet/Martin Fowler/Jeff Atwood/Joel Spolsky (replace it with your favorite technologist) either.
We were reviewing some code and started discussing why we cut corners and don’t follow common sense principles. Even though everyone has their own nuggets of wisdom regarding how classes should be structured based on the functional context but there are still some fundamental principles which one could keep in mind while designing classes.
I. Follow Single Responsibility Principle. Every class should have one and only one axis of change. It is not only true for classes but methods as well. Haven’t you seen those long-winded-all-encompassing gobbledygook classes or methods which when spread out on a piece of paper turn out to be half the length of great wall of China? Well, the idea is not to do that.
The idea is that every class or a method has one reason to exist. If the class is called Loan then it shouldn’t handle Bank Account related details. If the method is called GetLoanDetails then it should be responsible for Getting Loan Details only!
II. Follow Open Closed Principle. It enables you to think about how would your system adapt to the future changes. It states that a system should allow the new functionality to be added with minimal set of changes to the existing code. Thus, the design should be open for extension but closed for modification. In our case, the developer had done something like this –
The problem with LoanProcessor is that it would have to change the moment there is a new type of Loan, for example HomeLoan. The preferable to structure this would have been –
This way the LoanProcessor would be unaffected if a new type of Loan was added.
III. Try to use composition over inheritance. If not followed properly, this could lead to brittle class hierarchies. The principle is really straightforward and one needs to ask – If I was to look at the child class, would I able to say “Child is a type of Parent”? Or, would it sound more like “Child is somewhat type of Parent”?
Always use inheritance for the first one as it would enable to use the Child wherever Parent was expected. This would also enable you to honor yet another design principle called Liskov Substitution Principle. And, use composition whenever you want to partially use the functionality of a class.
IV. Encapsulate data and behavior. Most of the developers only do data encapsulation and forget to encapsulate the code that varies based on the context. It’s not only important to hide the private data of your class but create well encapsulated methods that act on the private data.
V. Follow Loose Coupling among classes. This goes hand-in-hand with encapsulating the right behavior. One could only create loosely coupled classes if the behavior was well encapsulated within classes. One could achieve loose coupling by relying on abstractions rather than implementations.
VI. Make your classes highly cohesive. It shouldn’t be the data and behavior are spread out among various classes. One should strive to make classes which don’t leak/break their implementation details to other classes. It means not allowing the classes to have code which extend beyond its purpose of existence. Of course, there are design paradigms like CQRS which would want you to segregate certain types of behavior in different classes but they are only used for distributed systems.
VII. Code to interfaces than implementations. This promotes loose coupling and enables one to change the underlying implementations or introduce new ones without impacting the classes using them.
VIII. Stay DRY(Don’t Repeat Yourself). Yet another design principle which states not to repeat the same code at two different places. Thus, a specific functionality or algorithm should be implemented at one place and one place only. It leads to maintenance issues if the implementation is duplicated. The reverse of this is called WET – Write Everything Twice 🙂
IX. Principle of Least Knowledge i.e. Law of Demeter. This states that an object shouldn’t know the internal details of the objects it collaborates with. It is famously called – talk to friends and not friends of friend. A class should only be able to invoke public data members of the class it is collaborating with. It shouldn’t be allowed to access behavior or data for the classes that are composed by those data members. It leads to tight coupling if not followed properly thus creating systems which are harder to change.
X. Follow the Hollywood Principle – Don’t call us, we would call you. This enables to break the mold of conditional flow logic and allowing code to be executed based on events. This is either done via event callbacks or injecting the implementations of an interface. Dependency Injection, Inversion of Control or Observer design pattern are all good examples of this principle. This principle promotes loose coupling among classes and make the implementation very maintainable.