DEV Community

Cover image for SOLID Principles Explained
Mario Sobhy
Mario Sobhy

Posted on

SOLID Principles Explained

The SOLID principles are a set of guidelines for writing clean and maintainable object-oriented code. The acronym SOLID stands for:

  • Single Responsibility Principle
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

I’ll explain each of these principles with examples in Ruby:

Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should have only one responsibility.

Let’s say we have a User class that handles both authentication and profile management:

class User
  def authenticate(username, password)
    # some functionality
  end

  def update_profile(attributes)
    # some functionality
  end
end
Enter fullscreen mode Exit fullscreen mode

This violates the SRP because the User class has two responsibilities: authentication and profile management. To follow the SRP, we should split this class into two:

class Authenticator
  def authenticate(username, password)
    # some functionality
  end
end

class UserProfileManager
  def update_profile(attributes)
    # some functionality
  end
end
Enter fullscreen mode Exit fullscreen mode

so now each class has only one responsibility.

Open/Closed Principle

The Open/Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to add new features to the software without changing the existing code.

Here’s an example of how to apply the OCP in Ruby:

# Bad example - not following OCP
class Car
  def initialize(make, model, year)
    @make = make
    @model = model
    @year = year
  end

  def start_engine
    puts "Starting engine for #{@make} #{@model}"
  end

  def shift_gears
    puts "Shifting gears for #{@make} #{@model}"
  end
end

class ElectricCar < Car
  def initialize(make, model, year)
    super(make, model, year)
  end

  def start_engine
    puts "Electric car does not have an engine."
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, we have a Car class that has a start_engine and shift_gears method. We also have an ElectricCar class that inherits from Car. The problem is that the start_engine method is not applicable to an electric car, as they don't have an engine. So, we need to modify the Car class to handle this new type of car.

Let’s refactor this code to follow the OCP:

# Good example - following OCP
class Car
  def initialize(make, model, year)
    @make = make
    @model = model
    @year = year
  end

  def shift_gears
    puts "Shifting gears for #{@make} #{@model}"
  end
end

class ElectricCar < Car
  def initialize(make, model, year)
    super(make, model, year)
  end

  def start_engine
    puts "Electric car does not have an engine."
  end
end

class GasCar < Car
  def initialize(make, model, year)
    super(make, model, year)
  end

  def start_engine
    puts "Starting engine for #{@make} #{@model}"
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, we removed the start_engine method from the Car class and created two new classes: ElectricCar and GasCar. Each class now handles its own implementation of the start_engine method, depending on the type of car. This way, we can add new types of cars without modifying the existing code.

In conclusion, the Open/Closed Principle encourages us to design software that is open to extension but closed to modification. By following this principle, we can create more flexible and maintainable software.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) is a principle in object-oriented programming that states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, a subclass should be able to substitute for its superclass without any unexpected behavior.

Let’s take an example of a class hierarchy in Ruby:

class Shape
  def area
  end
end

class Rectangle < Shape
  def area
    # calculates area of rectangle
  end
end

class Square < Shape
  def area
    # calculates area of square
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, we have a superclass called Shape and two subclasses called Rectangle and Square. According to the LSP, we should be able to substitute an instance of Rectangle or Square for instance of Shape without any issues.

Let’s say we have a function that takes an instance of Shape and calculates its area:

def print_area(shape)
  puts "The area is: #{shape.area}"
end
Enter fullscreen mode Exit fullscreen mode

We should be able to pass an instance of Rectangle or Square to this function without any issues:

rectangle = Rectangle.new
print_area(rectangle)

square = Square.new
print_area(square)
Enter fullscreen mode Exit fullscreen mode

This code will run without any errors because Rectangle and Square both implement the area method, which is defined in the Shape superclass.

However, if we were to violate the LSP by creating a subclass that does not behave properly when substituted for its superclass, we could run into issues. For example:

class Triangle < Shape
  def area
    raise "Triangles don't have a defined area!"
  end
end

triangle = Triangle.new
print_area(triangle)
Enter fullscreen mode Exit fullscreen mode

This code will raise an error because Triangle does not properly implement the area method. This violates the LSP because we cannot substitute an instance of Triangle for instance of Shape without unexpected behavior.

Therefore, it is important to follow the LSP when creating class hierarchies to ensure that the behavior of a subclass can be predicted and that it can be substituted for its superclass without causing issues.

Interface Segregation Principle(ISP)

The Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. In other words, it’s better to have multiple smaller interfaces that are focused on a specific task rather than one large interface that tries to do everything.

Let’s consider an example in Ruby where we have a class Printer that can print documents. It has two methods, print_pdf and print_word.

class Printer
  def print_pdf(pdf_file)
    # Code to print PDF file
  end

  def print_word(word_file)
    # Code to print Word file
  end
end
Enter fullscreen mode Exit fullscreen mode

This implementation violates the ISP because it forces clients to depend on methods they may not use. For instance, if a client only needs to print Word files, it still has to depend on the print_pdf method.

To apply ISP, we can refactor our code to create separate interfaces for each method. For example, we can create two interfaces PdfPrinter and WordPrinter.

module PdfPrinter
  def print_pdf(pdf_file)
    # Code to print PDF file
  end
end

module WordPrinter
  def print_word(word_file)
    # Code to print Word file
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, we can include only the required interface in our client classes instead of including the entire Printer class. For example, if we have a client class that only needs to print Word files, we can include the WordPrinter module:

class WordDocument
  include WordPrinter
end
Enter fullscreen mode Exit fullscreen mode

Similarly, if we have another client class that only needs to print PDF files, we can include the PdfPrinter module:

class PdfDocument
  include PdfPrinter
end
Enter fullscreen mode Exit fullscreen mode

This way, we have segregated our interfaces based on client needs.

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle(DIP) states:

“High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.”

In other words, code should be designed so that high-level modules (like classes or modules) do not depend directly on lower-level modules or details of implementation, but rather on abstractions or interfaces that can be implemented by any number of lower-level modules. This allows for greater flexibility, extensibility, and maintainability of code.

Here’s an example of how you might implement the Dependency Inversion Principle in Ruby:

# High-level module (depends on abstraction)
class PaymentProcessor
  def initialize(payment_gateway)
    @payment_gateway = payment_gateway
  end

  def process_payment(amount)
    @payment_gateway.charge(amount)
  end
end

# Abstraction (does not depend on details)
class PaymentGateway
  def charge(amount)
    raise NotImplementedError
  end
end

# Low-level modules (implement abstraction)
class StripeGateway < PaymentGateway
  def charge(amount)
    # Code to charge using Stripe API
  end
end

class PaypalGateway < PaymentGateway
  def charge(amount)
    # Code to charge using PayPal API
  end
end
Enter fullscreen mode Exit fullscreen mode

In this example, the PaymentProcessor class is a high-level module that needs to process payments. However, it doesn't depend on any specific payment gateway implementation - instead, it depends on an abstraction, the PaymentGateway class. This allows any number of lower-level modules (like StripeGateway or PaypalGateway) to implement the PaymentGateway interface, and be used by the PaymentProcessor without any modifications.

By following the Dependency Inversion Principle, this code is more flexible and easier to maintain. If a new payment gateway implementation needs to be added, for example, it can be implemented using the PaymentGateway interface and used by the PaymentProcessor without any changes to the PaymentProcessor itself.

In the end, I hope you found this article helpful in understanding the SOLID principles and how they can be applied in any programming language. Do you have any additional tips or examples for applying these principles in your own projects? I would love to hear your thoughts and feedback in the comments below.

Top comments (0)