ThisCalculator

Everyday

SOLID Principles C Examples & When to Use Design Patterns

Updated · Standard formula, shown below

SOLID Principles C Examples & When to Use Design Patterns

SOLID principles are five design guidelines for object-oriented programming that make code more maintainable, flexible, and testable. This article explains each principle with practical C# examples (the language where they are most commonly applied) and clarifies when design patterns become necessary—patterns are solutions to recurring architectural problems, while SOLID helps you decide how to structure individual classes and methods. You will learn the concrete code changes each principle requires and the signals that indicate a pattern is the right next step.

Background: Why SOLID Exists

Before SOLID, software projects often suffered from “god classes� that did everything, tangled dependencies that made testing impossible, and changes that broke unrelated features. Robert C. Martin (Uncle Bob) formulated these five principles in the early 2000s, drawing on decades of object-oriented practice. The acronym stands for:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

They are not rules you must follow at all costs; they are heuristics that reduce the cost of change. In small scripts, applying them fully can be overkill. In long-lived applications, they are often the difference between a codebase that evolves and one that collapses under its own weight.

Single Responsibility Principle (SRP)

SRP states that a class should have only one reason to change. That reason is a single actor or stakeholder. For example, an Invoice class that calculates totals, saves to a database, and prints HTML violates SRP because three different changes (tax rules, persistence, presentation) all modify the same class.

Practical C# example:

// Violates SRP
public class Invoice {
    public decimal CalculateTotal() { /* ... */ }
    public void SaveToDb() { /* ... */ }
    public string ToHtml() { /* ... */ }
}

// Refactored
public class Invoice { public decimal CalculateTotal() { /* ... */ } }
public class InvoiceRepository { public void Save(Invoice inv) { /* ... */ } }
public class InvoiceHtmlRenderer { public string Render(Invoice inv) { /* ... */ } }

When to apply: whenever you find yourself adding a method to an existing class because it is “convenient,� stop and ask who will request changes to that method. If it is a different stakeholder, extract it.

Open/Closed Principle (OCP)

OCP says software entities should be open for extension but closed for modification. You add new behavior without altering tested, working code. The classic C# example is a Shape base class with a virtual Area() method, rather than a switch statement on type.

// Violates OCP
public double Area(object shape) {
    if (shape is Circle c) return Math.PI * c.Radius * c.Radius;
    if (shape is Rectangle r) return r.Width * r.Height;
    throw new NotSupportedException();
}

// Complies
public abstract class Shape { public abstract double Area(); }
public class Circle : Shape { public double Radius; public override double Area() => Math.PI * Radius * Radius; }
public class Rectangle : Shape { public double Width, Height; public override double Area() => Width * Height; }

Now adding a Triangle requires no changes to existing code—just a new subclass. This principle is the foundation for many design patterns, especially Strategy and Template Method.

Liskov Substitution Principle (LSP)

LSP states that derived classes must be substitutable for their base classes without altering correctness. A classic violation is the Square inheriting from Rectangle: if you set width on a square, height must also change, breaking the base class contract.

// Violates LSP
public class Rectangle { public virtual int Width { get; set; } public virtual int Height { get; set; } }
public class Square : Rectangle {
    public override int Width { set { base.Width = value; base.Height = value; } }
    public override int Height { set { base.Height = value; base.Width = value; } }
}

// Fix: don't inherit; use a common interface or composition
public interface IShape { int Area(); }
public class Rectangle : IShape { public int Width, Height; public int Area() => Width * Height; }
public class Square : IShape { public int Side; public int Area() => Side * Side; }

Signs of LSP violation: checking if (obj is Square) in a method that accepts Rectangle, or overriding a method to throw NotImplementedException. Prefer composition over inheritance when behavior differs.

Interface Segregation Principle (ISP)

ISP says clients should not depend on interfaces they do not use. Instead of one fat interface, split into smaller, role-specific ones. In C#, this often means replacing a single interface with multiple, each representing a capability.

// Violates ISP
public interface IWorker { void Work(); void Eat(); }
public class Robot : IWorker { public void Work() { } public void Eat() { throw new NotSupportedException(); } }

// Complies
public interface IWorkable { void Work(); }
public interface IFeedable { void Eat(); }
public class Robot : IWorkable { public void Work() { } }
public class Human : IWorkable, IFeedable { public void Work() { } public void Eat() { } }

When to apply: if you find yourself throwing NotImplementedException for interface members, or if a change to one method forces recompilation of many unrelated classes, split the interface.

Dependency Inversion Principle (DIP)

DIP has two parts: 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 C#, this is typically achieved with interfaces and dependency injection.

// Violates DIP
public class EmailService { public void Send(string msg) { /* SMTP */ } }
public class Notification { private EmailService _email = new EmailService(); public void Send(string msg) => _email.Send(msg); }

// Complies
public interface IMessageService { void Send(string msg); }
public class EmailService : IMessageService { public void Send(string msg) { /* SMTP */ } }
public class SmsService : IMessageService { public void Send(string msg) { /* SMS */ } }
public class Notification {
    private readonly IMessageService _svc;
    public Notification(IMessageService svc) { _svc = svc; }
    public void Send(string msg) => _svc.Send(msg);
}

This makes Notification testable (you can inject a fake service) and allows swapping email for SMS without changing the high-level class. Use constructor injection as shown, or a DI container in larger applications.

When to Use Design Patterns

Design patterns are general, reusable solutions to common problems in software design. They are not invented by you; they are cataloged from successful code. Use them when you recognize a problem that matches a known pattern’s context. SOLID principles often guide you toward a pattern: if you find yourself violating OCP, the Strategy pattern may help; if you have many conditional statements creating objects, Factory Method might be appropriate.

Common patterns and their SOLID connection:

  • Strategy – encapsulates interchangeable algorithms, supporting OCP and DIP.
  • Factory Method / Abstract Factory – creates objects without specifying concrete classes, supporting DIP.
  • Repository – mediates between domain and data mapping, supporting DIP and SRP.
  • Observer – defines a one-to-many dependency, supporting OCP.
  • Decorator – adds behavior without modifying existing classes, directly implementing OCP.

However, do not force a pattern. If a simple if-else is clearer, use it. Patterns add indirection; use them only when the complexity they manage is real and likely to grow.

Common Misconceptions and Mistakes

One common misconception is that SOLID applies only to OOP languages like C# or Java. While the principles are expressed in OOP terms, the underlying ideas (single responsibility, open/closed, etc.) apply to any modular code, including functional programming with modules and pure functions.

Another mistake is over-engineering: applying every principle to a 50-line script. SOLID is about managing change; if the code will never change, the principles add unnecessary complexity. Also, people often confuse LSP with inheritance in general—LSP is specifically about behavioral substitutability, not just “is-a� relationships.

Finally, do not treat SOLID as a checklist for code review. A class that violates SRP might still be fine if the project is small and stable. Use your judgment, and refactor when the pain becomes real.

Frequently Asked Questions

What is the difference between SOLID and design patterns?

SOLID is a set of five design principles that guide how to structure classes and dependencies. Design patterns are specific, reusable solutions to recurring problems (e.g., Singleton, Observer). Think of SOLID as the “why� and patterns as the “how.� You can use patterns that violate SOLID (e.g., a Singleton that is also a god class), but ideally patterns should help you follow SOLID.

Can I use SOLID in JavaScript or Python?

Yes, though the syntax differs. JavaScript uses prototypes or classes; Python uses classes and duck typing. The principles apply equally: keep classes focused, depend on interfaces (even if implicit), and use composition over inheritance. Many dynamic languages make some principles easier (duck typing reduces interface segregation pain) but also easier to violate accidentally.

How do I know which design pattern to use?

Start by describing the problem in plain language: “I need to create objects without specifying the exact class� → Factory. “I need to swap algorithms at runtime� → Strategy. “I need to notify multiple objects when something changes� → Observer. Consult a pattern catalog (e.g., GoF book or online resources) and match the problem context, not the code structure.

Is it bad to use a switch statement with SOLID?

Not inherently. A switch on a simple enum value is often fine. The problem arises when you have multiple switches on the same type scattered across the codebase, each needing modification when a new type is added. That violates OCP. If the switch appears in only one place and is unlikely to grow, keep it.

Do SOLID principles apply to microservices?

Yes, at a higher level. Each microservice should have a single responsibility (SRP), should be independently deployable (OCP-like), and should communicate via well-defined contracts (DIP). However, the principles are primarily about code-level design; microservices add architectural concerns like network latency and data consistency that SOLID does not address.

What is the easiest way to start applying SOLID to legacy code?

Start with SRP: identify classes that do too much and extract methods into new classes. Then use DIP to introduce interfaces at boundaries like database access or external services. Write tests before refactoring to ensure behavior stays the same. Apply one principle at a time, and do not try to refactor everything at once.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.