Open/Closed Principle (OCP) – Building for Extension, Not Modification

 

The second principle in the SOLID family is the Open/Closed Principle (OCP). It’s a simple but powerful idea:

“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
— Bertrand Meyer

This principle was first introduced in 1988 by Bertrand Meyer, who emphasized using inheritance to extend software behavior without changing existing code. Later, Robert C. Martin (Uncle Bob) adopted and expanded the principle as part of the SOLID design principles, promoting the use of interfaces, abstractions, and composition to make it more practical in modern object-oriented design. Uncle Bob summarized it as:

“You should be able to extend a software module’s behavior without modifying its source code.”
— Robert C. Martin

The core idea remains the same: build systems that are easy to extend without risking the stability of what's already working.


Common Signs You May Be Violating OCP

  • Large switch or if-else chains based on types or enum values.

  • Classes that require frequent modifications to add new behavior.

  • Classes handling multiple responsibilities (which often indicates an SRP violation, increasing OCP risk).

  • Hard-coded dependencies rather than abstractions.

  • Duplicated logic spread across multiple places instead of centralized extension points.


The Problem: Violating the Open/Closed Principle

Suppose you’re building a reporting system that generates different types of reports:

This implementation works, but every time you need to add a new report type, you must modify the GenerateReport method. That violates the Open/Closed Principle.

public class ReportGenerator
{
    public string GenerateReport(string reportType)
    {
        if (reportType == "PDF")
            return "Generating PDF report...";
        
        if (reportType == "Excel")
            return "Generating Excel report...";

        if (reportType == "Html")
            return "Generating HTML report...";

        return "Unknown report type.";
    }
}

 This implementation works, but every time you need to add a new report type, you must modify the GenerateReport method. That violates the Open/Closed Principle.

The Solution: Apply the Open/Closed Principle

Let’s refactor the design to allow new behavior without modifying existing logic.

Example usage:

var generator = new ReportGenerator();

Console.WriteLine(generator.GenerateReport(new PdfReport()));
Console.WriteLine(generator.GenerateReport(new HtmlReport()));

Step 1: Define an abstract base class

public abstract class Report
{
    public abstract string Generate();
}

Step 2: Implement each report format as a separate class

public class PdfReport : Report
{
    public override string Generate() => "Generating PDF report...";
}

public class ExcelReport : Report
{
    public override string Generate() => "Generating Excel report...";
}

public class HtmlReport : Report
{
    public override string Generate() => "Generating HTML report...";
}

Step 3: Use the base class in the generator

public class ReportGenerator
{
    public string GenerateReport(Report report)
    {
        if (report == null) return "No report provided.";
        return report.Generate();
    }
}

Now if you want to add a new report format—say WordReport—you just create a new class without touching any existing code.

Summary

Without OCP With OCP
Uses switch or if logic Uses polymorphism
Must change logic to add behavior Extend by adding new classes
High chance of breaking existing code Existing code stays untouched

When to Apply the Open/Closed Principle

  • When new behavior is expected to be added in the future

  • When you notice repeated modification of the same class

  • When changes in one place frequently cause bugs elsewhere

Avoid overengineering. If there’s only one type of behavior and no likelihood of change, a simple implementation might be more appropriate. The goal is to prepare for change when change is likely.


Final Thoughts

The Open/Closed Principle helps you build code that is easier to maintain, extend, and test. You reduce risk by protecting existing code from frequent edits while still making room for growth.

This is the second article in the SOLID series. Up next, we’ll explore the Liskov Substitution Principle, which teaches us when and how subclassing can go wrong.

Want to read the full series? Browse all SOLID principle articles: Create better software by applying SOLID principles

 

Single Responsibility Principle (SRP) - Only one reason to change

The Single Responsibility Principle is the first of the five SOLID principles of object-oriented design, introduced by Robert C. Martin (Uncle Bob). It helps make software easier to understand, maintain, and extend.

Definition: A class should have only one reason to change.

This means a class should encapsulate only one responsibility or role. When a class has multiple responsibilities, those become tightly coupled, making changes risky and costly.


What Uncle Bob Says About SRP Smells

Robert C. Martin (Uncle Bob) describes several code smells that help identify when the Single Responsibility Principle may be violated. These smells fall into two groups:


Direct Indicators of SRP Violations

  • Divergent Change
    A class changes for multiple unrelated reasons (e.g., business logic, logging, persistence). This tightly couples different concerns and clearly violates SRP.

  • Vague Class Names
    Names like Manager, Processor, or Service often signal that a class handles multiple responsibilities.

  • Chunky Methods
    Large methods containing multiple unrelated operations indicate the class may be doing too much.


Related Design Smells Often Associated with SRP Violations

  • Shotgun Surgery (Inverse)
    Responsibilities scattered across many classes require multiple changes spread out for a single change. This reflects poor responsibility distribution in the system.

  • Feature Envy
    A class frequently accesses data or methods of another class, indicating misplaced behavior but not necessarily multiple responsibilities.

  • Inappropriate Intimacy
    Excessive coupling through access to internal details of other classes; often correlates with SRP issues but can appear independently.

  • Data Clumps
    Groups of related variables passed together instead of encapsulated. This points to missing abstractions and can contribute to SRP violations.

  • Multiple Stakeholders Affected
    When different teams or roles must change the same class for different reasons; a design smell important at an organizational level.


Bad Example: A Class Doing Too Much (Smells Annotated)

using System;
using System.IO;
using System.Runtime.Caching;
using log4net;

public class ReportManager
{
    // Smell: Divergent change - Logging responsibility mixed in using a 3rd party library
    // Violation: Tight coupling to log4net means if we want to change logging
    // frameworks, we must modify this class and others using log4net directly.
    private static readonly ILog _logger = LogManager.GetLogger(typeof(ReportManager));
    
    // Smell: Divergent change - Caching responsibility mixed in
    // Violation: Direct dependency on MemoryCache couples business logic
    // to caching implementation; changing caching tech requires class modification.
    private readonly MemoryCache _cache = MemoryCache.Default;

    public string CreateReport(string reportId)
    {
        if (_cache.Contains(reportId))
        {
            _logger.Info($"Report {reportId} retrieved from cache.");
            return _cache.Get(reportId) as string;
        }

        // Core business logic — report data generation
        var reportData = new 
        {
            ReportId = reportId,
            GeneratedAt = DateTime.Now,
            Content = $"This is the content of report {reportId}."
        };

        // Smell: Divergent change - Serialization logic mixed with business logic
        // Violation: Tight coupling to System.Text.Json means changes to format or serializer
        // affect this class, violating SRP.
        string reportJson = System.Text.Json.JsonSerializer.Serialize(reportData, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });

        // Smell: Divergent change - File persistence mixed in
        // Violation: Using File.WriteAllText directly couples file I/O to business logic;
        // changing persistence strategy requires editing this method.
        string filePath = $"report_{reportId}.json";
        File.WriteAllText(filePath, reportJson);

        _logger.Info($"Report {reportId} saved to {filePath}");

        _cache.Set(reportId, reportJson, DateTimeOffset.Now.AddMinutes(10));

        return reportJson;
    }
}

 

Summary: Why the example Violates SRP

The ReportManager class in the example violates the Single Responsibility Principle by taking on multiple distinct responsibilities, each of which could change for a different reason:

  • Business Logic – Generating the contents of the report.

  • Logging – Logging using a specific framework (log4net), tightly coupling the class to a particular logging mechanism.

  • Caching – Storing and retrieving data using MemoryCache, which could change independently from business logic.

  • Serialization – Converting the report to JSON using System.Text.Json, which may need to be swapped or modified in isolation.

  • Persistence – Writing the report to disk with File.WriteAllText, introducing concerns related to file systems, security, or output formats.

Any change in any one of these areas — e.g., switching logging libraries, changing report format, updating caching rules — would require modifying this class, increasing the chance of bugs and creating ripple effects.

This coupling of responsibilities creates fragile code that is harder to test, extend, and maintain.

Refactoring by separating each responsibility into its own class restores SRP and makes each concern independently testable and changeable.

Refactored Example: Applying SRP by Separating Responsibilities


ReportService

Purpose:
Coordinates the report creation workflow by delegating to specialized classes.

How it helps SRP:
Acts as an orchestrator without taking on multiple responsibilities, preventing coupling and divergent change.

public class ReportService
{
    private readonly ReportGenerator _generator = new();
    private readonly JsonSerializer _serializer = new();
    private readonly FileSaver _saver = new();
    private readonly Logger _logger = new();
    private readonly Cache _cache = new();

    public string GetReport(string reportId)
    {
        if (_cache.TryGet(reportId, out var cached))
        {
            _logger.Info($"Report {reportId} retrieved from cache.");
            return cached;
        }

        var reportData = _generator.Generate(reportId);
        var reportJson = _serializer.Serialize(reportData);

        string path = $"report_{reportId}.json";
        _saver.Save(reportJson, path);
        _logger.Info($"Report {reportId} saved to {path}");

        _cache.Set(reportId, reportJson, TimeSpan.FromMinutes(10));
        return reportJson;
    }
}

 

Summary: How ReportService Fixes the SRP Violation

The ReportService class acts as an orchestrator, coordinating multiple well-defined components that each have a single responsibility:

  • ReportGenerator handles the creation of report content (business logic).

  • JsonSerializer takes care of converting data to JSON (serialization).

  • FileSaver encapsulates writing to disk (persistence).

  • Logger abstracts away logging logic and dependency on a specific framework.

  • Cache manages temporary storage and retrieval of report data (caching).

By delegating each responsibility to a specialized class, ReportService eliminates the divergent change smell found in the original ReportManager. Each class can now be modified or tested independently — for example:

  • Swapping out the caching mechanism won't affect how the report is serialized.

  • Updating the report structure won't impact file writing logic.

  • Changing the logging library affects only the Logger class.

This design keeps the business workflow centralized while preserving the single responsibility of each collaborator, making the system easier to extend, test, and maintain.

ReportGenerator

Purpose:
This class focuses solely on generating the core report data.

How it helps SRP:
Changes related to report creation logic will affect only this class, preventing unrelated reasons for modification.

using System;

public class ReportGenerator
{
    public ReportData Generate(string reportId)
    {
        return new ReportData
        {
            ReportId = reportId,
            GeneratedAt = DateTime.Now,
            Content = $"This is the content of report {reportId}."
        };
    }
}

public class ReportData
{
    public string ReportId { get; set; } = string.Empty;
    public DateTime GeneratedAt { get; set; }
    public string Content { get; set; } = string.Empty;
}

 

JsonSerializer

Purpose:
Handles serialization of objects into JSON format.

How it helps SRP:
Isolates serialization so that changes to serialization logic affect only this class.

using System.Text.Json;

public class JsonSerializer
{
    public string Serialize<T>(T data)
    {
        return JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
    }
}

 

FileSaver

Purpose:
Manages saving content to the file system.

How it helps SRP:
Encapsulates file persistence, so changes to storage affect only this class.

using System.IO;

public class FileSaver
{
    public void Save(string content, string filePath)
    {
        File.WriteAllText(filePath, content);
    }
}

 

Logger

Purpose:
Encapsulates logging behavior using log4net.

How it helps SRP:
Abstracts logging implementation to isolate logging-related changes.

using log4net;

public class Logger
{
    private static readonly ILog _log = LogManager.GetLogger(typeof(Logger));

    public void Info(string message)
    {
        _log.Info(message);
    }
}

 

Cache

Purpose:
Manages caching of data using MemoryCache.

How it helps SRP:
Separates caching concerns to localize cache-related changes.

using System;
using System.Runtime.Caching;

public class Cache
{
    private readonly MemoryCache _cache = MemoryCache.Default;

    public bool TryGet(string key, out string value)
    {
        value = _cache.Get(key) as string;
        return value != null;
    }

    public void Set(string key, string value, TimeSpan duration)
    {
        _cache.Set(key, value, DateTimeOffset.Now.Add(duration));
    }
}

 

 

Create better software by applying SOLID principles

SOLID is an acronym for five design principles that are considered fundamental to object-oriented programming. These principles, when followed correctly, can help to create more robust, flexible, and maintainable code. The SOLID principles are:

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

In this article, we will discuss each of these principles in detail and how they can be applied in practice.

  1. Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) states that a class should have only one reason to change. In other words, a class should have only one responsibility. This principle aims to make classes more focused and easier to understand, maintain and test.

If a class has multiple responsibilities, it becomes harder to change the class without affecting other parts of the system. This can lead to a situation where a change in one part of the codebase leads to unintended consequences in other parts of the system. By adhering to the SRP, each class has a clear and distinct purpose, making it easier to modify and maintain.

Read Article: Single Responsibility Principle (SRP) - Only one reason to change

  1. Open-Closed Principle (OCP)

The Open-Closed Principle (OCP) states that classes should be open for extension but closed for modification. In other words, a class should be designed in such a way that new functionality can be added to it without changing its existing code.

This principle encourages the use of inheritance and interfaces to create a more flexible and extensible design. By designing classes in this way, it becomes easier to add new features to the system without having to modify existing code. This can lead to a more maintainable and scalable codebase.

Read Article: Open/Closed Principle (OCP) – Building for Extension, Not Modification

  1. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) states that subclasses should be substitutable for their base classes. In other words, if a program is written to use a base class, it should be able to use any subclass of that base class without issues.

This principle is important for maintaining the behavior and correctness of the codebase. By adhering to the LSP, it becomes easier to create and use new classes that are related to existing ones. This can lead to a more modular and reusable codebase.

  1. Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) states that a class should not be forced to depend on methods it does not use. In other words, interfaces should be designed to be as specific as possible to the needs of the class that uses them.

This principle encourages the use of smaller, more specific interfaces rather than large, general-purpose interfaces. By adhering to the ISP, classes can be designed to depend on only the functionality they need, which can lead to a more maintainable and robust codebase.

  1. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. In addition, abstractions should not depend on details. Details should depend on abstractions.

This principle encourages the use of abstractions to reduce coupling between different parts of the system. By designing classes in this way, it becomes easier to change the implementation of a class without affecting other parts of the system. This can lead to a more flexible and maintainable codebase.

In conclusion, the SOLID principles are a set of design principles that can help to create more robust, flexible, and maintainable code. By adhering to these principles, developers can create more modular and reusable code, which can lead to a more efficient and effective development process. While these principles may take some time and effort to apply

Decorating the tree for Christmas

As a software developer, I've always been fascinated by design patterns and how they can help solve common problems in software design. This holiday season, I've been thinking a lot about how some of these patterns can be incorporated into our daily lives, just like the themes and traditions of Christmas.

One pattern that I've found particularly useful is the "Decorator" pattern. This pattern allows us to add new functionality to an existing object without altering its structure. In the spirit of Christmas, we can think of this pattern as a way to add extra decorations or ornaments to a Christmas tree without changing the tree itself. Just like how we can add various ornaments to a tree to make it more festive and personalized, the Decorator pattern allows us to add new features to an object without changing its core functionality.

Here is an example using C#

// The base component interface
public interface ITree
{
    void Display();
}

// The concrete component classes
public class PineTree : ITree
{
    public void Display()
    {
        Console.WriteLine("I am a pine tree.");
    }
}

public class FirTree : ITree
{
    public void Display()
    {
        Console.WriteLine("I am a fir tree.");
    }
}

// The base decorator class
public abstract class TreeDecorator : ITree
{
    protected ITree tree;

    public TreeDecorator(ITree tree)
    {
        this.tree = tree;
    }

    public virtual void Display()
    {
        tree.Display();
    }
}

// Concrete decorator classes
public class ChristmasLightsDecorator : TreeDecorator
{
    public ChristmasLightsDecorator(ITree tree) : base(tree) {}

    public override void Display()
    {
        base.Display();
        Console.WriteLine("I am decorated with Christmas lights.");
    }
}

public class OrnamentsDecorator : TreeDecorator
{
    public OrnamentsDecorator(ITree tree) : base(tree) {}

    public override void Display()
    {
        base.Display();
        Console.WriteLine("I am decorated with ornaments.");
    }
}

public class GarlandDecorator : TreeDecorator
{
    public GarlandDecorator(ITree tree) : base(tree) {}

    public override void Display()
    {
        base.Display();
        Console.WriteLine("I am decorated with garland.");
    }
}

// Client code
ITree tree = new PineTree();
tree = new ChristmasLightsDecorator(tree);
tree = new OrnamentsDecorator(tree);
tree = new GarlandDecorator(tree);
tree.Display();

I am a pine tree.
I am decorated with Christmas lights.
I am decorated with ornaments.
I am decorated with garland.

This demonstrates how the decorator pattern allows you to dynamically add new behavior to an existing object by wrapping it in decorator objects that implement the same interface. Using an interface to define the base component class allows you to decorate objects of different types, as long as they implement the same interface.

Merry Christmas

A book that inspired my journey Design Patterns Explained 

Spawning multiple .NET delegates really slow

I recently worked on a project that had a listener service running that took TCP requests and made external calls to a remote WebService. The listener service could have up to 300 concurrent requests.

In testing the service it was showing a significant delay in executing requests above 3-4 concurrent clients. The first bottleneck had to do with the number of allowed HTTP connections to a remote machine from our service. It turns out that by default only 2 concurrent HTTP connections are allowed by default.

Here is an MSDN article that explains the settings that need to be set to allow more connections to be made.

http://msdn.microsoft.com/en-us/library/fb6y0fyc.aspx

So setting something like this what required.

configuration>
  <system.net>
    <connectionManagement>
      <add address = "http://www.contoso.com" maxconnection = "10" />
      <add address = "*" maxconnection = "2" />
    </connectionManagement>
  </system.net>
</configuration>

While this showed a little improvement the problems were not over. It turns out that the ThreadPool by default only starts up with 4 idle threads. As you exceed that limit the ThreadPool checks every 500ms to see if more threads are needed and only spawns up one thread every 500ms. So you can see that it will take a long time to get spawned a large number of concurrent requests.

Solution to this is to set the minimum threads in the threadpool.

ThreadPool.SetMinThreads - http://msdn.microsoft.com/en-us/library/system.threading.threadpool.setminthreads.aspx

So we set the Min to the minimum number of threads we wanted sitting idle. Ok now performance was very good up to the number of threads we set as our minimum. But wait things are not over. We moved the code to another machine and the performance degraged. What could be wrong? It worked on my machine just fine!!

So we looked at what was different about the machines. Turned out my machine was running .NET 2.0 and the other machine was running .NET 2.0 SP1. I guess in .NET 2.0 SP1 a new bug was introduced that if threads are requested to quickly it will revert back to slowly invoking new threads.

Solution is to put a delay after each new call to a delegate for a few milliseconds using something like Thread.Sleep(50);. I had to put about 50ms delay on the machine in question. However I found that this bug was to be fixed in .NET 2.0 SP2. A bit of searching I found that .NET 2.0 SP2 had actually been released but not by itself. You have to download .NET 3.5 SP1 which also includes .NET 2.0 SP2. After installing this on the machine no delay was required any longer and performance was very good.

Hope this helps someone