Clean code: Class design principles
Сlean code: Class design principles
Single Responsibility Principle
The Single Responsibility Principle (SRP) states that a class or module should have one, and only one, reason to change. This principle gives us both a definition of responsibility, and a guidelines for class size. Classes should have one responsibility—one reason to change.
The seemingly small SuperDashboard class in Listing has two reasons to change. First, it tracks version information that would seemingly need to be updated every time the software gets shipped. Second, it manages some GUI Components. Trying to identify responsibilities (reasons to change) often helps us recognize and create better abstractions in our code. We can easily extract all three SuperDashboard methods that deal with version information into a separate class named Version.
SRP is one of the more important concept in OO design. It’s also one of the simpler concepts to understand and adhere to. Yet oddly, SRP is often the most abused class design principle. We regularly encounter classes that do far too many things. Why? Getting software to work and making software clean are two very different activities. Most of us have limited room in our heads, so we focus on getting our code to work more than organization and cleanliness. This is wholly appropriate. Maintaining a separation of concerns is just as important in our programming activities as it is in our programs.
The problem is that too many of us think that we are done once the program works. We fail to switch to the other concern of organization and cleanliness. We move on to the next problem rather than going back and breaking the overstuffed classes into decoupled units with single responsibilities. At the same time, many developers fear that a large number of small, single-purpose classes makes it more difficult to understand the bigger picture. They are concerned that they must navigate from class to class in order to figure out how a larger piece of work gets accomplished.
However, a system with many small classes has no more moving parts than a system with a few large classes. There is just as much to learn in the system with a few large classes.
Open Closed Principle
Classes should be open for extension but closed for modification. Class is open to allow new functionality via subclassing, but it can make this change while keeping every other class closed. Interface specifications can be reused through inheritance but implementation need not be. The existing interface is closed to modifications and new implementations must, at a minimum, implement that interface.
Comment: We will receive benefits as flexebility and prevent appearing new bugs. Cons are appearing many near the same entities with different behaviour. It seems the same issue like with SRP, that was described early. We recommend to use this principle when new Entity has rather strong differences in object characteristics.
Liskov Substitution Principle
Derived classes must be substitutable for their base classes.
The Liskov Substitution Principle says that the object of a derived class should be able to replace an object of the base class without bringing any errors in the system or modifying the behavior of the base class. In short: if S is subset of T, an object of T could be replaced by object of S without impacting the program and bringing any error in the system.
Let’s say you have a class Rectangle and another class Square. Square is as Rectangle, or in other words, it inherits the Rectangle class. So as the Liskov Substitution principle states, we should able to replace object of Rectangle by the object of Square without bringing any undesirable change or error in the system.
Liskov’s principle imposes some standard requirements on signatures which have been adopted in newer object-oriented programming languages (usually at the level of classes rather than types; see nominal vs. structural subtyping for the distinction):
- Contravariance of method arguments in the subtype.
- Covariance of return types in the subtype.
- No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.
In addition to the signature requirements, the subtype must meet a number of behavioral conditions. These are detailed in a terminology resembling that of design by contract methodology, leading to some restrictions on how contracts can interact with inheritance:
- Preconditions cannot be strengthened in a subtype.
- Postconditions cannot be weakened in a subtype.
- Invariants of the supertype must be preserved in a subtype.
- History constraint (the “history rule”). Objects are regarded as being modifiable only through their methods (encapsulation). Since subtypes may introduce methods that are not present in the supertype, the introduction of these methods may allow state changes in the subtype that are not permissible in the supertype. The history constraint prohibits this. It was the novel element introduced by Liskov and Wing. A violation of this constraint can be exemplified by defining a mutable point as a subtype of an immutable point. This is a violation of the history constraint, because in the history of the immutable point, the state is always the same after creation, so it cannot include the history of a mutable point in general. Fields added to the subtype may however be safely modified because they are not observable through the supertype methods. Thus, one can derive a circle with fixed center but mutable radius from immutable point without violating LSP.
Dependency Inversion Principle
We analyze Class Coupling in previous article. By minimizing coupling in this way, our classes adhere to another class design principle known as the Dependency Inversion Principle (DIP). In essence, the DIP says that our classes should depend upon abstractions, not on concrete details.
Comment: following of this principle gives a lot of benefits during application creation. We would like to focus on disadvantages:
- It doesn’t make a lot of sense to follow this principle for standard well-known components. If project is based on technology X, no make sense to use Interfaces of it instead of concrete implementation. We would say, that it is not possible in a lot of cases. Example: GUI creation on abstractions.
- Debugging of such application is more difficult compare with using concrete implementations.
- If use DI/IoC (dependency injection containers) need to care about performance
Interface Segregation Principle
When we design an application we should take care how we are going to make abstract a module which contains several submodules. Considering the module implemented by a class, we can have an abstraction of the system done in an interface. But if we want to extend our application adding another module that contains only some of the submodules of the original system, we are forced to implement the full interface and to write some dummy methods. Such an interface is named fat interface or polluted interface. Having an interface pollution is not a good solution and might induce inappropriate behavior in the system. The Interface Segregation Principle states that clients should not be forced to implement interfaces they don’t use. Instead of one fat interface many small interfaces are preferred based on groups of methods, each one serving one submodule.
Smaller classes are easier to grasp. Classes should be smaller than about 100 lines of code. Otherwise, it is hard to spot how the class does its job and it probably does more than a single job.
Comment: We follow a rule in company, that method should be smaller than 50 lines of code.
Next article will be about Class principles: Loose Coupling, High Cohesion, Change is Local, It is Easy to Remove