Software development is a complex set of processes and procedures needs to be done in order to build software application. Software design is a by far the most important phase in any software project of any scale.

The solid foundation of project is solely depends upon sound design to help the project meet its goals in terms of timelines and other expectations from stakeholders. In this article we are going to talk about five principles of object-oriented programming the SOLID design principles.

Design pattern is a descriptive way to defined solution to commonly occurring problem in the software industry in general and software design and development in particular.

A design pattern proven battle tested and resalable solution to commonly occurring solutions, which can easily be translated into code, which mean it is not solution instead its description of solution for common problems under the discussion.

Why design patterns are important?

Design patterns help in solving common design problems that is encountered during designing phase of any software system. These are descriptions for commonly encountered problems, and independent of any technology source code, and can be applied in any programming language.

Design patterns decompose the system into objects and modules, which helps in code-reuse, encapsulation and decoupling various parts of system. Design patterns identifies various opportunities of abstraction during the design phase which polish design by making code and components reusable throughout the system, and better organization of modules within the system. The code organization and quality also play very important role in the software development design patterns helps in making the code cleaner and more organized.

Design patterns encourages contact base or interface-based system design in which each object and module is designed to implement interfaces and the interaction with other objects using these interfaces while hiding implantation specifics, which leads loose coupling which is another important aspect of system design. Another less obvious advantage of design patterns is they help in simplify system design and code which makes it easier to understand the system and its source code for any newcomer developers and resources.

Encourage loose coupling in the system is another key feature of having design patterns, which helps to improve overall design. Code reuse is another key part of the system where common pieces of code or modules are re-used to reduce overhead and achieve modular design. In a large-scale applications code reusability becomes difficulty without following design patterns, it becomes harder to find reusable opportunities.

The SOLID design principles

The SOLID principals are set of design principles in object-oriented programming to design solid application foundation. SOLID stands for Single Responsibility, Open/Close, Liskov Substitution, Inversion of Control and Dependency Injection.

The SOLID principals enable object-oriented application clearer and more maintainable by loosely coupling the components of the system. This paper will use the university registration system for examples and Typescript programming language for code samples being used in this paper.

Single responsibility Principle

The single responsibility principle states that each class should have one and only one responsibility. This principle is a base upon the philosophy of do one thing and do it well.

This principle reduces coupling in the system, if a class have more than one thing to do, then changes on one related feature will result in changing class definition which can potential impact the other feature.

This principle solves the problem of decoupling by breaking classes into multiple classes each one having one responsibility, which makes the overall design cleaner.

It also makes code more testable and improve the quality of system. Following is an example of university registration system with Student class having two responsibilities, registration and logging the error in case of any exception.

class FileWriter { 
  static write(f: string, e: any) { 
    // implementation for writing to file. 
  } 
} 

class Database { 
  save(student: Student) { 
    // implementation for saving to database. 
  } 
} 

class Student { 
  firstName: string; 
  lastName: string; 
  //other fields omitted for simplicity. 

  register(db: Database) { 
    try { 
      db.save(this); 
    } catch (e) { 
      FileWriter.write("errors.txt", e); 
    } 
  } 
} 
Code without single responsibility principle

This can lead to number of maintenance issues, but it can be fixed by breaking this code into two classes each one having single responsibility.

class Student { 
  logger: ErrorLogger = new ErrorLogger();   
  //other fields omitted for simplicity. 
  
  register(db: Database) { 
    try { 
      db.save(this); 
    } catch (e) { 
      this.logger.log(e); 
    } 
  } 
}  

class ErrorLogger { 
  log(error: string) { 
    FileWriter.write("An error occurred: ", error); 
  } 
} 
Code with single responsibility principle

Open-close Principle

This rule states that every class or modules in the system should be open for extension and close for modification. In simple words this means, the system should be designed in a way, each module or class can incorporate changes with modify definition of class and just by extension.

This principle ensures proper use of inheritance in object-oriented programming. The class behavior can be extended by using extension and override methods to incorporate changes required by system.

In following example for Student and Instructor classes needs to get department from database. The problem here is if we need to make changes we need to modify Student, and Instructor.

class Database { 
    executeQuery() {
        // implementation for saving to database. 
    } 
}  

class Student { 
    db = new Database(); 

    getDepartment() {
        return this.db.executeQuery(); 
    } 
}  

class Instructor { 
    db = new Database(); 

    getDepartment() { 
        return this.db.executeQuery(); 
    } 
} 

This can be fixed by simple modification to code to comply with open-close principle, now with Department as super class, if there is any change to Department class the changes will automatically propagated using extension without having to modify Student and Instructor.

class Department { 
    db = new Database(); 

    getDepartment(query) { 
        return this.db.executeQuery(query); 
    } 
}  

class Student extends Department { 
    showDepartment() { 
        return this.getDepartment("student query goes here");
    } 
}  

class Instructor extends Department { 

    showDepartment() { 
        return this.getDepartment("instructor query goes here"); 
    } 
} 

Liskov substitution Principle

This rule states that the instance of each class should be able to replace by their sub-class instances. This principle looks arguably more twisted as compared to other four principles. This principle can be generalized as if Y is a sub-type of type X, then each instance of X should be able to replace by Y.

The following example shows this principle, Department is sub-type of Student and instructor which can be assigned to department instance such studentDpt and instructorDpt.

class Department { 
    db = new Database(); 

    getDepartment(query) { 
        return this.db.executeQuery(query); 
    } 
}  

class Student extends Department { 
    showDepartment() { 
        return this.getDepartment("student query goes here"); 
    } 
} 

class Instructor extends Department { 
    showDepartment() { 
        return this.getDepartment("instructor query goes here"); 
    } 
} 

const studentDpt: Department = new Student(); 

const instructorDpt: Department = new Instructor(); 

Interface Segregation Principle

This principle states that clients or consumers of classes should not be forced to implement every method. In other world larger interfaces should be broken down into small interfaces.

This also encourages the creation of new interfaces instead of adding more methods to existing ones. In the fowling example IPerson interface has different methods, one method belongs to Student and other belongs to Instructor.

class IPerson { 

    getStudents() { 
        // get department 
    } 

    graduationStatus() { 
        // get graduation status 
    } 
} 
 

class Student implements IPerson { 

    getStudents() { 
        new Error("not implemented"); 
    } 

    graduationStatus() { 
        // get graduation status 
    } 
} 
 

class Instructor implements IPerson { 

    getStudents() { 
        // Get students 
    } 

    graduationStatus() { 
        throw new Error("not implemented"); 
    } 
} 

This issue can be fixed by breaking IPerson into separate interfaces such as following, breaking it into IStudent & IInstructor.

class IInstructor { 
    getStudents() { 
        // get department 
    } 
} 

class IStudent { 
    graduationStatus() { 
        // get graduation status 
    } 
} 

class Student implements IStudent {
    graduationStatus() { 
        // get graduation status 
    } 
} 

class Instructor implements IInstructor { 
    getStudents() { 
        // Get students 
    } 
} 

Dependency Inversion Principle

The dependency inversion principle states that the high-level modules in the system should not be dependent on lower lever modules and abstractions shouldn't be dependent on details.

This principle focuses on decoupling the system and forcing the interface driven interaction between modules within the system. This principle ensures proper use of polymorphism in object-oriented programming, by implementing each class with interfaces or contacts and inheritance depend upon interfaces instead of concert implementations.

Following example show dependency inversion principle, Student & Instructor both implements IPerosn , showFullname method on ShowInfo class accepts IPerson interface which can either be Student or Instructor.

class IPerson { 
    getFullName() { 
        // get full name 
    } 

    getAddress() { 
        // get address 
    } 
}  

class Student implements IPerson { 
    getFullName() { 
        // get full name 
    } 

    getAddress() { 
        // get address 
    } 
} 

class Instructor implements IPerson { 
    getFullName() { 
        // get full name 
    } 

    getAddress() { 
        // get address 
    } 
}  

class ShowInfo { 
    static showFullName(person: IPerson) { 
        console.log(person.getFullName()); 
    } 
}  

// show student address 
ShowInfo.showFullName(new Student()); 

// shows instructor address 
ShowInfo.showFullName(new Instructor()); 

Summary

Software project is the completed process consist of various steps include analyzing, building, testing and maintaining the software application. Design is by far the most crucial step in software development, if done right boosts changes for the project to be successful.

Developing software application is complicated task, design patterns combined with principals provide a solid foundation to build successfully and maintainable software project. There are number of design patterns in object-oriented software methodology, which can be used in various types software applications of varying scales, to make the design better.