S.O.L.I.D Principles for Software Development

In object-oriented computer programming, the term SOLID is an acronym for five designs principles intended to make software designs more understandable, flexible and maintainable. The principles are a subset of many principles promoted by Robert C. Martin. Thought they apply to an object-oriented design, the SOLID principles can also form a core philosophy for methodologies such as agile development or adaptive software development. The SOLID acronym was introduced by Michael Feathers.

Single responsability principle.

The single responsibility principle is a computer programming principle that states that every module or class should have responsibility over a single part of the funcionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility. Robert C. Martin express the principle as, «A class should have only one reason to change»

Example

Martin defines a responsibility as a reason to change, and concludes that a class or module should have one, and only one, reason to be changed (i.e. rewritten). As an example, consider a module that compiles and prints a report. Imagine such a module can be changed for two reasons. First, the content of the report could change. Second, the format of the report could change. These two things change for very different causes; one substantive, and one cosmetic. The single responsibility principle says that these two aspects of the problem are really two separate responsibilities, and should therefore be in separate classes or modules. It would be a bad design to couple two things that change for different reasons at different times.

The reason it is important to keep a class focused on a single concern is that it makes the class more robust. Continuing with the foregoing example, if there is a change to the report compilation process, there is greater danger that the printing code will break if it is part of the same class.

Open/closed principle

In object-oriented programming, the open/closed principle states «software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification«;[1] that is, such an entity can allow its behaviour to be extended without modifying its source code.

The name open/closed principle has been used in two ways. Both ways use generalizations (for instance, inheritance or delegate functions) to resolve the apparent dilemma, but the goals, techniques, and results are different.

Meyer’s open/closed principle

Bertrand Meyer is generally credited for having originated the term open/closed principle, which appeared in his 1988 book Object Oriented Software Construction.

  • A module will be said to be open if it is still available for extension. For example, it should be possible to add fields to the data structures it contains, or new elements to the set of functions it performs.
  • A module will be said to be closed if [it] is available for use by other modules. This assumes that the module has been given a well-defined, stable description (the interface in the sense of information hiding).[3]

At the time Meyer was writing, adding fields or functions to a library inevitably required changes to any programs depending on that library. Meyer’s proposed solution to this dilemma relied on the notion of object-oriented inheritance (specifically implementation inheritance):

A class is closed, since it may be compiled, stored in a library, baselined, and used by client classes. But it is also open, since any new class may use it as parent, adding new features. When a descendant class is defined, there is no need to change the original or to disturb its clients.[4]

Polymorphic open/close principle

During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other.

In contrast to Meyer’s usage, this definition 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.

Robert C. Martin‘s 1996 article «The Open-Closed Principle» was one of the seminal writings to take this approach. In 2001 Craig Larman related the open/closed principle to the pattern by Alistair Cockburncalled Protected Variations, and to the David Parnas discussion of information hiding.

Liskov substitution principle

Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replacedwith objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.). More formally, the Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping, that was initially introduced by Barbara Liskov in a 1987 conference keynote address titled Data abstraction and hierarchy. It is a semanticrather than merely syntactic relation; because, it intends to guarantee semantic interoperability of types in a hierarchy, object types in particular. Barbara Liskov and Jeannette Wing formulated the principle succinctly in a 1994 paper as follows:

Subtype Requirement: Let {\displaystyle \phi (x)} be a property provable about objects {\displaystyle x} of type T. Then {\displaystyle \phi (y)} should be true for objects {\displaystyle y} of type S where S is a subtype of T.

In the same paper, Liskov and Wing detailed their notion of behavioral subtyping in an extension of Hoare logic, which bears a certain resemblance to Bertrand Meyer‘s design by contract in that it considers the interaction of subtyping with preconditionspostconditions and invariants.

Principle

Liskov’s notion of a behavioral 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 (e.g. correctness).

Behavioral subtyping is a stronger notion than typical subtyping of functions defined in type theory, which relies only on the contravariance of argument types and covariance of the return type. Behavioral subtyping is undecidable in general: if q is the property «method for x always terminates«, then it is impossible for a program (e.g. a compiler) to verify that it holds true for some subtype S of T, even if q does hold for T. Nonetheless, the principle is useful in reasoning about the design of class hierarchies.

Liskov’s principle imposes some standard requirements on signatures that 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). Because 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.

Interface segregation principle

The interface-segregation principle (ISP) states that no client 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.

Importance in object-oriented design

Within object-oriented designinterfaces provide layers of abstraction that facilitate conceptual explanation of the code and create a barrier preventing coupling to dependencies.

According to many software experts who have signed the Manifesto for Software Craftsmanship, writing well-crafted and self-explanatory software is almost as important as writing working software.[4] Using interfaces to further describe the intent of the software is often a good idea.

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.

Origin

The ISP was first used and formulated by Robert C. Martin while consulting for Xerox. Xerox had created a new printer system that could perform a variety of tasks such as stapling and faxing. The software for this system was created from the ground up. As the software grew, making modifications became more and more difficult so that even the smallest change would take a redeployment cycle of an hour, which made development nearly impossible.

The design problem was that a single Job class was used by almost all of the tasks. Whenever a print job or a stapling job needed to be performed, a call was made to the Job class. This resulted in a ‘fat’ class with multitudes of methods specific to a variety of different clients. Because of this design, a staple job would know about all the methods of the print job, even though there was no use for them.

The solution suggested by Martin utilized what is today called the Interface Segregation Principle. Applied to the Xerox software, an interface layer between the Job class and its clients was added using the Dependency Inversion Principle. Instead of having one large Job class, a Staple Job interface or a Print Job interface was created that would be used by the Staple or Print classes, respectively, calling methods of the Job class. Therefore, one interface was created for each job type, which were all implemented by the Job class.

Typical violation

The Xerox case is an example of a clear violation (and resolution) of the Interface Segregation Principle, but not all violations are so clear cut.

A more commonly known example is the ATM Transaction example given in Agile Software Development: Principles, Patterns, and Practices and in an article also written by Robert C. Martin specifically about the ISP. This example is about an interface for the User Interface for an ATM, that handles all requests such as a deposit request, or a withdrawal request, and how this interface needs to be segregated into individual and more specific interfaces.

Dependency inversion principle

In object-oriented design, the dependency inversion principle refers to a specific form of decoupling software modules. When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details. The principle states:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

By dictating that both high-level and low-level objects must depend on the same abstraction this design principle inverts the way some people may think about object-oriented programming.

The idea behind points A and B 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 and it may be necessary to change its usage interface.

In many cases, thinking about the interaction in itself as an abstract concept allows the coupling of the components to be reduced without introducing additional coding patterns, allowing only a lighter and less implementation dependent interaction schema.

When the discovered abstract interaction schema(s) between two modules is/are generic and generalization makes sense, this design principle also leads to the following dependency inversion coding pattern.

Traditional layers pattern

n conventional application architecture, lower-level components (e.g. Utility Layer) are designed to be consumed by higher-level components (e.g. Policy Layer) which enable increasingly complex systems to be built. 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.png

The goal of the dependency inversion pattern is to avoid this highly coupled distribution with the mediation of an abstract layer, and to increase the re-usability of higher/policy layers.

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. Both layers should depend on abstractions that draw the behavior needed by higher-level layers.

DIPLayersPattern.png

In a direct application of dependency inversion, the abstracts are owned by the upper/policy layers. This architecture groups the higher/policy components and the abstractions that define lower services together in the same package. The lower-level layers are created by inheritance/implementation of these abstract classes or interfaces .

The inversion of the dependencies and ownership encourages the re-usability of the higher/policy layers. Upper layers could use other implementations of the lower services. When the lower-level layer components are closed or when the application requires the reuse of existing services, it is common that an Adapter mediates between the services and the abstractions.

Dependency inversion pattern generalization

In many projects the dependency inversion principle and pattern are considered as a single concept that should be generalized. There are at least two reasons for that:

  1. It is simpler to see a good thinking principle as a coding pattern. Once an abstract class or an interface has been coded, the programmer may say: «I have done the job of abstraction».
  2. Because many unit testing tools rely on inheritance to accomplish mocking, the usage of generic interfaces between classes (not only between modules when it makes sense to use generality) became the rule.

If the used mocking tool relies only on inheritance, generalizing the dependency inversion pattern may become a necessity. This has major drawbacks:

  1. Simply implementing an interface over a class isn’t sufficient and generally does not reduce coupling, only thinking about the potential abstraction of interactions can lead to a less coupled design.
  2. Implementing generic interfaces everywhere in a project makes it by far harder to understand and maintain. At each step the reader will ask themself what are the other implementations of this interface and the response is generally: only mocks.
  3. The interface generalization requires more plumbing code, in particular factories that generally rely on a dependency-injection framework.
  4. Interface generalization also restricts the usage of the programming language.
Generalization restrictions

The presence of interfaces to accomplish the Dependency Inversion Pattern (DIP) have other design implications in an object-oriented program:

  • All member variables in a class must be interfaces or abstracts.
  • All concrete class packages must connect only through interface or abstract class packages.
  • No class should derive from a concrete class.
  • No method should override an implemented method.
  • All variable instantiation requires the implementation of a creational pattern such as the factory method or the factory pattern, or the use of a dependency-injection framework.
Interface mocking restrictions

Using inheritance-based mocking tools also introduce restrictions:

  • Static externally visible members should systematically rely on dependency injection making them far harder to implement.
  • All testable methods should become an interface implementation or an override of an abstract definition.
Future directions

Principles are ways of thinking, patterns are common ways to solve problems. Coding patterns may be seen as missing programming language features.

  • Programming languages will continue to evolve to allow to enforce stronger and more precise usage contracts, in at least two directions: enforcing usage conditions (pre-, post- and invariant conditions), and state-based interfaces. This will probably encourage and potentially simplify a stronger application of the dependency inversion pattern in many situations.
  • More and more mocking tools now use code injection to solve the problem of replacing static and non virtual members. Programming language will probably evolve to generate mocking-compatible bytecode. One direction will be to restrict the usage of non virtual members, the other one will be to generate, at least in test situations, a bytecode allowing non-inheritance based mocking.
Implementations

Two common implementations of DIP use similar logical architecture, with different implications.

A direct implementation packages the policy 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 required by the high-level component are 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.png

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.

A more flexible solution extracts the abstract components into an independent set of packages/libraries:

DIPLayersPattern v2.png

The separation of all layers into their own package encourages re-utilization of any layer, providing robustness and mobility.

Examples

Genealogical module

A genealogical system may represent relationship between people as a graph of first level relationships between them (father/son, father/daughter, mother/son, mother/daughter, husband/wife, wife/husband…). This is very efficient (and extensible: ex-husband/ex-wife, legal guardian…)

But the high level modules may require a simpler way to browse the system: a person may have children, a father, a mother, brothers and sisters (including or not half brothers and sisters), grandfathers, grandmothers, uncles, aunts, cousins …

Depending on the usage of the genealogical module, presenting common relationships as distinct direct properties (hiding the graph) will make the coupling between the high-level module and the genealogical module by far lighter and allow one to change the internal representation completely without any effect on the using modules. It also allows one to embed in the genealogical module the exact definition of brother and sisters (including or not half ones), uncles … allowing one to enforce the single responsibility principle.

Finally, if the first extensible generalized graph approach seems the most extensible, the usage of the genealogical module may show that a more specialized and simpler relationship implementation is sufficient for the application(s) and helps you to make a more efficient system.

In this example abstracting the interaction between the modules leads to a simplified interface of the low-level module and may lead to a simpler implementation of it.

Remote file server client

Imagine you have to implement a client to a remote file server (FTP, cloud storage …). You may think of it as a set of abstract interfaces:

  1. Connection/Disconnection (a connection persistence layer may be needed)
  2. Folder/tags creation/rename/delete/list interface
  3. File creation/replacement/rename/delete/read interface
  4. File searching
  5. Concurrent replacement or delete resolution
  6. File history management …

If both local files and remote files offers the same abstract interfaces, any high-level module using local files and fully implementing the dependency inversion pattern will be able to access local and remote files indiscriminately.

Local disk will generally use folder, remote storage may use folder and/or tags. You have to decide how to unify them if possible.

On remote file we may have to use only create or replace: remote files update do not necessarily make sense because random update is too slow comparing local file random update and may be very complicated to implement). On remote file we may need partial read and write (at least inside the remote file module to allow download or upload to resume after a communication interruption), but random read isn’t adapted (except if a local cache is used).

File searching may be pluggable : file searching can rely on the OS or in particular for tag or full text search, be implemented with distinct systems (OS embedded, or available separately).

Concurrent replacement or delete resolution detection may impact the other abstract interfaces.

When designing the remote file server client for each conceptual interface you have to ask yourself the level of service your high level modules require (not necessary all of them) and not only how to implement the remote file server functionalities but maybe how to make the file services in your application compatible between already implemented file services (local files, existing cloud clients) and your new remote file server client.

Once you have designed the abstract interfaces required, your remote file server client should implement these interfaces. And because you probably restricted some local functionalities existing on local file (for example file update), you may have to write adapters for local or other existing used remote file access modules each offering the same abstract interfaces. You also have to write your own file access enumerator allowing to retrieve all file compatible systems available and configured on your computer.

Once you do that, your application will be able to save its documents locally or remotely transparently. Or simpler, the high level module using the new file access interfaces can be used indistinctly in local or remote file access scenarios making it reusable.

Remark : many OSes have started to implement these kind of functionalities and your work may be limited to adapt your new client to this already abstracted models.

In this example, thinking of the module as a set of abstract interfaces, and adapting other modules to this set of interfaces, allows you to provide a common interface for many file storage systems.

Model View Controller

Example of DIP

UI and ApplicationLayer packages contains mainly concrete classes. Controllers contains abstracts/interface types. UI has an instance of ICustomerHandler. All packages are physically separated. In the ApplicationLayer there is a concrete implementation that Page class will use. Instances of this interface are created dynamically by a Factory (possibly in the same Controllers package). The concrete types, Page and CustomerHandler, don’t depend on each other; both depend on ICustomerHandler.

The direct effect is that the UI doesn’t need to reference the ApplicationLayer or any concrete package that implements the ICustomerHandler. The concrete class will be loaded using reflection. At any moment the concrete implementation could be replaced by another concrete implementation without changing the UI class. Another interesting possibility is that the Page class implements an interface IPageViewer that could be passed as an argument to ICustomerHandler methods. Then the concrete implementation could communicate with UI without a concrete dependency. Again, both are linked by interfaces.

Related patterns

Applying the dependency inversion principle can also be seen as an example of the adapter pattern, i.e. the high-level class defines its own adapter interface which is the abstraction that the other high-level classes depend on. The adaptee implementation also depends on the adapter interface abstraction (of course, since it implements its interface) while it can be implemented by using code from within its own low-level module. The high-level has no dependency on the low-level module since it only uses the low-level indirectly through the adapter interface by invoking polymorphic methods to the interface which are implemented by the adaptee and its low-level module.

Various patterns such as Plugin, Service Locator, or Dependency Injection are employed to facilitate the run-time provisioning of the chosen low-level component implementation to the high-level component.

 

 

 

References:

https://en.wikipedia.org/wiki/SOLID_(object-oriented_design

https://en.wikipedia.org/wiki/Single_responsibility_principle

https://en.wikipedia.org/wiki/Open/closed_principle

https://en.wikipedia.org/wiki/Liskov_substitution_principle

https://en.wikipedia.org/wiki/Interface_segregation_principle

https://en.wikipedia.org/wiki/Dependency_inversion_principle

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *