Skip to main content

Solid Principles for Software Design - Part 2


Interface Segregation Principle

The Interface Segregation Principle (ISP) is one of the five SOLID principles of object-oriented design, which recommends that "Clients should not be forced to depend on interfaces they do not use". This means we should avoid implementing an interface that has unnecessary methods and therefore not going to be implemented. 

Some signs of violating ISP are:

  • Having a "fat" interface, which means having a high number of methods in one interface that are not so related to each other or low cohesion.
  • Empty implementation of methods, certain methods of interface are not needed for implementation.

Considering the following example, we violate the principle because CannonPrinter is designed only with the functionality to print, leaving the scan and fax method unimplemented.


interface IMultiFunction {
    void print();
    void scan();
    void fax();
}

public class HPPrinterNScanner implements ImultiFunction {
	@Override
    public void print() {
    	//Implementation
    }
	@Override
    public void scan() {
    	//Implementation
    } 
	@Override
    public void fax() {
    	//Implementation
    }
}
public class CannonPrinter implements ImultiFunction {
	@Override
    public void print() {
    	//Implementation
    }
	@Override
    public void scan() {
    	//No Implementation
    }
	@Override
    public void fax() {
    	//No Implementation
    }
}
 

To solve this problem, we can follow the ISP and refactor the code by splitting the IMultiFuntioin interface like below:


interface IPrint {
    void print();
}
interface IScan {
    void scan();
}
interface IFax {
    void fax();
}
public class HPPrinterNScanner implements IPrint, IScan, IFax {
	@Override
    public void print() {
    	//Implementation
    }
	@Override
    public void scan() {
    	//Implementation
    } 
	@Override
    public void fax() {
    	//Implementation
    }
}
public class CannonPrinter implements IPrint{
	@Override
    public void print() {
    	//Implementation
    }
}
 

By applying the ISP, we also indirectly apply Single responsibility and Liskov substitution principle since each interface now is only responsible for its specific purpose, and the class that implements for example IPrint can play a role as IPrint's subtitution.

Dependency Inversion Principle

The idea of this principle is to ensure that high level module/class doesn't depend on low level mdule class, instead both should depend on abstrations for example an interface. And abstractions should not depend on details. Details should depend on abstraction.

Let’s consider an example below. Suppose you have a Book class and a Library class, and the Library class uses the Book class:


class Book {
    String getTitle() {
        return "Some Title";
    }
}

class Library {
    private Book book;

    Library(Book book) {
        this.book = book;
    }

    void displayBookTitle() {
        System.out.println(book.getTitle());
    }
}
 

In this scenario, it is evident that the Library class, a higher-level module, relies on the Book class, a lower-level module. This reliance contradicts the Dependency Inversion Principle, which states that high-level modules should not depend on low-level modules; instead, both should depend on abstractions. Changes in the lower-level module can significantly affect the higher-level module, resulting in a system that is less flexible and maintainable. To address this issue, we can use an abstraction, such as an interface, between the Library class and the Book class as follows:


class Book implements IBook {
    String getTitle() {
        return "Some Title";
    }
}

interface IBook {
	String getTitle();
}

class Library {
    private IBook book;

    Library(IBook book) {
        this.book = book;
    }

    String displayBookTitle() {
        System.out.println(book.getTitle());
    }
}
 

Comments

Popular posts from this blog

Maximizing Efficiency: The Power of Database Indexing

What is database performance? There are two main aspects of database performance: response time and throughput . Response time is the total time it takes to process a single query and returns result to the user. It's critical metrics because it directly impacts the user's experience, especially in applications where fast access to data is essential. The response time includes CPU time (complex queries will require more computational power and increase processing time), disk access, lock waits in multiple-user environment (more about database transaction ), network traffic. Throughput refers to how many translations the system can handle per second (TPS). A transaction could include different activities to retrieve and manipulate data. A single command like SELECT, INSERT, UPDATE, DELETE or a series of commands could be used to trigger these activities. If you’re running an e-commerce site, a single transaction might include checking the inventory, confirming the payment, and...

LINQ - Deferred Execution

Deferred Execution means that queries are not executed immediately at the time it's being created. The benefit of this is to improve performance by avoiding unnecessary executions. It also allows the query to be extendable when needed such as sorting, filtering. In the example below, the queries to retrieve courses are not being executed yet var context = new DbContext(); var courses = context.Courses      .Where(c => c.Level == 1)      .OrderBy(c => c.Name); The query is only executed when one of the following scenarios occurs: Iterating over query variable Calling ToList, ToArray, ToDictionary Calling First, Last, Single, Count, Max, Min, Average For example when we loop through the course or turn the result to a list: foreach ( var c in courses) Console.WriteLine(c.Name); OR context.Courses      .Where(c => c.Level == 1)      .OrderBy(c => c.Name).ToList();