Design Patterns: SOLID Principles of designing an application or module

 
  • SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.
  • Although the SOLID principles apply to any object-oriented design, they can also form a core philosophy for methodologies such as agile development or adaptive software development.

Single-Responsibility Principle

  • The single-responsibility principle (SRP) principle states that every module, class or function in a program should have responsibility over a single part of that program’s functionality, and it should encapsulate that part.
  • The reason it is important to keep a class focused on a single concern is that it makes the class more robust. If there is a change to the one of the responsibility of the class, there is a greater danger that the other part of code with a different responsibility will break if it is part of the same class.
  • So, the two aspects of the problem that are really two separate responsibilities, and should be in separate classes or modules.
  • It would be a bad design to couple two things that change for different reasons at different times.

Open–Closed Principle

  • Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
  • Such an entity can allow its behaviour to be extended without modifying its own source code.
  • Polymorphic open–closed principle:
    • Uses abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other.
    • This advocates inheritance from abstract base classes.
    • 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.

Liskov Substitution Principle

  • The principle is based on the concept of substitutability – a principle in object-oriented programming stating that an object (such as a class) and a sub-object (such as a class that extends the first class) must be interchangeable without breaking the program.
  • Liskov’s notion of a behavioural subtype defines a notion of substitutability for objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
  • Derived types must be completely substitutable for their base types, i.e., Base class object/pointer can be substituted by a Derived class object.
  • It is an extension of the Open-Close Principle.
  • Implementation Guidelines:
    • No new exceptions can be thrown by the subtype.
    • Client should not know which specific subtype they are calling, i.e., when base class pointer can accept any of the derived class passed to it at run-time.
    • New derived classes just extend without replacing the functionality of old classes.
  • Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

Interface Segregation Principle

  • The Interface Segregation Principle (ISP) states that no code should be forced to depend on methods it does not use.
  • ISP 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. Such shrunken interfaces are also called role interfaces.
  • ISP is intended to keep a system decoupled and thus easier to refactor, change, and redeploy.
  • ISP is one of the five SOLID principles of object-oriented design, similar to the High Cohesion Principle of GRASP.
  • Beyond object-oriented design, ISP is also a key principle in the design of distributed systems in general and microservices in particular. ISP is one of the six IDEALS principles for microservice design.
  • Within object-oriented design, interfaces provide layers of abstraction that simplify code and create a barrier preventing coupling to dependencies. A system may become so coupled at multiple levels that it is no longer possible to make a change in one place without necessitating many additional changes. Using an interface or an abstract class can prevent this side effect.

Dependency Inversion Principle

  • In object-oriented design, the Dependency Inversion Principle is a specific methodology for loosely coupling software modules.
  • The principle states:
    • High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
    • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
  • The idea behind the two points of this principle is that when designing the interaction between a high-level module and a low-level one, the interaction should be thought of as an abstract interaction between them. This not only has implications on the design of the high-level module, but also on the low-level one: the low-level one should be designed with the interaction in mind.
  • In Traditional layers pattern, In conventional application architecture, lower-level components are designed to be consumed by higher-level components. In this composition, higher-level components depend directly upon lower-level components to achieve some task. This dependency upon lower-level components limits the reuse opportunities of the higher-level components. Traditional_Layers_Pattern
  • In Dependency inversion pattern, with the addition of an abstract layer, both high- and lower-level layers reduce the traditional dependencies from top to bottom. Nevertheless, the “inversion” concept does not mean that lower-level layers depend on higher-level layers directly. Both layers should depend on abstractions (interfaces) that expose the behavior needed by higher-level layers.
    DIPLayersPattern
  • In a direct application of dependency inversion, the abstracts are owned by the upper/policy layers. This architecture groups the higher components and the abstractions together in the same package. The lower-level layers are created by inheritance/implementation of these abstract classes or interfaces.
  • Implementations:
    • Approach-1:
      • A direct implementation packages the high-level classes with service abstracts classes in one library.
      • In this implementation high-level components and low-level components are distributed into separate packages/libraries, where the interfaces defining the behavior/services are required and owned by and exist within the high-level component's library.
      • The implementation of the high-level component’s interface by the low-level component requires that the low-level component package depend upon the high-level component for compilation, thus inverting the conventional dependency relationship. Dependency_inversion
      • Figures 1 and 2 illustrate code with the same functionality, however in Figure 2, an interface has been used to invert the dependency. The direction of dependency can be chosen to maximize policy code reuse, and eliminate cyclic dependencies.
      • In this version of DIP, the lower layer component's dependency on the interfaces/abstracts in the higher-level layers makes re-utilization of the lower layer components difficult. This implementation instead ″inverts″ the traditional dependency from top-to-bottom to the opposite, from bottom-to-top.
    • Approach-2:
      • A more flexible solution extracts the abstract components into an independent set of packages/libraries:
      • The separation of each layer into its own package encourages re-utilization of any layer, providing robustness and mobility.
        DIPLayersPattern_v2

 

References: