Clean Code SOLID Principles

Notes about SOLID Principles & Clean Coding


Last Updated: April 04, 2021 by Pepe Sandoval



Want to show support?

If you find the information in this page useful and want to show your support, you can make a donation

Use PayPal

This will help me to create more stuff and fix the existent content... or probably your money will be used to buy beer


Clean Code SOLID Principles

My personal notes from Uncle Bob clean code lectures SOLID video series, they have been paraphrased or written in my own words just for my personal reference, in some cases adding my personal opinions about certain topics

SOLID Foundations

  • Architecture: the shape a system adopts to meet its use case
  • Details should depend on high level policies, policies should never not depend on details

  • The source code is the design

  • The running application or binary is the product and for SW building refers to compiling and executing so building is cheap in SW but design is expensive unlike other disciplines where building is much more expensive than design, for example think of design and build a house, in that case build is expensive compared to it's design

  • Source code dependencies should oppose the flow of control

OO definition

  • OO is about passing message when you pass a message you lose control on how that message is going to be interpreted you can only hope the receiver reacts appropriately, neither the sender nor the receiver depend on each other they both depend upon the message which is an abstraction

OO message example

  • Programming is about modeling the real world using SW but there is nothing special about OO that allows this

  • Encapsulation, inheritance and polymorphism are mechanism build within OO but they are not it's essential quality, its essential quality is the ability to invert dependencies to protect high level policies from low level details

Dependency management

  • SOLID principles control relationships and operations between classes, describe the way classes relate to one another, they are all about the dependencies about those classes and motivation to creating those dependencies
  • Component Cohesion Principles describe the forces that cause classes to be grouped as independently deployable components
  • Component Coupling Principles describe the forces that govern the dependencies between components

Bad Design Characteristics

Rigidity

  • Rigid systems are hard to change
  • The cost of making a change is high
  • A small changes requires us to rebuild and retest the whole thing
  • If a tiny change requires the whole system to be rebuild this is a symptom of tight coupling
  • We need to manage the dependencies correctly so when a module change the others are not affected (they don't need to be rebuild and retested)

Fragility

  • A fragile system is one where a change in module causes other unrelated modules to misbehave or break. E.x. in a car system if you fix a bug on the radio it affects the brakes
  • You make a change and break a lot of other unrelated functionality so you need to fix that which has a high cost
  • Fragility is usually caused by long distance dependencies between modules for this wee need to manage and isolate the dependencies between modules

Immobility

  • A system is immobile when the internal components of a system cannot be easily extracted and reused in new environments we have an immobile system
  • Modules cant be moved
  • Immobility is caused by dependencies to other modules and utilities

Viscosity

  • A system is viscous when essential operations like compile, build or test take a long time and/or are difficult to perform
  • If check-in, checkout, merges, releases is a hard and time consuming process this means we have viscosity in our system
  • If new features require we make changes at multiple levels of abstraction and require we deal with multiple transfer mechanism, this points to a viscous system

Needless complexity

  • This deals with the question "Should we put the hooks for the future or not?"
  • A system that carries a lot of anticipatory design tend to become needlessly complex, each hook or extension point for the future needs to be carried in the present
  • What we need is to maintain a good set of tests so we are not afraid to change the code

Single Responsibility Principle (SRP)

  • SRP is about functions and modules which should have only one responsibility
  • SRP is about Users and the responsibilities the functions and classes have to the users of these functions and classes,the users that will require changes to those functions and classes
  • A responsibility is a source of change
  • The responsibilities SRP refers to the responsibilities your SW has to all the different groups of people that it serves
  • We need to separate the users of our SW from the roles they play, responsibilities are tied to a role/actor
  • When the needs of a actor change the functions and classes that serve that actor will need to change
  • A responsibility is a family of functions or classes that serves the needs of one particular actor

Actors image

  • We need to gather things that change for the same reasons and separate the things that change for different reasons
  • We need to identify the actors and what are the responsibilities that serve those actors, then we allocate those responsibilities in functions of certain modules so each module has only one responsibility
  • The goal is to separate responsibilities into physical locations in the code where single responsibilities exists
  • Classes have responsibilities to their users
  • Separation of responsibilities Options: SRP Implementation Options

Open Close Principle (OCP)

  • States that a SW module should be open for extension but closed for modification
  • Open for extension means changing the behavior of that module should be easy but Closed for modification means the source code should not change for this
  • the OCP wants us to be able to change the behavior of module without changing the actual code of the module
  • To implement this we need to separate the extensible behavior of a module, in an abstraction interface
  • Designing a whole system that conform to the OCP it is possible but usually so hard that it is not practical however we can create, modules, classes and functions that do conform to the OCP
  • The goal is not to completely eliminate the pain of change we just need to minimize it
  • Crystal ball problem: OCP can only protect you from change if you can predict the future. This means it is easy to design abstractions for future changes if you have some idea of how those changes are going to be. There are usually two solutions for this problem
    • Big Design Up Front (BDUF) carefully consider the customer and problem domain to anticipate customer needs having abstractions to isolate the components but are costly and complex to follow
    • Agile Design you get the simplest design working and put it in front of the customer so the customers can start shooting at it with requests, changes and requirements then you make sure to know what are the things that are likely to change so you can protect those parts, wait for the customer to make changes to create an abstraction that protect you from those type of changes in the future

Liskov Substitution Principle (LSP)

  • States that you should be able to substitute a base type for a derivative type and everything must still work

  • A Type is just a bag of operations, because It doesn't matter what is contained on a type what matters are the operations that can be performed on that type (the results of those operations).

    • We don't care of what is inside a type we care about what it can do
    • E.x. you don't care how an int is stored, if it use one's or two's complements, where is the sign, etc as long as the int that represents 1 added to the int that represents 2 results in the integer that represents 3, then everything is fine -E.x. you don't care about the internal representation or structure of a float what you care is that 1.0/2.0 results in 0.5
  • A class from the outside is just a bunch of methods the data is just hidden behind those methods

  • Subtype new type that can be used as another type (inheritance -> substitutable for its parent type)

  • Subtypes can be used as their parent types. You have a user U that uses T if they can use T they should be able to use S without knowing it

Subtype example

  • In dynamically typed languages we send messages (instead of calling methods but effect is the same), until runtime if the object can respond to the message, the method is invoked otherwise a runtime errors occur

  • Refuse Bequest occur when

    • We call a method that doesn't exists
    • A method form the derived class that is being used by a method in the base class throws an exception that the base class does not expects
    • The derived class causes a side effect the base class does not expects
  • Representatives of things do not share the relationships of the things they represent this is why a square is a rectangle but the representative SW that represent a rectangle and a square do not share this relationship. When you create models in SW, these models are mere representatives

  • The complex-real-integer representation is a real world model that makes sense in real world but makes no sense at all in SW complex-real-integer representation example

  • if we create a list of circles which are a derived of class shape, we can not pass that list to a method that expects a list of shapes, because that function may put a Square or a Triangle in the list and although those can also inherit from shape, having a Square or a Triangle in a list of circles do not make sense, other modules would expect to call !methods for Circles in the list!

List example

  • Key points to avoid LSP violations:
    • If the base class does something the derived class must do it in a way it doesn't violate the expectations of the caller, so we cannot take expected behaviors away from a subtype, a derived class can do more but never less
    • If you have functions that don't do anything in the derived class and those are implemented in the base class you most likely have a LSP violation
    • If a method in a derived class always throws an exception, this is likely a LSP violation because the author of derived function doesn't want you to call it
    • The presence of if isinstance(myobj, list) could be a LSP violation, you should only check the type if you already know what type it is because we are dynamically loading an object
    • The Typecase scenario (use if-elif-else asking for types) is likely an LSP violation because is not scalable and should be replaced with polymorphic dispatch (visitor pattern)

LSP example

  • Design by contract Every type has invariants that can be stated as Boolean expressions Boolean expressions, every method can be surrounded by precondition (Boolean expression that must be true before method can be called) and postcondition (Boolean expression that must be true after method is called or returns)

Design by contract example

  • If we need to add a hack make sure all the dependencies point away from it, instead of having modules depend on the ugly hack

Adapter example

Interface Segregation Principle (ISP)

  • Don't depend on things you don't need, Don't force your users to depend on things they don't need, users in this context can be (modules, people, tests, etc), don't force them to know more that they need to know

  • Inheritance is the strongest of the physical coupling but it is the weakest of the logical couplings

  • Interfaces has more to do with the classes that use them than with the classes that implement them

  • The class that uses the interface (The Switch) is the one that has the tight logical coupling cause this class can't do its job without the interface but the class that implements the interface (The light) it is forced to implement the interface but if the interface disappears from its view it doesn't really affect it, while the interface doesn't even know its implementation (The Light) exists

    • The class that uses the interface and the interface must be deployed together in a package
    • The class that implements and interface should be deployed separately (this becomes a plugin to the other package)
    • An interface has more to do with it's users than with it's implementers

Adapter example

  • An interface is just an abstract class with abstract methods in it

  • The Deadly Diamond of Death caused by multiple inheritance is not that complicated to avoid so languages that don't support multiple-inheritance are crippled languages

  • The ISP addresses the problem of Fat Classes (classes with lots of methods) and making sure you don't depend on methods you don't call

Dependency Inversion Principle (DIP)

  • Break a big monolithic program and turn it into a bunch of independently deployable modules

  • Invert the direction of the source code dependencies, try to make source code dependencies oppose flow of control (flow of execution)

  • Dependencies:

    • Run Time Dependencies exists between two modules whenever these two modules interact, for example when:
      • The flow of control leaves one module and enter another one
      • A module access the variables from another module
    • Source Code/Compile Time Dependencies when a name is defined in one module but appears in another module, the last module has a dependency of the definer module, for example:
      • Define a class in one file then in another file you define another class which use the first one

Adapter example

  • Structured design is a top down methodology: You start with main then design the sub-routines main should call, then the subroutines those other subroutines will call and so on. This leads to a design where the source code dependencies are an exact mimic of the runtime dependencies, high level functions call the low level functions which then call lower level functions and so on...

  • We don't want the source code dependency to follow the run time dependency

  • Using interfaces removes the source code direct dependency, it still has a runtime dependency, classes that uses and implement the interface will have a source dependency on the interface

  • Dependencies are inverted whenever the source code dependencies oppose the flow of execution/control

  • Inverting dependencies is the means by which we create boundaries between SW modules, which allows to create plugins
    • plugin is a module that is anonymously called by another module, the caller has no idea what or who is calling
  • High level policy should not depend on low level details, while low level details should depend on high level policy

  • Modules that contain high level policies, like use cases, should not depend on modules than contain low level details (like DB, web formatting) and yet these high level modules must eventually invoke the low level functions

Dependency Inversion

Building a reusable framework is HARD! if you don't build it in parallel with more than one application that reuses it will most likely fail

  • OS provides device independence, this means you should be able to access the device (read & write) without knowing what kind of device it is. IO drivers are just plugins to the OS, all their dependencies point towards the OS and the OS has no dependencies on the drivers

  • A good app architecture is a plugin architecture which is achieved through careful and a decisive use of the Dependency Inversion Principle

  • DIP is the inversion of source code dependencies against the flow of control (the callable object depends on the caller)

SOLID Principles Summary

  • General design process:
    1. Try to enumerate as many possible requirements and use cases as possible
      • Enumerate a list of use cases (functionality)
      • Enumerate Data elements (entities) you have identified so far
    2. Assemble them into a use case driven arch (Any architecture)
      • Break the requirements and use cases into a bunch of classes
      • Ask who are the actors of the app? who uses it and name those: we want to separate the modules so each module is responsible to only one actor
    3. Choose a couple of use cases to do a deep dive of the design
      • Draw a couple of diagrams just to show in general the relationship between classes, don't need to add all the details
      • Don't try to spend too much time creating diagrams with all the details unless you want will need to communicate your idea to other people with a lot of technical details Go fast to code something that can be tested, and create tests for it
    4. Implement the use cases defined
    5. Repeat from step 1 until design and arch is complete

No design is fully SOLID compliant, no design should be fully SOLID compliant, when you ignore a principle you should know which principle you are ignoring and do it deliberately. Principles illuminate design issues and recommend solutions but are not immovable laws, respect them but down vow to them

SOLID Components

  • A component is an independently deployable library (piece of code) like a .jar, a .dll, a .gem, a shared library, etc
    • independently deployable means a change in this library doesn't cause other components (pieces of code) to be recompiled or redeployed. E.x. a library I use in my code is usually independently deployable I can change the application without recompiling or redeploying the library and vice versa
  • Components are a larger scale entities made up of many classes, functions, variables

  • Applications call libraries (sub-routines), frameworks call applications

Libraries and Frameworks

  • Noun-Verb Analysis General design process to decompose requirements into an object oriented design
    • This process usually lets to abstractions of real world objects with classes that represent those objects
    • It usually results on designs where the abstractions don't really help the system to be scalable, testable and flexible
    • You end up with a bunch of classes, each will represent real world objects but these classes might not share the relationships of the things they represent so the relationships in software will end up being useless, adding unnecessary complexity or even making the system rigid and inflexible
  • SOLID design Example:
    • Requirements: Design the software that controls a coffee make machine, you turn it on with button, signal alight when coffee is ready, you have boiler with a sensor to know if there is water and an actuator to turn the heat on/off on the boiler (heating element), water evaporates passes through tube fall into coffee beans, passes trough a filter and falls into a pot (you can control/stop flow of water using a actuator valve on the boiler), the pot that has a sensor to know if there is an empty pot, pot with coffee or no pot, you can control the heat on the pot as well to keep coffee warm using a warmer plate where the pot sits on
    • Design process Example:
      1. First apply the SRP to identify the actors (Groups of people or users that will request changes to the system) so you can separate the responsibilities
        • Common Brewer: Person who decides to make coffee, he is the one that fills the boiler with water and pushes the button, he is interested in the User Interface, will most likely request changes button or light
        • Now Drinker: actor that wants to remove the pot from the plate at any time, while coffee is still brewing
        • Hot drinker Interested in the temperature of the brewed coffee
      2. Define modules for the actors, we should at least have one module for each actor and Design high level policy, avoid mentioning low level details, we want to describe abstract purpose of the components
      3. We need to define the relationship between the modules, still at the high level, for that we need to go up and down the abstraction layer, and define the messages each module will send to other modules
      4. Start implementing using the OCP,we should try to get to a design where we are independent and interchangeable components (set of classes) SOLID design Example

Component Cohesion

  • Cohesion: forces that glue together classes into components

  • Components that contain details must depend on the components that contain high level policy not the other way around

  • The fact a group of classes work together to achieve a certain goal is not necessarily a good reason to bind them together into a component
    • Base classes and their derivatives are often packaged in separate components, while classes that use a base classes are usually packaged with the base class
  • Release-reuse Equivalent Principle (REP)
    • In order for a component to be reused it must follow release process
    • the smallest thing you are willing to release is also the smallest thing anyone will be able to reuse
    • The granular or reuse is the granular of release
  • Common Closure Principle (CCP):
    • Gather the classes that change from the same reasons and separate classes that change for different reasons (reasons are responsibilities)
    • The classes we group together into components should be close against the same kinds of changes
    • Given a change in a requirement we want only one component per layer of abstraction to change
    • Create systems that serve multiple actors, those system have architectural layers that separate high level policies from details, the layers are partitioned into components and each component is composed of classes that serve the actor
    • It's purpose is to minimize the number of components that change when the requirements change
  • Common Reuse Principle (CRP):
    • When you depend on a component try to depend on all the classes within that component
    • Group classes that are used together and separate classes that aren't used together
    • Construct your components so when you use a component you use all the classes within that component
    • Avoid making components that depend on classes they don't use
  • Systems are made up of layer that are made up of components that contain classes that house functions/methods which server single actors/users of the system

  • REP, CCP and CRP principles are not cooperative in nature

Triangle tension

Component Coupling

  • Coupling: the forces that bind together components into systems
  • Morning after syndrome when you work on something make it work and the next day it is not working because somebody else changed something you depend upon
    • Weekly build: solution that states all developers pull code on certain day (i.e. Monday) and on another certain day they do integration (i.e. Friday), integration could result on pushing the next pull day because it can take more that expected, you can end up pushing this to next week next month, etc.
  • Acyclic Dependencies Principle (ACP)
    • A software system is composed of components, the dependencies between those components must not form a cycle (if you start in one component and follow the dependencies you must not end in the same component)
    • The dependencies of the components should form a directed acyclic graph
    • Arrows between components must never be in a circle, arrows must point in a downward direction
  • Stability: is measured by the amount of work needed to change its state therefor it can be an analog value
    • Something that is hard to change is stable. Something that is easy to change is unstable
    • Sometimes we want instability or components that are easy to change so we can put the code that is likely to change there
    • To measure the stability (I) we can count the number of classes that depend on a certain class (Fan-in) then we can count the number of classes a certain class depend (Fan-out), then we can take the ratio of these two number I=Fan-out/(Fan-in+Fan-out) this will result around 0 for very stable (hard to change=a lot of components depend on this) and around 1 for unstable (easy to change=depends on lots of components)
  • Stable Dependencies Principle (SDP)
    • A component should only depend on other components that are more stable than it is (dependencies should point in the direction of increasing stability)
    • When someone changes something you depend upon make sure this change hurts them more that it hurts you
    • If we make sure the code is clean and unit tested the only thing that can make it unstable are the dependencies
    • All stable components should only depend on more stable components
    • All the dependencies arrows should point in the direction of decreasing I
  • The fact that a module is hard to change doesn't mean it is hard to extend
  • Stable Abstraction Principle (SAP)
    • Components should be as abstract as they should be stable
    • The more stable a component should be the more abstract it should be so it can be open for extension
    • The stable components at the end of components dependencies graph should be very abstract, while unstable at the top of the dependencies graph should be more concrete (unstable and easy to change)
    • A component is composed of several classes some will be abstract other will be concrete and we can calculate how abstract a component is by taking the ratio A=Abstract Classes/Total Classes, this will result on around 1 for very abstract
    • We try to make A + I = 1 which means stuff at the top is concrete and the stuff at the bottom is abstract
    • Concrete components that a lot of people depend upon is a zone of pain SAP

Components Case Study

  • Controllers use builders to create request data structures that they pass to interactors that they acquire through factories, interactors manipulate entities and the database in order to create response data structures, these response data structures are passed into presenters which create view models data structures which are then driven into the views

  • Move classes around components as the code is created, this is signal of a good refactoring discipline

Components Case Study

References

Want to show support?

If you find the information in this page useful and want to show your support, you can make a donation

Use PayPal

This will help me to create more stuff and fix the existent content... or probably your money will be used to buy beer