SOLID series: Interface Segregation Principle in Go (part 4)

SOLID series: Interface Segregation Principle in Go (part 4)

SOLID series:

Part 1: Single Responsibility Principle

Part 2: Open-closed Principle

Part 3: Liskov Substitution Principle

Part 4: Interface Segregation Principle <- You are here ~~

Part 5: Dependency Inversion Principle

1. What is Interface Segregation Principle?

Interface Segregation Principle is a fundamental design principle that encourages the creation of client-specific interfaces rather than one large interface. By breaking down interfaces into smaller, focused units, Interface Segregation Principle promotes modularity, reusability, and better code organization. In this article, we will explore the concept of Interface Segregation in Go and understand its significance in building flexible and maintainable software.

The Interface Segregation Principle is based on the idea that clients should not be forced to depend on interfaces they do not use. It emphasizes the importance of creating interfaces that are tailored to specific client requirements, rather than having a monolithic interface that includes methods irrelevant to certain clients. This principle helps in reducing coupling and making the codebase more modular and adaptable.

Benefits of Interface Segregation:

  • Modularity: By splitting large interfaces into smaller ones, Interface Segregation Principle enables a modular design where each interface represents a specific set of related behaviors. This modularity allows developers to work on individual components independently, making the codebase more maintainable and extensible.

  • Reusability: Smaller, focused interfaces increase code reusability. Clients can selectively implement and utilize only the required methods from interfaces, leading to more concise and efficient code. Additionally, this promotes the reuse of interfaces across different components, enhancing code consistency and reducing redundancy.

  • Flexibility and Scalability: With the Interface Segregation Principle, adding new functionality or modifying existing behavior becomes easier. Clients can implement additional interfaces without being burdened by unnecessary methods, and new clients can be developed without modifications to existing interfaces. This flexibility and scalability improve the overall agility of the system.

2. Interface Segregation Principle example

Let's explore a practical example that demonstrates the application of Interface Segregation Principle in Go:

Before applying Interface Segregation Principle:

type Document struct {
    // document fields
}

type Machine interface {
    Print(d Document)
    Scan(d Document)
    Fax(d Document)
}

type MultiFunctionPrinter struct {
    // printer, scanner, faxer fields
}

func (mfp MultiFunctionPrinter) Print(d Document) {
    // print implementation
}

func (mfp MultiFunctionPrinter) Scan(d Document) {
    // scan implementation
}

func (mfp MultiFunctionPrinter) Fax(d Document) {
    // fax implementation
}

func main() {
    doc := Document{}
    mfp := MultiFunctionPrinter{}
    mfp.Print(doc)
    mfp.Scan(doc)
    mfp.Fax(doc)
}

In the above example, we have a Machine interface that represents a machine capable of printing, scanning, and faxing documents. We then have a MultiFunctionPrinter struct that implements the Machine interface and provides the implementations for all three methods.

However, this violates the Interface Segregation Principle because not all clients that use the Machine interface require all three functionalities. For example, if we have a client that only needs to print documents, it is forced to implement the Scan() and Fax() methods, even though it doesn't need them.

After applying Interface Segregation Principle:

type Printer interface {
    Print(d Document)
}

type Scanner interface {
    Scan(d Document)
}

type Faxer interface {
    Fax(d Document)
}

type MultiFunctionPrinter struct {
    // printer, scanner, faxer fields
}

func (mfp MultiFunctionPrinter) Print(d Document) {
    // print implementation
}

func (mfp MultiFunctionPrinter) Scan(d Document) {
    // scan implementation
}

func (mfp MultiFunctionPrinter) Fax(d Document) {
    // fax implementation
}

func main() {
    doc := Document{}
    mfp := MultiFunctionPrinter{}
    mfp.Print(doc)
}

Certainly! Let's consider an example that demonstrates the violation of the Interface Segregation Principle and how it can be rectified by applying Interface Segregation Principle.

Before applying Interface Segregation Principle:

goCopy codetype Document struct {
    // document fields
}

type Machine interface {
    Print(d Document)
    Scan(d Document)
    Fax(d Document)
}

type MultiFunctionPrinter struct {
    // printer, scanner, faxer fields
}

func (mfp MultiFunctionPrinter) Print(d Document) {
    // print implementation
}

func (mfp MultiFunctionPrinter) Scan(d Document) {
    // scan implementation
}

func (mfp MultiFunctionPrinter) Fax(d Document) {
    // fax implementation
}

func main() {
    doc := Document{}
    mfp := MultiFunctionPrinter{}
    mfp.Print(doc)
    mfp.Scan(doc)
    mfp.Fax(doc)
}

In the above example, we have a Machine interface that represents a machine capable of printing, scanning, and faxing documents. We then have a MultiFunctionPrinter struct that implements the Machine interface and provides the implementations for all three methods.

However, this violates the Interface Segregation Principle because not all clients that use the Machine interface require all three functionalities. For example, if we have a client that only needs to print documents, it is forced to implement the Scan() and Fax() methods, even though it doesn't need them.

After applying Interface Segregation Principle:

goCopy codetype Printer interface {
    Print(d Document)
}

type Scanner interface {
    Scan(d Document)
}

type Faxer interface {
    Fax(d Document)
}

type MultiFunctionPrinter struct {
    // printer, scanner, faxer fields
}

func (mfp MultiFunctionPrinter) Print(d Document) {
    // print implementation
}

func (mfp MultiFunctionPrinter) Scan(d Document) {
    // scan implementation
}

func (mfp MultiFunctionPrinter) Fax(d Document) {
    // fax implementation
}

func main() {
    doc := Document{}
    mfp := MultiFunctionPrinter{}
    mfp.Print(doc)
}

In the updated example, we have segregated the Machine interface into three smaller interfaces: Printer, Scanner, and Faxer. Each interface represents a specific functionality. We then have the MultiFunctionPrinter struct that implements all three interfaces.

By applying Interface Segregation Principle, we provide clients with the flexibility to depend only on the interfaces that are relevant to their needs. In the main function, if a client only needs printing functionality, it can simply use the Printer interface and call the Print() method. This allows for better code organization and ensures that clients are not burdened with unnecessary methods.

3. Conclusion

Applying Interface Segregation Principle in this manner enhances the modularity, reusability, and flexibility of the codebase. Clients can implement and utilize only the required interfaces, promoting better code organization and reducing coupling. Additionally, it allows for easy extension of functionality without impacting existing clients, making the system more scalable and adaptable.