Sunday, May 16, 2021

SOLID Principles, the foundation of GoF Patterns

        Robert Martin, affectionately called Uncle Bob, in many ways has been one of the towering figures in Object Oriented development methodology! He is the pioneer of the clean-code movement and has laid out some fundamental principles in object-oriented development methodology. I believe it is possible to describe most of GoF patterns in terms of the first principles proposed by Uncle Bob. These principles are immortalized by term SOLID principles.

        In the remaining article we will try to study the rationale behind few GoF patterns and examine whether the SOLID principles can explain the motivations behind each GOF pattern by first principle thinking.

        Once each pattern is broken down into set of first principles, it is left to designer’s flair and “syntactic sugar borrowed” from the programming language construct to implement or use that pattern.

Let’s revisit each of SOLID principle and connect it with the GoF Patterns!

SOLID acronym expands into

  • Single responsibility Principle
  • Open-Closed Principle
  • Liskov substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

 Single Responsibility Principle (SRP).

A class must bring only related and cohesive concepts together. It is also emphasized that “a class must have only one reason to change”. The changes might occur because the concept that the class wrapped in its definition has undergone a change or has evolved.

A direct consequence of following this principle is that this makes software maintenance and evolution easy. If a change percolates due to maintenance or software evolution, the changes will be localized in the class boundary that undergoes the change. Another benefit is the Increase of reuse of the classes. This follows as the class represents one single concept or responsibility.

"High cohesion within a module and low coupling between modules", a practice put forward in structured programming is just an extension of this principle. This principle paves way for more maintainable software with ability to evolve easily.

GOF pattern application

Factories abstract away the object creation responsibility from the class. This helps the served class (class whose objects are constructed by factory) to concentrate on the real- world concept it seeks to model. Builder and composite lend similar help for specific types of complex objects. As a matter of fact, all creational patterns abstract away the creation part of object from the served class. 

Similarly, Structural patterns take away the responsibility of representing the relation of classes (entities) among themselves. This helps the classes modelling the real entities classes to concentrate on representing the concepts they represent. For example, an Adapter takes abstracts away the difference in interface of two closely related classes. Iterators take away the traversal from collection and localize the traversal code. Mediators simplify the communication between objects and take full responsibility for brokering the communication. Memento takes away the state representation from a complex object. The Adapters, Iterators, Mediators and Memento handle single Responsibility!

Taking away one concern (such as logging, concurrency, traversal) and representing it in one full-fledged class to simplify the representation of another (entity) class better is underlying idea for several of GoF patterns. This helps the served class to narrowly focus on its core responsibility. If we were to extend the Single Responsibility principle to cover “separation of concerns” then we have a more emphatic way to cove several other GoF patterns

 

Open-Closed Principle

A module will be said to be OPEN if it is available for extension. It should be possible to extend the module with ease. A module will be said to be closed if available for use by other modules. The principle for module is extended further for defining boundaries of class.

        The basic underpinning thought is that classes should allow extensions, but it should not necessitate modifications to use it. Put simply, the interface for classes should be such defined that it allows adding of new functionality by extending (subclassing) the interface. At the same time, the interfaces should be stable enough so that modification of existing code is not needed the class code is reused.

        In object-oriented paradigm, adding code to enhance the functionality by extending the interface is welcomed. However, if code reuse necessitates adding too many branches and changing existing code, then the software design is deemed brittle. This principle makes perfect sense as changing existing code increases demand for tests and make the software fragile. Adding code by extending interfaces, however has less ripple down impact and places less demand for re-testing the delivered software.

        Software maintenance phase is true test of how well the implemented software has adhered to the Open-closed Principle! Open-closed principle enhances re-use by localising of changes

GoF Patterns Application

Bridge pattern: An interface in an inheritance hierarchy can be published, and the implementation is done its own inheritance hierarchy. This helps the library provider change implementation without impacting client code, as long as they have stable interface. The design of extensible interfaces as proposed by Open-closed principle clearly serves as a guidance in implementing this pattern.

Visitor: The main problem tackled by visitors is that there is a resistance to change existing class hierarchy of working code. However, there is a need to add functionality to all the related objects. Visitor pattern comes to rescue by introducing another visitor interface that helps us add functionality without modifying the existing working code but by adding functionality in visitor classes.

Iterator: Developers are reluctant to adapt code in each container for specific traversal mechanism. So, a full-fledged class that exposes container’s traversal mechanism called Iterators is created. This makes container’s interface more stable and extensions for different traversals techniques can be added in Iterators.

 

Liskov substitution principle

This principle emphasis careful subclassing and judicious use of inheritance. The child class inherits all the public and protected interface of the base class, so its concepts must conform to the base class characteristics.

Inheritance defines a very rigid relationship between derived and base classes (interfaces). Changes in Base classes often trickle down to the derived classes. It extends the Open/Closed Principle by focusing on the behaviour of a superclass and its subtypes. This principle together with Open-closed principle serves to provide blueprint for defining cleaner interface and class hierarchy.

GoF Patterns Application

          Abstract factory: While defining the class of related products, extra care is taken to extend the Factories of related objects. One abstract interface is exposed to client and the Concrete Factories implement the interface.

 

Interface Segregation Principle

This principle suggests that when we are faced with a complex interface then it is best advised to break it into distinct simpler, single concept representing interfaces. Doing this will save the designers of concrete classes from inserting dummy implementation in the concrete classes if the concrete classes do not have really need to implement that method

GoF Patterns Application

Segregating interface into smaller neat interface avoids bloated interfaces. Much like Single Responsibility Principle applied to designing classes the Interface segregating principle serves as a guideline to design lean interfaces.

 

Dependency Inversion principle

High level modules should not depend on low level modules but instead both should depend on higher level abstractions. Alternately, abstractions should not depend upon details but conversely details must depend upon abstractions. This aims to reduce the coupling between the classes by introducing abstraction between the layer, thus doesn’t care about the real implementation.

The idea is higher level abstraction are more stable than the lower level implementation details. The lower level implantation has to absorb much more variability. If the dependencies are on the implementation details, then very likely the software will be fragile.

Client code should write code against the interface of the classes rather than against the concrete classes. This will reduce the changes in client code should a change in the implementation of classes that provides the API changes

GoF Patterns Application

Bridge (pImpl idiom) The client is provided a higher-level interface class which provides stability to the client code. The implementation hierarchy separately absorbs all changes in implementation. It’s a classic case of Dependency inversion.

Factory method and abstract factory also recommends that client code should depend on the Factories for object creation. The factories are higher-level interfaces, defined just for object creation. This directly confirms to Dependency Inversion principle. Similarly, in prototype pattern the client code again depends on the interface class that exposes “clone” method rather than on concrete classes.


No comments: