SOLID Principles of Object Oriented Design

SRP: The Single Respnsibility Principle
  • The Single Responsibility Principle states that every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class
  • There should never be more than one reason for a class to change
  • Cohesion: how strongly-related and focused are the various responsibilities of a module
  • Coupling: the degree to which each program module relies on each one of the other modules
  • We strive for low coupling but high cohesion
  • You'll find that if you follow this principle, a lot of the times you'll have very few if statements, or switch statements, or branching logic in your code. Because you've isolated the things that would act differently into separate classes, that know how to behave in their specific environment and there context.
  • Related principles
    • Open/Close Principle
    • Interface Segregation Principle
    • Separation of Concerns
Open Closed Principles

The Open/Close Principle states that the software entities(classes, modules, functions, etc.) should be open for extension, but closed for modification

  • Open To Extension: New behaviour can be added in future
  • Closed to Modification: Changes to source or binary code are not required

The Problem

  • Adding new rules require changes to the function every time
  • Each change can introduce bugs and requires re-testing, etc.
  • We want to avoid introducing changes that cascade through many modules in our application
  • Writing new classes is less likely to introduce problems
    • Nothing depends on new classes(yet)
    • New classes have no legacy coupling to make them hard to design or test

Three Approaches to achieve OCP

  • Parameters (Procedural Programming)
    • Allow client to control behavior specifics via a parameter
    • Combined with delegates/lambda, can be very powerful approach
  • Inheritance / Template Method Pattern
    • Child types override behavior of a base classes (or interface)
  • Composition / Strategy Pattern
    • Client code depends on abstraction
    • Provides a "plug in" model
    • Implementations utilize Inheritance; Client utilizes Composition

When do we apply OCP?

  • Experience Tells You : If you know from your own experience in the problem domain that a particular class of change is likely to recur, you can apply ocp up front in your design
  • Otherwise - "Fool me once, shame on you; fool me twice, shame on me"
    • Don't apply OCP at first
    • If the module changes once, accept it. do the minimum thing that can fix it
    • If it changes a second time, refactor to achieve OCP
  • Remember TANSTAAFL
    • There Ain't No Such Thing AS A Free Lunch
    • OCP adds complexity to design
    • No design can be closed against all changes

  • OCP practice yields
    • flexibility
    • reusability
    • maintainability
  • Know when to abstract out interfaces, resis for premature abstraction
  • Related Fundamentals
    • Single Responsibility Principle
    • Strategy Pattern
    • Template Method Pattern

Liskov Substitution Principle

The Liskov Substitution Principle states that Subtypes must be substitutable for their base types

Substitutability

  • Child classes must not:
    • Remove base class behavior
    • Violate base class invariants
  • And in general must not require calling code to know they are different from their base type

Inheritance and the IS-A Relationship

  • Naive OOP teaches use of IS-A to descrive child classes' relationship to base classes
  • LSP suggests that IS-A should be replaced with IS-SUBSTITUTABLE-FOR

Invariants

  • Consist of reasonable assumptions of behavior by clients
  • Can be expressed as preconditions and postconditions for methods
  • Frequently, unit tests are used to specify expected behavior of a method or class
  • Design By Contract is a technique that makes defining these pre- and post- conditions explicit within code itself
  • To follow LSP, derived classes must not violate any constraints defined(or assumed by clients) on the base classes

The Problem

  • Non-substitutable code breaks polymorphism
  • Client code expects child classes to work in place of their base classes
  • "Fixing" substitutability problems by adding if-then or swith statements quickly becomes a maintenance nightmare (and violates OCP)

LSP Violation "Smells"


foreach(var emp in Employees)
{
    if(emp is Manager)
    {
        _printer.PrintManager(emp as Manager);
    }
    else
    {
        _printer.PrintEmployee(emp);
    }
}

public abstract class Base
{
    public abstract void Method1();
    public abstract void Method2();
}

public class Child : Base
{
    public override void Method1()
    {    
        throw new NotImplementedException();
    }
    public override void Method2()
    {
        // do stuff
    }
}

LSP Tips

  • "Tell, Don't Ask"
    • Don't interrogate objects for their internals - move behavior to the object. Have high cohesion. Behavior should be inside object and they should use those internal states.
    • Tell the object what you want it to do
  • Consider Refactoring to a new Base Class
    • Given two classes that share a lot of behavior but are not substitutable...
    • Create a third class that both can derive from
    • Ensure substitutability is retained between each class and the new base

  • Conformance to LSP allows for proper use of polymorphism and produces more maintainable code
  • Remember IS-SUBSTITUTABLE-FOR instead of IS-A
  • Related Fundamentals:
    • Polymorphism
    • Inheritance
    • Interface Segregation Principle
    • Open/Closed Principle

Interface Segregation Principle

The Interface Segregation Principle states that clients should not be forced to depend on methods they do not use.

ISP Smells

  • 
    public override string ResetPassword(string username, string answer)
    {
        throw new NotImplementedException();
    }
    
  • These violate LSP
  • Client references a class but uses small portion of it

When do we fix ISP

  • Once there is pain
    • If there is no pain, there's no problem to address
  • If you find yourself depending on a fat interface that you own
    • Create a smaller interface with just what you need
    • Have the fat interface implement your smaller interface
    • Reference the smaller interface in your code
  • If you find "fat" interfaces are problematic but you do now own them
    • Create smaller interfaces with just what you need
    • Implement this interface using an adapter that implements the full interface

  • Don't force client code to depend upon things it doesn't need
  • Keep interfaces lean and focused
  • Refactor large interfaces so they inherit smaller interfaces
  • Related fundamentals
    • Polymorphism
    • Inheritance
    • LSP
    • Facade pattern

Dependency Inversion Principle

  • 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

What are dependencies

  • Framework(.net etc)
  • Third party libraries
  • Database
  • File System
  • Email
  • Web Services
  • System resources(Clock)
  • Configuration
  • The new keyword
  • Static methods
  • Thread.Sleep
  • Random