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

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

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