Software Craftsmanship

SOLID Design Principles

SOLID Design Principles

These are agile, class level design principles. They allow you to minimize coupling and maximize cohesion. The result is code that is easy to read and maintain.

 

  • Single Responsibility Principle (SRP)

  • Open/Closed Principle (OCP)

  • Liskov Substitution Principle (LSP)

  • Interface Segregation Principle (ISP)

  • Dependency Inversion Principle (DIP)

What are principles?

 

  • guidelines

  • less prescriptive than design patterns

  • not a replacement for thinking critically about code

Coupling and Cohesion

Coupling is a measure of interdependence between two routines or modules.

Cohesion is a measure of how strongly related each piece of functionality is.

With tightly coupled systems:

  • a change in one module forces changes in the others.

  • modules are harder to reuse.

  • modules are harder to test.

With low cohesion systems:

  • modules are complex with more operations.

  • modules are less maintainable and harder to work with.

Module A

Module A

High Coupling, Low Cohesion

Module A

Module A

Low Coupling, Low Cohesion

Module A

Module A

High Coupling, High Cohesion

Module A

Module A

Low Coupling, High Cohesion

Orthogonality

Two or more modules are orthogonal if

changes in one do not affect the other.

  • When components are highly interdependent, there is no such thing as a quick, local fix.
     

  • Orthogonal software provides increased productivity and decreased risk because developers never have to worry about side-effects of making changes.
     

  • Orthogonal components are easier to swap meaning less dependence on a specific library or vendor.

Law of Demeter

  • Object A can call a method of object B but should not "reach through" B to acquire a reference to object C.
  • A method m of class A may only invoke methods on:
    1. A itself
    2. m's parameters
    3. objects instantiated within m
    4. A's direct instance objects
    5. a spherical variable, accessible by A, in the scope of m

Since objects are less dependent on the internal structure of other objects, classes can be changed without reworking their callers.

SOLID 

Each class or module should have only one responsibility, and thus, one reason to change.

 

  • Why this principle? It provides guidance around how large to make classes/modules.

  • Large classes tend to be incohesive.

  • The single responsibility should be entirely encapsulated by its class. 

  • Bottom line: make classes small enough to maximize cohesion, but large enough to minimize coupling.

Single Responsibility Principle (SRP)

A single responsibility

a single reason for change

class Book {
 
    function getTitle() {
        return "A Great Book";
    }
 
    function getAuthor() {
        return "John Doe";
    }
 
    function turnPage() {
        // pointer to next page
    }
 
    function printCurrentPage() {
        echo "current page content";
    }
}

The function printCurrentPage changes depending on how you want to output the page's content.

 

Console, File, or DataBase. All require altering the Book class.

A single responsibility

a single reason for change

class Book {
 
    function getTitle() {
        return "A Great Book";
    }
 
    function getAuthor() {
        return "John Doe";
    }
 
    function turnPage() {
        // pointer to next page
    }
 
    function getCurrentPage() {
        return "current page content";
    }
 
    function getLocation() {
        // returns the position in the library
        // ie. shelf number & room number
    }
}

Location is only meaningful in the context of a library, so the library should handle the location.

Location is only meaningful in the context of a library, so the library should handle the location.

class Book {
 
    function getTitle() {
        return "A Great Book";
    }
 
    function getAuthor() {
        return "John Doe";
    }
 
    function turnPage() {
        // pointer to next page
    }
 
    function printCurrentPage() {
        echo "current page content";
    }

    function save() {
        $filename = '/documents/'. $this->getTitle(). ' - ' . $this->getAuthor();
        file_put_contents($filename, serialize($this));
    }

    function printCurrentPage() {
        echo "current page content";
    }
}

Let's break it into multiple classes each with a single responsibility.

SOLID

No client should be forced to depend on methods it does not use.

Interface Segregation Principle (ISP)

  • Why this principle? It provides guidance around how large to make interfaces.
  • Multiple client specific interfaces are better than one general purpose interface.
  • Keep interfaces segregated by behavior.
  • Bottom line: make interfaces large enough to handle specific client use cases, but no larger.

Interface-based design:

  • The foundation of APIs
  • Design from the outside in
  • Write the interfaces first
  • Practice TDD

 

Interface Segregation Principle

SOLID

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

B. Abstractions should not depend upon details. Details should depend upon abstractions.

Dependency Inversion Principle (DIP)

  • Why this principle? It provides guidance around how to correctly use interfaces.

  • Don't force the client to know the details of the implementation.
  • Classes should encapsulate (and hide) the details.
  • Bottom line: implementations should depend on interfaces, not the other way around.

SOLID

Routines that use objects of parent classes must be able to use objects of subclasses without altering the correctness of the program.

Liskov Substitution Principle (LSP)

  • Why this principle? It provides guidance around how to correctly use inheritance.
  • Inheritance facilitates an "is-a-kind-of" relationship. Make sure that your subclass really is-a-kind-of its parent.
  • Bottom line: A subclass may or may not accept more input and provide more functionality than its parent, but it must accept and provide as much as its parent.
Rectangle rect = new Rectangle();
rect.SetHeight(10); rect.SetWidth(2);
assert(rect.CalculateArea() == 20);

Rectangle rect1 = new Square(); 
rect1.SetHeight(10); rect1.SetWidth(2);
assert(rect1.CalculateArea() == 20);

public class Rectangle
{
    private int height, width;
    public void SetHeight(int height) { this.height = height; }
    public void SetWidth(int width) { this.width = width; }

    public int CalculateArea()
    {
        return this.Height * this.Width;
    }
}

public class Square : Rectangle
{
    public override void SetHeight(int height) { 
        base.height = height;
        base.width = height; 
    }

    public override void SetWidth(int width) { 
        base.height = width; 
        base.width = width; 
    }
}
IList<Card> cards = new ArrayList<Card>();

void setUp() {
    cards.add(new DebitCard());
}

void testProcessTransaction() {
    for (Card card : cards) {
        Card.setAvailableFunds(300);
        assert(Card.canDebit(320) == false);
    }
}
public class Card {
    protected int availableFunds;

    public void setAvailableFunds(int funds) {
        availableFunds = funds;
    }

    public virtual bool canDebit(int amount) {
        return (availableFunds > amount);
    }
}
public class DebitCard : Card {
    private int overdraftProtection;

    public DebitCard(int odProtection) {
        overdraftProtection = odProtection;
    }

    public override bool canDebit(int amount) {
        return (super.availableFunds + overdraftProtection > amount);       
    }
}

SOLID

  • Why this principle? It provides guidance around how to make your software maintainable.
  • You should be able to add new features to an existing system without modifying preexisting code.
  • The concepts for "extension" are language specific, for example subclasses, extension methods, or categories.
  • Bottom line: implementation of new functionality should not require modification of existing functionality.

Open/Closed Principle (OCP)

Software entities should be open for extension,

but closed for modification.

This seems obvious but why is it important?

@implementation C1NetworkOperation

- (NSString *)endpoint
{
    NSString *base = @"http:someurl/api";
    if (self.command != None)
        base = [base stringByAppendingFormat:@"/%@", [self commandEnumToString]];
        
    return base;
}

- (NSDictionary *)parameters
{
    return @{
        @"amount" : self.amount
    };
}

- (C1CSSerializerType)responseSerializerType
{
    return C1CSSerializerTypeJSON;
}

@end

SOLID (Open/Closed)

A Real World Example

  • What is closed for modification?
    • Layout (The autolayout system figures out where the textfield should be positioned)
    • Lifecycle (There is an entire system for figuring when to destroy and recreate the view)
  • What is open for modification?
    • Pretty much everything related to how the Tefield is draw onto the screen

UI components in iOS are an excellent implementation of this principle. 

//###########################
#pragma Behavior Overrides

- (CGRect)textRectForBounds:(CGRect)bounds
{
    return [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, self.edgeInsets)];
}

- (CGRect)editingRectForBounds:(CGRect)bounds
{
    return [super editingRectForBounds:UIEdgeInsetsInsetRect(bounds, self.edgeInsets)];
}

- (CGRect)leftViewRectForBounds:(CGRect)bounds
{
    CGRect textRect = [super leftViewRectForBounds:bounds];

    textRect.origin.x = _iconPadding;

    int padding = (int)((bounds.size.height - 25) / 2);
    textRect.origin.y = bounds.origin.y + padding;

    textRect.size.width = 25;
    textRect.size.height = 25;

    return textRect;
}

- (CGRect)caretRectForPosition:(UITextPosition *)position
{
    CGRect originalRect = [super caretRectForPosition:position];

    float height = self.frame.size.height - (2 * _cursorPadding);
    float offset = _cursorPadding;

    originalRect.origin.y = self.bounds.origin.y + offset;
    originalRect.size.height = height;
    originalRect.size.width = originalRect.size.width / 1.5f;

    return originalRect;
}

- (BOOL)becomeFirstResponder
{
    [self setBackgroundFromState:eActive];
    return [super becomeFirstResponder];
}

- (BOOL)resignFirstResponder
{
    [self setBackgroundFromState:eDefault];
    return [super resignFirstResponder];
}

@end
Made with Slides.com