- DRY - Don’t Repeat Yourself
- KISS - Keep it simple, stupid!
- YAGNI - You Aint’t gona need it
- ADP - Acyclic dependencies principle
- LoD - Law of Demeter
During my study, I have collected quite a large and interesting list of object-oriented programming design principles that may be useful to you. This post is a shortened note. I invite you to explore knowledge reading books such as “Clean Architecture”, Martin Robert C. Ready? I invite you to read the 10 most important OOP principles.
Object-Oriented Design Principles represent a set of rules that are the essence of object-oriented programming and help us/others to prepare well, easy to understand design.
Solid is a set of five rules which tell us how to arrange our methods and data structures in classes. There are three main goals to understand and use SOLID:
✓ Code and functionality changes are easier
✓ Code is easy to understand
✓ Software structures are the basis of components that can be used in many software systems
There are five SOLID principles (each letter - one principle):
Single Responsibility Principle
Single Responsibility Principle tells us that each software entity (classes, modules, methods) is designed to assume only one responsibility. If a code change is to be made, we should need to make a changes in one class that processes it. The good example is the Employee class from a company application:
Why Employee class have to know something about currency exchange? By putting these two methods into a single Employee class, the developers have coupled Employee class with class responsible for exchange rates. How to do it to comply with the Single Responsibility Principle? We should remove getSalary.. methods from Employee class and create new class (e.g ExchangeService, SalaryCalculator, SalaryConverter…) which has knowledge of exchange rates. Next, we should provide a salary to ExchangeService class. ExchangeService takes the salary, calculate salary in another currency and return it.
Open/closed principle tell us that sofware entities should be open for extension, but closed for modification. Developer’s goal is to extend a class/module functionality without modyfying its source code. Let’s imagine creating a Square class. In accordance with the Single Responsibility Principle, you create a Square class and AreaCalculator which contain the method calculateArea(). See the following example:
Unfortunately, after some time we get a new task - we have to add two new figures: a triangle and a trapeze. The only solution is to modify the calculateArea() method:
This code starts to look like Arrow Anti-Pattern
How to do it in accordance with O/C principle? You should use Shape interface:
Thanks to it AreaCalculator does not have to know all kinds of shapes. It relying on Shape abstraction:
We’ve just made AreaCalculator closed for modification. If we get the task of creating a new shape - we will not have to modify this class. However, we can extend it.
Liskov Substitution Principle
Liskov Substitution Principle is a rule about the contract of the clasess: if a base class satisfies a contract, then by the LSP derived classes must also satisfy that contract. The main objectives of this principle are:
✓ Classes in the application should be swapped by their subclasses without affecting the correctness of the program, i.e. the inheriting class must be a good equivalent of the base class.
✓A subclass should not do less than the base class. So it should always do more.
Good example is the“Square extends Rectangle” class:
Mathematically, a square is a rectangle. Most people would misinterpret “is a” relation and model the relationship between the rectangle and a square with inheritance:
As you probably guess, you can not have two different dimensions for a square. It is possible to bypass this by:
We overrided setHeight and setWidth methods to set both dimensions to the same value. What do you think of this fix? This design breaks LSP. A client can works with instances of Rectangle, but breaks when instances of Square are passed to it:
How to do it correctly? The most critical aspect to inheritance is that we should model inheritance based on behaviours, not object properties. The easiest way to understand this is by way of example:
Now clients of Shape cannot make any behavior changes via setter methods. When clients have to change properties of shapes, they have to do it in concrete classes.
Interface Segregation Principle
The main assumption of the ISP:
No client should be forced to depend on methods it does not use.
in other words:
Many client-specific interfaces are better than one general purpose interface.
So interfaces that we create should not contain methods that we do not need. The class that implements the interface can not be forced to implement methods that it does not need, and this is often the case with large interface. Let us understand the interface segregation principle by below example:
We have one interface with two methods to generate time sheet report (e.g for Employee). Consider a case client TimeSheet wants to use this interface but want to use only Excel time sheets. The interface forces client to implement an unnecessary method generateCSV();. A better solution would be breaking the GenerateTimeSheet interface into two small ones which contains separate methods.
Dependency Inversion Principle
The general idea of this principle is as simple: High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules. Robert C. Martin’s definition of the Dependency Inversion Principle consists of two goals:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
In practice DIP tells that a method requires an interface object instead of specific class. This way we can pass many different versions of our entity into the same method.
EventService class uses concrete DBRepository class which save or delete events from the database. In above example EventService is High-level module. DBRepository is a low-level module. We have a direct dependence between classes here. In this way, we violate the DIP policy. How to do it correctly? To solve the above problem, we should make the EventService class not dependent on the DBRepository class. In addition, both classes must depend on abstraction. Let’s create an abstraction - Repository interface. It will have methods for writing and reading tasks:
Let’s change the EventService class to use the Repository interface and thus depend on abstraction:
Now we have to implement our DBRepository class (it will also depend on abstraction):
Relationships have been inverted. Now the “high level module” does not depend on the “low level module”. The lower layer module depends on the abstract interface from the upper layer (Repository interface). Changes in the module at the lower level do not affect the module at a higher level. If, for example, we need to save events in the file instead of in a database it is simple. It is enough to add the appropriate class at a lower level (FileRepository).
DRY - Don’t Repeat Yourself
We should write a code that is reusable, not repeat the logic contained in one place in the application. If you are close to the copy/paste code, think about creating an abstraction (loop, common interface, function, class, some design pattern, eg Strategy, etc.) that you will be able to repeatedly use.
KISS - Keep it simple, stupid!
Simplicity (and avoiding complexity) should be a priority during programming. The code should be easy to read and understand, requiring as little effort as possible. Each method should only solve one small problem, not many use cases. If you have a lot of conditions in the method, break these out into smaller methods. This is not only about the way the code is created and written, but also about the names of our classes, methods, variables and objects. Everything should be written in such a way that the name of the variable, object, method or class tells what its purpose or use is.
YAGNI - You Aint’t gona need it
This is a nice principle that says that in our program we should put the most important functionalities that we will need at a given moment. We should not write code that will not be useful at the moment, which will be redundant and which will only grow unnecessarily in our program.
ADP - Acyclic dependencies principle
This principle says that there should be no cycles in the dependency structure. An exemplary structure breaking this principle will be when: packet A has a dependency in packet B, which has a relationship in packet C, which in turn has a relationship in packet A:
We can prevent this by using the Dependency inversion principle, design patterns (eg Observer) or create a new package and put all the common dependencies there.
LoD - Law of Demeter
The Law of Demeter is often described this way:
“Only talk to your immediate friends.”
This principle says that a class’s method can only refer to:
- methods of the same class
- fields (and their methods) of the same class
- parameters (and their methods) passed to this method
- objects (and their methods) that this method will create
- reduction of dependence
- the code calling the given method does not have to know the structure of other objects
- changes in other objects do not require changing the method