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 the 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 the 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 the 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 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:

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 a 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 the 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 that the data and behavior are spread out among various classes. One should strive to make classes that don’t leak/break their implementation details to other classes. Itmeans not allowing the classes to have code, which extends 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 that 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 friends. 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'll 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 makes the implementation very maintainable.