Software Design Principles: SOLID, DRY, WET, and More

When developing software, applying established design principles helps ensure your code is clean, maintainable, and scalable. In this article, we explore some of the most important principles, including SOLID, DRY, and WET, along with practical examples.


1. SOLID Principles

The SOLID principles are five design principles aimed at improving object-oriented design.

S: Single Responsibility Principle (SRP)

A class should have only one reason to change.

// BAD Example
public class ReportManager
{
    public void GenerateReport() { /* Generate report */ }
    public void SaveToFile() { /* Save report to file */ }
}

// GOOD Example
public class ReportGenerator
{
    public void GenerateReport() { /* Generate report */ }
}

public class FileManager
{
    public void SaveToFile() { /* Save report to file */ }
}

O: Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

// BAD Example: Modifying the Shape class every time a new shape is added.
public class Shape
{
    public string Type { get; set; }
}

public class AreaCalculator
{
    public double Calculate(Shape shape)
    {
        if (shape.Type == "Circle") { /* Circle logic */ }
        else if (shape.Type == "Rectangle") { /* Rectangle logic */ }
    }
}

// GOOD Example: Use polymorphism to extend functionality without modifying existing code.
public abstract class Shape
{
    public abstract double CalculateArea();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double CalculateArea() => Math.PI * Radius * Radius;
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    public override double CalculateArea() => Width * Height;
}

public class AreaCalculator
{
    public double Calculate(Shape shape) => shape.CalculateArea();
}

L: Liskov Substitution Principle (LSP)

Derived classes must be substitutable for their base classes.

// BAD Example: Violates LSP because the Penguin class breaks expected behavior.
public class Bird
{
    public virtual void Fly() { Console.WriteLine("Flying"); }
}

public class Penguin : Bird
{
    public override void Fly() { throw new NotImplementedException(); }
}

// GOOD Example: Use interfaces to define appropriate behavior.
public interface IFlyable
{
    void Fly();
}

public class Bird : IFlyable
{
    public void Fly() { Console.WriteLine("Flying"); }
}

public class Penguin
{
    public void Swim() { Console.WriteLine("Swimming"); }
}

I: Interface Segregation Principle (ISP)

A class should not be forced to implement interfaces it does not use.

// BAD Example: A large interface forces unused implementations.
public interface IWorker
{
    void Work();
    void Eat();
}

public class Robot : IWorker
{
    public void Work() { Console.WriteLine("Working"); }
    public void Eat() { throw new NotImplementedException(); }
}

// GOOD Example: Split the interface into smaller, focused interfaces.
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public class Robot : IWorkable
{
    public void Work() { Console.WriteLine("Working"); }
}

public class Human : IWorkable, IFeedable
{
    public void Work() { Console.WriteLine("Working"); }
    public void Eat() { Console.WriteLine("Eating"); }
}

D: Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

// BAD Example: Direct dependency on a low-level module.
public class EmailService
{
    public void SendEmail() { /* Send email logic */ }
}

public class Notification
{
    private EmailService _emailService = new EmailService();

    public void Notify() { _emailService.SendEmail(); }
}

// GOOD Example: Use dependency injection with abstractions.
public interface IMessageService
{
    void SendMessage();
}

public class EmailService : IMessageService
{
    public void SendMessage() { /* Send email logic */ }
}

public class Notification
{
    private readonly IMessageService _messageService;

    public Notification(IMessageService messageService)
    {
        _messageService = messageService;
    }

    public void Notify() { _messageService.SendMessage(); }
}

2. DRY: Don’t Repeat Yourself

The DRY principle encourages reducing duplication in code by consolidating logic into reusable methods or modules.

// BAD Example: Repeated validation logic.
public class OrderService
{
    public void PlaceOrder()
    {
        Console.WriteLine("Validating order...");
        Console.WriteLine("Processing payment...");
    }

    public void CancelOrder()
    {
        Console.WriteLine("Validating order...");
        Console.WriteLine("Processing refund...");
    }
}

// GOOD Example: Consolidate shared logic.
public class OrderService
{
    private void ValidateOrder()
    {
        Console.WriteLine("Validating order...");
    }

    public void PlaceOrder()
    {
        ValidateOrder();
        Console.WriteLine("Processing payment...");
    }

    public void CancelOrder()
    {
        ValidateOrder();
        Console.WriteLine("Processing refund...");
    }
}

3. WET: Write Everything Twice / We Enjoy Typing

The WET principle stands in contrast to DRY, emphasizing duplication and redundancy. WET often results from:

  • Copy-pasting code.
  • Lack of abstraction.

Consequences of WET:

  • Harder to maintain.
  • Increased risk of bugs due to inconsistencies.

4. YAGNI: You Aren’t Gonna Need It

The YAGNI principle advises against adding functionality until it is necessary. It helps avoid over-engineering and keeps the codebase simple.

// BAD Example: Adding unnecessary methods for future use.
public class UserService
{
    public void RegisterUser() { /* Registration logic */ }

    public void DeleteUser() { /* Unnecessary functionality for now */ }
}

// GOOD Example: Focus only on what is required now.
public class UserService
{
    public void RegisterUser() { /* Registration logic */ }
}

5. KISS: Keep It Simple, Stupid

The KISS principle emphasizes simplicity in design and implementation. Complex solutions should be avoided when simpler ones suffice.

// BAD Example: Overly complex logic for summing two numbers.
public int Add(int a, int b)
{
    return Enumerable.Range(0, a).Sum() + Enumerable.Range(0, b).Sum();
}

// GOOD Example: A straightforward approach.
public int Add(int a, int b)
{
    return a + b;
}

Conclusion

Adhering to principles like SOLID, DRY, and KISS ensures your code remains maintainable, scalable, and easy to understand. By following these guidelines, you can avoid common pitfalls, write better software, and create systems that stand the test of time. Start small by applying one principle at a time, and soon, they will become a natural part of your development process.

Avatar von admin