DEV Community

Cover image for SOLID Design Principles and Design Patterns with Examples
Burak Boduroğlu
Burak Boduroğlu

Posted on • Updated on

SOLID Design Principles and Design Patterns with Examples

Today we will talk about SOLID principles and design patterns. These are the most important concepts in software development. They help us to write clean, maintainable, and scalable code. Let's start with the SOLID principles.


SOLID Principles

SOLID is an acronym for five principles that help software developers design maintainable and scalable code. These principles were introduced by Robert C. Martin in the early 2000s. The five principles are:

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Let's discuss each of these principles in detail.


Single Responsibility Principle (SRP)

Single Responsibility Principle states that a class should have only one reason to change. In other words, a class should have only one responsibility. If a class has more than one responsibility, it becomes harder to maintain and test. It's better to split the class into smaller classes, each with its own responsibility. However most of time we call interface or abstract class as single responsibility principle.
It's a quick example with .NET code:

public interface IEmailService
{
    void SendEmail(string to, string subject, string body);
}

public class EmailService : IEmailService
{
    public void SendEmail(string to, string subject, string body)
    {
        // Send email
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an IEmailService interface and an EmailService class that implements this interface. The EmailService class has a single responsibility, which is to send emails. If we need to change the way emails are sent, we only need to modify the EmailService class.


Open/Closed Principle (OCP)

Open/Closed Principle states that a class should be open for extension but closed for modification. In other words, we should be able to extend a class's behavior without modifying it. This can be achieved by using inheritance and interfaces. It's a quick example with .NET code:

public interface IShape
{
    double Area();
}

public class Circle : IShape
{
    public double Radius { get; set; }

    public double Area()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : IShape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double Area()
    {
        return Width * Height;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an IShape interface and two classes Circle and Rectangle that implement this interface. If we need to add a new shape, we can create a new class that implements the IShape interface without modifying the existing classes.


Liskov Substitution Principle (LSP)

Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, a subclass should be able to replace its superclass without any
issues. It's a quick example with .NET code:

public class Rectangle
{
    public virtual double Width { get; set; }
    public virtual double Height { get; set; }

    public double Area()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    public override double Width
    {
        get => base.Width;
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }

    public override double Height
    {
        get => base.Height;
        set
        {
            base.Width = value;
            base.Height = value;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have a Rectangle class and a Square class that inherits from the Rectangle class. The Square class overrides the Width and Height properties to ensure that they are always equal. This allows us to use a Square object wherever a Rectangle object is expected.


Interface Segregation Principle (ISP)

Interface Segregation Principle states that a client should not be forced to implement an interface that it doesn't use. In other words, we should split large interfaces into smaller, more specific interfaces so that clients only need to implement the methods they are interested in. It's a quick example with .NET code:

public interface IShape
{
    double Area();
    double Perimeter();
}

public class Circle : IShape
{
    public double Radius { get; set; }

    public double Area()
    {
        return Math.PI * Radius * Radius;
    }

    public double Perimeter()
    {
        return 2 * Math.PI * Radius;
    }
}

public class Rectangle : IShape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double Area()
    {
        return Width * Height;
    }

    public double Perimeter()
    {
        return 2 * (Width + Height);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an IShape interface that defines two methods Area and Perimeter. The Circle and Rectangle classes implement this interface and provide their own implementations for these methods.


Dependency Inversion Principle (DIP)

Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. In other words, classes should depend on interfaces or abstract classes rather than concrete classes. This allows us to decouple classes and make them easier to test and maintain. It's a quick example with .NET code:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class EmailLogger : ILogger
{
    public void Log(string message)
    {
        // Send email
    }
}

public class Logger
{
    private readonly ILogger _logger;

    public Logger(ILogger logger)
    {
        _logger = logger;
    }

    public void Log(string message)
    {
        _logger.Log(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an ILogger interface and two classes ConsoleLogger and EmailLogger that implement this interface. The Logger class depends on the ILogger interface rather than concrete classes. This allows us to easily switch between different loggers without modifying the Logger class.


Design Patterns

Design patterns are reusable solutions to common problems in software design. They help us write clean, maintainable, and scalable code. There are three categories of design patterns:

  1. Creational Patterns
  2. Structural Patterns
  3. Behavioral Patterns

Let's discuss each of these categories in detail.


Creational Patterns

Creational patterns are concerned with object creation mechanisms. They help us create objects in a way that is flexible and decoupled from the client code. Some common creational patterns are:

  1. Factory Method
  2. Abstract Factory
  3. Builder
  4. Prototype
  5. Singleton

Structural Patterns

Structural patterns are concerned with object composition. They help us define how objects are composed to form larger structures. Some common structural patterns are:

  1. Adapter
  2. Bridge
  3. Composite
  4. Decorator
  5. Facade
  6. Flyweight
  7. Proxy
  8. Module

Behavioral Patterns

Behavioral patterns are concerned with object interaction. They help us define how objects communicate with each other. Some common behavioral patterns are:

  1. Chain of Responsibility
  2. Command
  3. Interpreter
  4. Iterator
  5. Mediator
  6. Memento
  7. Observer
  8. State
  9. Strategy
  10. Template Method
  11. Visitor

Let's discuss each of these patterns in detail with examples.


Repository Pattern

The Repository pattern is a design pattern that abstracts the data access logic from the rest of the application. It provides a way to access data without directly querying the database. This makes the application more maintainable and testable. It's a quick example with .NET code:

public interface IRepository<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
}

public class Repository<T> : IRepository<T>
{
    private readonly DbContext _context;

    public Repository(DbContext context)
    {
        _context = context;
    }

    public T GetById(int id)
    {
        return _context.Set<T>().Find(id);
    }

    public IEnumerable<T> GetAll()
    {
        return _context.Set<T>().ToList();
    }

    public void Add(T entity)
    {
        _context.Set<T>().Add(entity);
        _context.SaveChanges();
    }

    public void Update(T entity)
    {
        _context.Entry(entity).State = EntityState.Modified;
        _context.SaveChanges();
    }

    public void Delete(T entity)
    {
        _context.Set<T>().Remove(entity);
        _context.SaveChanges();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an IRepository interface and a Repository class that implements this interface. The Repository class provides methods to interact with the database without directly querying it. This allows us to easily switch between different data access technologies without modifying the client code.


Factory Method Pattern

The Factory Method pattern is a design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It's a quick example with .NET code:

public interface IShapeFactory
{
    IShape CreateShape();
}

public class CircleFactory : IShapeFactory
{
    public IShape CreateShape()
    {
        return new Circle();
    }
}

public class RectangleFactory : IShapeFactory
{
    public IShape CreateShape()
    {
        return new Rectangle();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an IShapeFactory interface and two classes CircleFactory and RectangleFactory that implement this interface. The CircleFactory class creates Circle objects, and the RectangleFactory class creates Rectangle objects. This allows us to easily switch between different types of shapes without modifying the client code.


Singleton Pattern

The Singleton pattern is a design pattern that ensures a class has only one instance and provides a global point of access to it. It's a quick example with .NET code:

public class Logger
{
    private static Logger _instance;
    private static readonly object _lock = new object();

    private Logger()
    {
    }

    public static Logger Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new Logger();
                }
                return _instance;
            }
        }
    }

    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have a Logger class with a private constructor and a static Instance property that returns the singleton instance of the class. The Instance property uses a double-checked locking mechanism to ensure that only one instance of the class is created.


Adapter Pattern

The Adapter pattern is a design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces. It's a quick example with .NET code:

public interface ITarget
{
    void Request();
}

public class Adaptee
{
    public void SpecificRequest()
    {
        Console.WriteLine("Specific request");
    }
}

public class Adapter : ITarget
{
    private readonly Adaptee _adaptee;

    public Adapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }

    public void Request()
    {
        _adaptee.SpecificRequest();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have an ITarget interface and an Adapter class that implements this interface. The Adapter class uses the Adaptee class to adapt the SpecificRequest method to the Request method. This allows objects that expect an ITarget interface to work with the Adaptee class.


Builder Pattern

The Builder pattern is a design pattern that separates the construction of a complex object from its representation. It allows us to create an object step by step and produce different types and representations of an object using the same construction process. It's a quick example with .NET code:

public class Product
{
    public string Part1 { get; set; }
    public string Part2 { get; set; }
}

public interface IBuilder
{
    void BuildPart1();
    void BuildPart2();
    Product GetProduct();
}

public class ConcreteBuilder : IBuilder
{
    private readonly Product _product = new Product();

    public void BuildPart1()
    {
        _product.Part1 = "Part 1";
    }

    public void BuildPart2()
    {
        _product.Part2 = "Part 2";
    }

    public Product GetProduct()
    {
        return _product;
    }
}

public class Director
{
    private readonly IBuilder _builder;

    public Director(IBuilder builder)
    {
        _builder = builder;
    }

    public void Construct()
    {
        _builder.BuildPart1();
        _builder.BuildPart2();
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we have a Product class that represents a complex object with two parts. We have an IBuilder interface and a ConcreteBuilder class that implements this interface. The Director class uses the ConcreteBuilder class to construct a Product object step by step.


Conclusion

In this article, we discussed the SOLID principles and design patterns. These are the most important concepts in software development. They help us write clean, maintainable, and scalable code. By following these principles and patterns, we can build high-quality software that is easy to maintain and extend. I hope you found this article helpful. Thank you for reading!


References

  1. SOLID Principles
  2. Design Patterns
  3. Repository Pattern
  4. Factory Method Pattern
  5. Singleton Pattern
  6. Adapter Pattern
  7. Builder Pattern
  8. Design Patterns in C#

Top comments (0)