DEV Community

Cover image for Mastering Code Quality: Applying SOLID Principles in a Library Checkout System
Guilherme Nichetti
Guilherme Nichetti

Posted on

Mastering Code Quality: Applying SOLID Principles in a Library Checkout System

The SOLID principles are a group of five design rules in object-oriented programming and software engineering. These principles give you guidance to write code that's easier to maintain, change, and sturdy. In this blog post, we'll explore each of these SOLID principles, explaining what they mean and how they help you write better software.


Single Responsibility Principle (SRP)

• The Single Responsibility Principle states that a class should have only one reason to change. In other words, it should have a single responsibility. When a class has more than one responsibility, it becomes tightly coupled, making it challenging to make changes without affecting other parts of the code.

• To apply SRP, break down your code into smaller, focused classes or modules, each responsible for a single task. This not only makes your code more maintainable but also enhances its reusability.

• For instance, suppose we have a LibraryCheckoutclass responsible for both checking out books and calculating late fees:

class LibraryCheckout {
    public void checkOutBook(Book book) {
        // Process book checkout
    }

    public double calculateLateFee(Book book) {
        // Calculate late fee
        return lateFee;
    }
}
Enter fullscreen mode Exit fullscreen mode

• To adhere to SRP, you can separate the responsibilities:

class BookCheckout {
    public void checkOutBook(Book book) {
        // Process book checkout
    }
}

class LateFeeCalculator {
    public double calculateLateFee(Book book) {
        // Calculate late fee
        return lateFee;
    }
}
Enter fullscreen mode Exit fullscreen mode

Open/Closed Principle (OCP)

• The Open/Closed Principle suggests that software entities should be open for extension but closed for modification. This means you should be able to add new features or behaviors without altering existing code.

• To uphold OCP, employ inheritance, interfaces, and abstractions to enable easy extension. This allows new functionality to be added through new classes or interfaces, without necessitating changes to existing code.

• For example, if we have a LibraryCheckoutclass for checking out books, and we want to extend it to handle video rentals, we can create a new class without modifying the existing one:

class VideoRentalCheckout {
    public void checkOutVideo(Video video) {
        // Process video checkout
    }
}
Enter fullscreen mode Exit fullscreen mode

Liskov Substitution Principle (LSP)

• The Liskov Substitution Principle focuses on the relationship between a base class and its derived classes. It states that objects of derived classes should be able to replace objects of the base class without affecting the program's correctness.

• To illustrate LSP, let's consider a LibraryItem base class and its derived classes Book and DVD:

class LibraryItem {
    public void checkOut() {
        // Check out the library item
    }
}

class Book extends LibraryItem {
    private String isbn;

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    @Override
    public void checkOut() {
        // Check out the book
    }
}

class DVD extends LibraryItem {
    private String title;

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public void checkOut() {
        // Check out the DVD
    }
}
Enter fullscreen mode Exit fullscreen mode

• We should be able to substitute a DVD object wherever a LibraryItem object is expected without affecting the program's behavior:

public class Library {
    public void processCheckout(LibraryItem item) {
        item.checkOut();
    }
}

public class Main {
    public static void main(String[] args) {
        Library library = new Library();

        Book book = new Book();
        book.setIsbn("978-3-16-148410-0");
        library.processCheckout(book); // Process checkout for a book

        DVD dvd = new DVD();
        dvd.setTitle("The Matrix");
        library.processCheckout(dvd); // Process checkout for a DVD
    }
}
Enter fullscreen mode Exit fullscreen mode

Interface Segregation Principle (ISP)

• The Interface Segregation Principle advocates creating specific, client-focused interfaces rather than large, monolithic ones. It states that no client should be forced to depend on methods it does not use.

• To follow ISP, design interfaces tailored to the specific needs of clients. This reduces the need for clients to implement unnecessary methods, resulting in more cohesive and maintainable code.

• For example, instead of a broad LibraryServiceinterface:

interface LibraryService {
    void checkOutBook();
    void returnBook();
}
Enter fullscreen mode Exit fullscreen mode

• Create specific interfaces for each type of service:

interface BookCheckoutService {
    void checkOutBook();
}

interface BookReturnService {
    void returnBook();
}
Enter fullscreen mode Exit fullscreen mode

Dependency Inversion Principle (DIP)

• The Dependency Inversion Principle promotes decoupling high-level modules from low-level modules by suggesting that both should depend on abstractions rather than concrete implementations. This decoupling makes it easier to change low-level modules without impacting high-level ones.

• To implement DIP, use dependency injection, inversion of control containers, and abstractions to separate high-level and low-level components. This increases flexibility, eases testing, and reduces coupling.

• For example, instead of having a LibraryServicedirectly depend on specific book providers:

class LibraryService {
    private BookProvider provider;

    public LibraryService(BookProvider provider) {
        this.provider = provider;
    }

    public void checkOutBook() {
        // Use the provider to check out a book
    }
}

Enter fullscreen mode Exit fullscreen mode

• Depend on abstractions:

interface BookProvider {
    void checkOutBook();
}

class LibraryBookProvider implements BookProvider {
    public void checkOutBook() {
        // Checkout a book from the library
    }
}

class OnlineBookProvider implements BookProvider {
    public void checkOutBook() {
        // Checkout a book online
    }
}

class LibraryService {
    private BookProvider provider;

    public LibraryService(BookProvider provider) {
        this.provider = provider;
    }

    public void checkOutBook() {
        provider.checkOutBook();
    }
}
Enter fullscreen mode Exit fullscreen mode

• These examples illustrate how applying the SOLID principles can make a library checkout system more modular and extensible.


Conclusion

• The SOLID principles offer a robust framework for writing maintainable and extensible code. By adhering to these principles, you can develop software that is more reliable, easier to understand, and less prone to errors. While mastering these principles may require practice, the benefits in terms of code quality and maintainability are significant. Keep the SOLID principles in mind for your next software project or when refactoring existing code to create better, more adaptable software.

Top comments (0)