Skip to content
ilyana.dev
TwitterGithubLinkedIn

Notes from SOLID Principles of Object Oriented Design Pluralsight Course

software development, coding, SOLID, principles, pluralsight11 min read

solid principles pluralsight

Steve Smith's Pluralsight course on SOLID Principles of Object Oriented Design is a great overview of the 5 SOLID principles, as well as the Don't Repeat Yourself Principle.

I recently completed SOLID Principles of Object Oriented Design by Steve Smith. Here are my notes:

Single Responsibility Principle

  • Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class
  • Strive for cohesion - strong focus of responsibilities within a module or class
  • Strive for loose coupling - the degree to which each program module relies on each of the other modules
  • Responsibilities = axes of change
    • more responsibilities = higher likelihood of change
    • multiple responsibilities within a class couple together those responsibilities
    • more classes sharing a responsibility leads to a higher likelihood of errors when that responsibility changes
  • You can use inheritance in object-oriented programming to achieve this
  • Many small classes, each with distinct responsibilities -> more flexible design
  • Related to Open/Closed Principle, Interface Segregation Principle, and Separation of Concerns
  • Recommended reading: Clean Code by Robert C. Martin

Open/Closed Principle

  • Software entities should be open for extension, but closed for modification
    • You should not have to dig around in the internals of your software just to extend it/change its behavior
    • "Open chest surgery is not needed when putting on a coat"
  • New behavior/behavioral changes can be added in the future, but changes to source code should not occur
  • Rely on abstractions -> no limit to implementations of each abstraction
    • abstractions = interfaces, abstract base classes, parameters, etc.
  • Messing with source code introduces bugs which can cascade through many modules of the project
  • Writing new classes is less likely to introduce problems because nothing in your project depends on those classes
  • 3 approaches to achieving the OCP
    • Parameters (in procedural programming) - allow client to control behavior specifics via a parameter (passing a state string, etc.); can be combined with delegates/lambda
    • Inheritance/Template Method Pattern - child types override behavior of a base class or interface
    • Composition/Strategy Pattern - client code depends on abstraction; provides a plug in model; implementations utilize inheritance, client utilizes composition
  • When to apply OCP? What does your experience tell you?
    • If you know from experience that a certain type of change is likely to occur, apply OCP up front
    • Otherwise, don't apply OCP at first. Don't apply it the first time the module changes. Apply it if the module changes a second time
    • TANSTAAFL - There Ain't No Such Thing As A Free Lunch
      • OCP adds complexity and shouldn't be used unless it's actually adding value
  • OCP yields flexibility, reusability, maintainability
  • Related to Single Responsibility Principle, Strategy Pattern, Template Method Pattern
  • Recommended Reading: Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin

Liskov Substitution Principle

  • Subtypes must be substitutable for their base types
  • Substitutability requires that:
    • Child classes must not remove behavior from their base class
    • Child classes must not violate base class invariants
    • Child classes must not require calling code to know they are different from their base type
  • Inheritance: "Is a" relationship vs. "is substitutable for" relationship suggested by LSP
  • Invariants - related to integrity of model that classes represent
    • consist of reasonable assumptions of behavior by clients
    • can be expressed as preconditions and postconditions for methods
    • unit tests are often used to specify expected behavior of given method or class
    • Design by Contract - technique that requires explicit definitions of pre- and postconditions within code
    • to follow LSP, derived classes must not violate any constraints defined by or assumed by clients of the base classes
  • Non-substitutable code breaks polymorphism
    • Fixing this with if/then or case statements violates OCP
  • LSP Violation Smells
    • if/else statements looking to figure out what type of subclass an object is
    • child type inherits from base class but does not fully implement it
      • Follow Interface Segregation Principle to prevent this
  • When to fix LSP violations?
    • if you notice obvious smells
    • if you find yourself being repeatedly bitten by OCP violations
  • "Tell, Don't Ask" - don't interrogate objects for their internals, move behavior to the object; tell the object what you want it to do
  • Consider refactoring to a new base class, given classes that share a lot of behavior but are not substitutable (Square and Rectangle, for example)
    • create a third class both can derive from
    • ensure substitutability is retained between each class and the new base
  • LSP allows for proper use of polymorphism and produces more maintainable code
  • "Is Substitutable For"
  • Related to polymorphism, inheritance, interface segregation principle, open/closed principle
  • Recommended Reading: Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin

Interface Segregation Principle

  • Leads to more cohesive modules with fewer hidden dependencies
  • Clients should not be forced to depend on methods they do not use
    • Prefer small, cohesive interfaces to fat interfaces
  • Smells that indicate violation of ISP:
    • unimplemented interface methods (also violate LSP)
    • client references a class but uses only a small portion of it
  • When to fix ISP:
    • Once you're experiencing problems
    • If you are depending on a "fat" interface you own, and this is causing problems
      • create smaller interface with just what you need
      • have fat interface implement your new interface
      • use new interface with your code
    • If you find "fat" interface that is problematic but you don't own it
      • create smaller interface with just what you need
      • implement this interface using adapter that implements full interface
    • Keep interfaces small, cohesive, and focused
    • Let client define interface whenever possible
    • Package interface with the client whenever possible
      • Alternately package in a third assembly that both client and implementation depend upon
      • Last resort: package interfaces with their implementation
    • Don't force client code to depend on things it doesn't need
    • Refactor large interfaces so they inherit from smaller interfaces
    • Related to polymorphism, inheritance, LSP, Facade Pattern
    • Recommended Reading: Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin

Dependency Inversion Principle

  • Really important for object-oriented software
  • 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?
    • The framework you're writing in
    • Third-party libraries being used
    • Database
    • File system
    • Email
    • Web services
    • System resources (like a clock, for example)
    • Configuration
    • The new keyword (except when instantiating primitive types)
    • Static methods
    • Thread.Sleep
    • Random - it's hard to test code that's supposed to give random results!
  • Traditional Coding -> Lots of dependencies!
    • High level modules call low level modules
      • i.e. UI depends on Business Logic depends on Database, etc.
    • Static methods are used for convenience
    • Class instantiation/call stack logic is scattered through all modules
      • Violates Single Responsibility Principle
  • Class constructors should require any dependencies the class needs
    • This creates explicit dependencies
    • When dependencies are not made clear in this way, the result is implicit, or hidden dependencies, which can be dangerous, or frustrating at the very least
  • Dependency injection - a technique used to allow calling code to inject the dependencies a class needes when it is instantiated
    • The Hollywood Principle: "Don't call us; we'll call you"
    • 3 Primary Ways of doing this:
      • Constructor Injection
        • Use Strategy Pattern
        • Dependencies passed in via constructor
        • Classes are self-documenting what they need and always in a valid state once constructed
        • This can result in many parameters in a constructor
      • Property Injection
        • Dependencies passed in via a property
        • aka setter injection
        • Very flexible
        • Objects are in an invalid state between construction and setting of dependencies via settings
        • Less intuitive
      • Parameter Injection
        • Dependencies passed in via a method parameter
        • Granular and flexible
        • Requires no change to rest of class
        • Breaks method signature and can result in many parameters
        • Consider this if only one method has the dependency; otherwise prefer constructor injection
  • Refactoring for Dependency Inversion Principle:
    • Extract dependencies into interfaces
    • Inject implementations of interfaces into class
    • Reduce class's responsibilities using SRP
  • Smells that indicate violation of DIP:
    • Use of the new keyword a lot
    • Use of static methods/properties
      • e.g. DateTime.Now
      • You should only use static methods that only care about data that is passed in as parameters; if it instantiates other classes, you're probably going to run into problems when you start testing
  • Where do we instantiate our objects? Using DIP results in a whole bunch of interfaces that need to be instantiated somewhere
    • Create a default constructor which provides a default implementation of the interface
    • Main - manually instantiate whatever is needed in your application's startup routine or main() method
    • IoC Container - "Inversion of Control" container; like instantiating in main but easier to control
  • IoC Containers
    • responsible for object graph instantiation
    • initiated at application startup via code or configuration
    • managed interfaces and implementation to be used are Registered with the container
    • dependencies on interfaces are Resolved at application startup or runtime
    • some common IoC containers: Microsoft Unity, StructureMap, Ninject, Windsor
  • Depend on abstractions rather than concrete types wherever possible
  • Layered/Tiered Application Design
    • Separate logical (and sometimes physical) layers which each correspond to separate projects in Visual Studio
    • Supports Encapsulation and Abstraction and works at approapriate abstraction level for each layer
    • Provides units of reuse
    • Traditional architecture: UI depends on Business Logic depends on Data Access Depends on a Database
    • You can invert the traditional architecture to have Data Access and UI and Tests depending on Business Logic and the ObjectModel (core, domain objects); the Database can be housed to the side and depended on only by Data Access
    • Dependencies tend to flow towards infrastructure like database, xml files, data access layers in traditional programing
      • Results in tight coupling, OCP violation, and difficulties with testing
    • You want to structure your project such that your core layer is at the center and has the fewest dependencies
  • Dependency is transitive
  • Don't depend on infrastructure assemblies from your core buisiness layer
    • Apply DIP to reverse dependencies
  • Related to Single Responsibility Principle, Interface Segregation Principle, Facade Pattern, Strategy Pattern, Inversion of Control Containers, Open Closed Principle
  • Recommended reading: Agile Principles, Patterns, and Practices in C# by Robert C. Martin and Micah Martin and Martin Fowler article on dependency injection

Don't Repeat Yourself Principle

  • Results in code that is easier to maintain and easiser to use
  • Every piece of knowledge must have a single, unambiguous representation in the system.
  • Repetition in logic calls for abstraction. Repetition in process calls for automation.
  • Signals that DRY should be applied:
    • Magic strings/magic numbers
    • Duplicate logic
    • Repeated if-then logic
    • Conditionals instead of polymorphism
      • Flags over objects are an indication of this; they violate the DIP
    • Repeated execution patterns
    • Copy-Pasted code
    • Only manual tests (aka the programmer manually runs the code and checks if you're getting the right result)
      • this gets more and more expensive and less and less accurate as new operations are added
      • when writing tests, you can use mock objects so you don't have to initialize a bunch of normal objects just for the purposes of testing
    • Static methods everywhere
      • results in tight coupling and difficulties in testing
  • You can consider code generation to apply DRY
    • T4 Templates built into Visual Studio
    • ORM tools reduce repetitive data access code and eliminate common data access errors
    • CodeSmith, CodeBreeze, CodeHaystack
  • Repetition in Process
    • Testing by hand = tedious and wasteful
    • Building by hand = tedious and wasteful
    • Performing deployments by hand = tedious and wasteful
  • Repetition -> errors and waste
  • Refactor code to remove repetition
  • Abstract logic in code
  • Related to Template Method Pattern, Command Pattern, Dependency Inversion Principle
  • Recommended reading: The Pragmatic Programmer: From Journeyman to Master and 97 Things Every Programmer Should Know

Summary

  • Single Responsibility Principle: Every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class
  • Open/Closed Principle: Software entities should be open for extension, but closed for modification
  • Liskov Substitution Principle: Subtypes must be substitutable for their base types
  • Interface Segregation Principle: Clients should not be forced to depend on methods they do not use
  • 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.
  • Don't Repeat Yourself Principle: Every piece of knowledge must have a single, unambiguous representation in the system. Repetition in logic calls for abstraction. Repetition in process calls for automation.

Thanks for reading! I hope you find this and other articles here at ilyanaDev helpful! Be sure to follow me on Twitter @ilyanaDev.