Solid Principles

S.O.L.I.D: The First 5 Principles of Object Oriented Design ...

Solid Principles are a set of principles created by Robert C. Martin to guide software engineers in creating maintainable Object Oriented Applications.

SOLID Principles:

  • S : Single Responsibility
  • O : Open/Closed
  • L : Liskov Substitution
  • I : Interface Segregation
  • D : Dependency Inversion

Single Responsibility

  • Each class should do just one job and should have only one reason to change.

Open/Closed

  • A class should be open for extension but closed to modification.
  • Do not modify your classes to add class types.

Liskov Substitution

  • Subclasses can substitute parent classes without breaking functionality. Examples:
    • New exceptions shouldn’t be thrown in the derived classes.
    • Pre conditions cannot be more strict in the derived classes (e.g subclass accepting positive integers only)
    • Post conditions cannot be weakened (e.g opening without closing in subclasses while being closed in parent class)

Interface Segregation

  • Interface should represent one cohesive distinct behaviour
  • All methods in an interface should be implemented.

Dependency Inversion

  • A high level module should not depend on a low level module, both should depend on abstractions.
  • Abstractions should never depend upon details, detail should depend on abstractions.

CASE STUDY

Using an online Library as a case study, the class below violates the SOLID principles.

The initial class:

Class Book {

    Function getTitle() ;

    Function getAuthor() ;

    Function turnPage() ;

    Function printCurrentPage (String printerType) :
        Switch (printerType) :
            Case “plainTextPrinter” :
                return “plain text print out” ;
            Case “htmlPrinter” :
                return “html print out" ;

    Function savePage (String storeType) :
        Switch (storeType) :
            Case “flatFile” :
                return “save in flat file”  ;
            Case “mysql” :
                return “save in mysql DB” ;
}

We can apply the SOLID principles as follows:

Single Responsibility says a class should do just one thing. We can look at the class above and see three distinct entities.

  • Book
  • Store
  • Printer

Class Book {

    Function getTitle() ;

    Function getAuthor() ;

    Function turnPage() ;

    Function getCurrentPage() ;

    Function getNextPage() ;
}

Class Store {

    Function saveBook(String storeType) ;
        Switch (storeType) :
            Case “flatFile” :
                return “save in flat file”  ;
            Case “mysql” :
                return “save in mysql DB” ;
}

Class Printer {

    Function printBookPage (String printerType) :
        Switch (printerType) :
            Case “plainTextPrinter” :
                return “plain text printout” ;
            Case “htmlPrinter” :
                return “html printout" ;
}

Open/Closed says a class should be closed to modification but open for extension.

Class Printer {
    Function printBookPage (PrinterInterface printer) :
        printer.print() ;
}

Interface PrinterInterface {
    Function print() ;
}

Class HtmlPrinter implements PrinterInterface {
    Function print() :
        return  “html print out” ;
}

Class PlainTextPrinter implements PrinterInterface{
    Function print() :
        return “plain text print out” ;
}

This way a new type of printer can be added to the system without directly modifying the Printer class.

Liskov Substitution says the behaviour of subclasses should be same as parent classes, the example below is a violation of this principle because the BrowserPrinter throws an exception while the other subclasses have different behaviours (don’t throw exceptions)

Class Printer {
    Function printBookPage (PrinterInterface printer) :
        printer.print() ;
}
Interface PrinterInterface {
    Function print() ;
}

Class BasePrinter implements PrinterInterface {
    Function print() :
        return  “print out” ;
}

Class HtmlPrinter extends BasePrinter {
    Function print() :
        return  “html print out” ;
}

Class BrowserPrinter extends BasePrinter {
    Function print() :
        If (internet explorer) {
           Throw IHateInternetExplorerException();
        }
        return  “print browser page” ;
}

Interface Segregation says that an interface should represent one discrete cohesive behaviour, the interface should be created in a such a way that all methods must be implemented by the concrete class using the interface.

The PrinterInterface below isn’t cohesive enough in this case because not all printers use ink, therefore, it doesn’t make sense to force a POS (thermal) printer to implement this interface. A way to fix this will be to create an InkableInterface and extract methods that printers using ink only will need.

Interface PrinterInterface {

    print() ;

    removeInk();
}

Class POSPrinter implements PrinterInterface {
    Function print() :
        return  “POS print out” ;

    Function removeInk()
        Throw new doesNotHaveInkException;
}

Interface InkableInterface() {
    Function removeInk();
}

Dependency Inversion says that a high level module should not depend on a low level module, both should depend on abstractions (interfaces)

We have to change the implementation details of the Library class each time we want to use a different printer in the first implementation. The way to solve this problem will be to depend on an abstraction (interface) in Library class and this allows us to change the printer type without modifying the internal implementation in the Library class as shown in the second implementation.

A WRONG way to do it:

Class Library {
    HtmlPrinter printer = new HtmlPrinter;
    Books[] bookPages;

    Function printBook (Books[] bookPages) :
        Foreach (bookPage) {
            This.printer.print(currentPage) ;
        }
    }
}

The RIGHT way to do it.

Class Library {
    PrinterInterface printer;
    Books[] bookPages;

    ClassConstructor (PrinterInterface printer) {
        This.printer = printer;
    }

    Function printBook (Books[] bookPages) :
        Foreach (bookPage) {
           This.printer.print(currentPage) ;
        }
    }
}