SOLID Principles

May 15, 2020

What is SOLID job

Interface Segregation (ISP)

The first of the solid design principles and perhaps the most useful is the interface segregation principle. The interface segregation principle says no client should be forced to depend on methods it does not use.
A client should depend on the smallest set of interface features: the fewest method and attributes.

The wrap vs. extend decision

Object-oriented design always involves making the wrap versus extend decision. Interface segregation tends to come down firmly on the side of wrapping any generic built-in class. This leads to classes which delegate their methods to the built-in class. The wrapping technique provides a limited interface.

Why Wrap? Creating a wrapper segregates the interfaces for the application classes. Other collaborating classes only need the four special methods and nothing more. The underlying class that’s being wrapped can be any mutable sequence. There’s more to this, though.

There is always a decision to wrap or to extend an existing class. The interface segregation principle suggests it’s better to wrap a generic class, and thereby narrow the interface.

Liskov Substitution (LSP)

A proper design satisfies the rule that it’s possible to replace objects of some superclass S with objects of any subclass of S without altering any of the provable properties of that program, that is without breaking it. This is named for Barbara Liskov who originally called the idea strong behavioral subtyping. The idea is that the behavior of a subclass should be as correct as the behavior of the superclass.

The idea behind this is that an application will pick one of these functions or objects built from the classes during initialization. The decision might be based on command line arguments or a configuration file. Since the alternative implementations are all substitutable, no rebuilding of the application is required to make a selection among the available choices. This permits late binding, the final decision of what algorithm to use doesn’t have to be made until run time. A design includes properly substitutable classes when a simple choice among classes can be made at runtime. No code changes are required. There’s no rippled effect from making a change. This design also fits the open closed principle.

It’s essential to write unit tests to confirm that Liskov substitution really works. The Liskov Substitution Principle helps manage the subclass, superclass relationship. When a subclass can be used anywhere a superclass is used, the design fits the principle. A class can be replaced without a damaging ripple effect, that causes other problems in the application.

Open Close (OCP)

The open/closed principle comes from Bertrand Meyer’s book. Object-Oriented Software Construction. It says software entities, classes, modules, functions, et cetera, should be open for extension, but closed for modification. The open/closed principle is closely aligned with the interface segregation principle and the Liskov substitution principle. ISP segregates interfaces based on the needs of the client classes. This often creates a class that’s largely closed. Doesn’t need further modification. LSP assures that the subclasses are proper extensions of the superclass. This means the superclass is open to extension. The open/closed principle summarizes the goals of ISP and LSP. A design that has interface segregation and Liskov substitution principles applied tends to have classes to which the open/closed principle also applies.

The open/closed principle promotes adding features to a class by creating subclasses. There should be no need to tweak the code of a working class. Any tweaking indicates a failure to design a class that’s properly closed to modification.

Bug fix

The idea of closed to modification sometimes leads to arguments over bug fixes. I’m going to suggest that a class should be closed to modification even to bug fixes. What is a bug fix? It’s a modification to a faulty class definition. If the class is truly closed to modification, what about the possibility of disruptive ripples unfolding from the change? Ideally, a bug fix has no ripples. Start with a bug report, create a new unit test that now fails, revise the code to be sure the unit test passes, hey presto, it’s fixed. Not so quickly. Pragmatically, a design change may be made here to fix a bug there. This is common because buggy behavior often emerges from the interaction of multiple objects. Bugs may not be confined to the internals of a single class. When making a change here to fix a bug that arises somewhere else, the whole point is to leverage some ripple effect that fixes the problem, and of course, this might introduce new problems. To minimize destructive ripples from a change, consider this alternative approach to bug fixes. Where possible, extend a class to introduce a bug-free subclass. Not all bugs can be fixed this way. When a bug can be fixed by extension, this approach assures there are minimal ripples. Since this fix creates a new subclass, it’s possible to leave the old buggy class in place. If the Liskov Substitution Principle is followed, the substitution of good for bad, will work out well. Extending a class to fix bugs will minimize the ripples. The class remains closed. Leaving the buggy class definitions in place leverages the idea of late binding. The run-time bindings provided by configuration files, or command line parameters, avoid the buggy code. What possible reason can there be to leave it there? Consider the history of the bad code. Bad code did go through some quality assurance. It appeared good enough to be released. When a bug surfaces, it’s often because of a newly discovered edge case, perhaps an unforeseen user story. A process of discovery revealed new uses for the app, and the unforeseen problem. A new story means re-factoring a design, more than fixing a bug. The new design means consideration of all the solid principles. It’s handy to mark a buggy class as deprecated. The idea is to remove it later when a major design change is made. The warnings module is generally used for this. In some languages, a class is closed to modification because it’s frozen as compiled object code.

Here’s the suggestion for bug fixes based on the open/closed principle:

  • Modify existing code reluctantly. 
  • Extend code with subclasses eagerly.

The open-closed principle looks at each class as being closed to modifications, but open to future extensions. The idea is to avoid tweaking the source code. The focus for all modifications should be some kind of extension to previously released classes.

Dependency Inversion (DIP)

Two Elements, One Theme

The dependency inversion principle has two elements:

  •  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. 

There’s a common theme to these two elements: 

  • depend on abstractions. 

The inversion part of this principle makes a distinction between two views: 

  • The “direct” dependency where a concrete class depends on another concrete class 
  • “inverted” dependency where a concrete class depends only on an abstract interface.

Factories

The interface segregation principle suggested defining factory functions to segregate construction from essential behavior. The open closed principle encouraged factories and extension to a class can be paired with an extension to the factory, the only substantial change is to replace the old factory with the new extended factory. The dependency inversion principle also encourages factories. A factory means that exactly one function or class depends on a concrete class name. Code that depends on concrete classes can be difficult to change. Any extension which creates a new concrete class causes ripples in the form of rewrites to all the software that depended on the old concrete class. When depending on abstractions, a new concrete implementation is not disruptive.

Dependency Injection

It’s not possible to avoid all references to concrete classes. When an app runs, it will need to use a concrete class name. Somewhere, somehow, a class has to be named. Application code should only mention abstract classes. This leads to the idea of injecting the concrete class names into the application. The idea is that the concrete names are only mentioned in a separate configuration 

Testing

One of the most important consequences of the dependency inversion principle is suggesting that a good design permits replacing an application object with a mock object.
A design that doesn’t follow the dependency inversion principle will often be more difficult to test. It will be difficult to inject mock objects. Test-driven development is an important guiding principle. In fact a feature without a test case doesn’t exist. It’s unwise to use a feature that doesn’t have a fully automated test. Ideally developers write a test case then write codes of the test passes. If necessary they can refactor the working code to improve non functional requirements like performance or memory use. It’s hard to emphasize how important it is to have test cases in place before attempting to refactor the code. Code without unit tests is a hazard. There is no real point in refactoring code that doesn’t already work.

Without the unit tests in place, refactoring and performance tuning is a high risk activity. With working unit tests on the other hand, it’s possible to refactor confidently. Once the code works it can be made more elegant or made to perform better with little risk of damage. Any ripple effects that might break something else are confined.

Single Responsibility

The single responsibility principle says a class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. A common paraphrase of this is a class should have only one reason to change

This raises an interesting question. 
What does single mean in this context? 
Does single mean a whole boat or one mast or one sail? 
What’s single? 

The question is really about finding the right level of abstraction. The goal is to have a class which can be summarized with a succinct responsibility statement. The statement distinguishes one class from all the other classes in the application. 

GRASP

Membership in the class is unambiguous. A class which models a ship as a whole will be a composite object that includes hull and rig. The parts will also have single responsibilities but in a different level of abstraction. What’s a responsibility? It can help to take hints from the general responsibility assignment software principlesGRASP. For more information, read Craig Larman’s book, Applying UML and Patterns. GRASP provides a set of concepts that can be used to sort out relationships among classes. 

The GRASP concepts can help to identify a single responsibility for a class. The idea is to consider a class from a number of points of view. GRASP and solid principles provide ways to articulate the design issues. They help determine what is the best approach in the context of the given problem and solution technology. The single responsibility principle suggests responsibilities must be encapsulated and a class should have one responsibility. A class should have one reason to change. It can be difficult to determine the right level of abstraction to apply to that single responsibility. It can help to use the GRASP, the General Responsibility Assignment Software Principles to help identify a single responsibility for a class. 

Controller & Creator

There are a number of principles for determining what counts as responsibility. I’ll look at just two, controller and creator. 
A controller class is responsible for all the processing to complete a use case or story. In a Badger command line context a controller could process a whole file of data. In a graphic user interface context, a controller could implement a button click or a keyboard event.

Another kind of responsibility is a creator. This is a class or function that is responsible for creating other objects. Many of the examples have included factories to create individual objects and builders to create complex objects. 

Interface segregation principles suggests that construction be segregated from ordinary essential operations. The dependency inversion principle suggests classes depend on abstractions, not other concrete classes. Both of these principles suggest that a creator is an important kind of single responsibility. 

The solid single responsibility principle suggests that classes be focused, but what does single really mean? The hard part is locating an appropriate level of abstraction. The general responsibility assignment software principles provide a way to identify the single responsibility for a class or module. Controllers and creators are two ways to define a single responsibility.

High cohesion and indirection

There are a number of principles for determining what counts as responsibility. Here I’ll talk about two more concepts, using high cohesion as a reason for combining objects, attributes, or methods, or using indirection that is a bridge or a link that separates objects.

The idea of high cohesion is a way to look at combining features. It may be more cohesive to make a single class, which is responsible for closely related features. For example, the two methods that return the hard and soft point values for Blackjack cards seem cohesive. They seem to belong with each other and other features of a card. They’re not related to shuffling or dealing. In this case, some highly cohesive features are combined into a single responsibility. While it’s often clear that some elements of a class are cohesive, a poor choice of abstractions is what can lead to problems. For example, assuming that house rules for Blackjack always allow splitting a hand is a poor design. It’s not really a cohesive feature of the game because splits aren’t allowed in some houses. It turns out some rules belong to the house, other rules belong to the game. It can be challenging to locate the boundaries that define cohesive. Sometimes it feels like splitting hairs.

The indirection principle often leads to classes that have responsibility for a relationship between objects. In many cases this also involves the pure fabrication principle that we’ll look at later. The new class is not part of the problem domain, but exists only to support a technical consideration. One common example of indirection is an access layer. This is a class that provides a uniform interface for different databases or different file formats. This is an abstract class that handles an indirect relationship. It connects a file path and a stats_document. The idea is that a concrete subclass of this will be used to save statistical summaries to the file system. The dump method is left unspecified in this abstract version because it will vary with different file formats. Here’s a concrete subclass of StatsDB. This will save stats_documents as JSON formatted files. I can now create another concrete subclass that saves data to CSV files or any other format that’s required. This isn’t the data generating game simulation, nor does it actually handle persistence. The access class creates an indirect connection between the simulation and the file system. When indirection is part of a design, then the dependency and version principle also becomes important.

High cohesion can be a helpful concept for combining features into a single class. Indirection can be a good reason to separate features into distinct classes.

class responsibility collaborators (CRC)

Way to start solid design is to identify all of the nouns in the problem domain. Each noun may be a class of objects or it may be a property of an object. It’s often pretty easy to identity things like card, deck, shoe, hand, dealer, et cetera from a problem statement and user stories. A static structure diagram can show how the various candidate classes relate to each other. Get a stack of paper cards. Annotate the cards to describe the classes. Each card has a class name on the top, a list of detailed responsibilities and a list of collaborators. For first time developers, it can sometimes help to get a pad of sticky notes to represent objects of each class. 

The cards will start like this. 

Details will be added during walk-throughs of different scenarios. Each paper card has a class name, a summary of the responsibilities and a list of collaborating classes. Step through the scenarios that must be implemented. Examine the cards to see if the responsibilities and the collaborators on each card are correct and complete. It may require several passes to suggest designs, find scenarios that don’t work and suggest changes. Once the scenarios seem to work, then the solid principles can be reviewed for each card. Dependency inversion is mostly focused on code, so it can be differed until later.

TDD

The other technique that I think can help assure solid principles are followed is test-driven development. I find that a focus on unit testing helps create a better design. The guiding idea behind test-driven development is simple. If there’s no automated test, the feature doesn’t really exist. 

This guiding idea provides a way to define done for any particular feature. Software is done when it passes the automated acceptance tests. This means the definition of done can be formalized as a suite of test cases. Acceptance-test-driven development tries to write the final acceptance test cases first, code and unit tests can then follow. Test first development is the general idea of writing all test cases first, and then creating code that generates the expected results. 

It’s often helpful to describe software tests using given, when, then statements. Given some class in an initial state, when some interaction or method is evaluated, then there’s the expected result. 

Leave a Reply:

Your email address will not be published. Required fields are marked *