OCP - The Open/Closed Principle

5 min read
By Thiago Souza
SOLIDOCPSoftware DesignC#

The OCP (Open/Closed Principle) is one of the 5 SOLID Principles that every good programmer and/or software architect should know. The application of the Single Responsibility Principle is extremely important so that we can separate classes that can change for different reasons and will help us a lot in implementing the Open/Closed Principle.

Definition of OCP

This principle can be defined through the following phrase:

"A software artifact must be open for extension and closed for modification."

In other words, the main objective of this principle is to make our system able to receive modifications through extensions without new change requests eventually forcing us to make changes throughout the software.

Scenarios like this are common in evolving codebases.

Violation of OCP

Here we have a very simple example created only for educational purposes and, thinking about an API, let's start by creating our endpoints for downloading a report in PDF and another in CSV:

// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Endpoint to return PDF
app.MapGet("/download/pdf", async () =>
{
    var reportGenerator = new ReportGenerator();

    var bytes = reportGenerator.Generate(new ReportContent(), ReportType.PDF);

    return Results.File(bytes, "application/pdf", "xpto.pdf");
});

// Endpoint to return CSV
app.MapGet("/download/csv", () =>
{
    var reportGenerator = new ReportGenerator();

    var bytes = reportGenerator.Generate(new ReportContent(), ReportType.CSV);

    return Results.File(csvBytes, "text/csv", "xpto.csv");
});

app.Run();

So that the code of our classes doesn't become too extensive, use your imagination to visualize the content of the ReportContent and ReportGenerator classes:

public class ReportContent
{
    // Attributes related to the report content
}

public enum ReportType 
{
    PDF = 1,
    CSV = 2
}

public class ReportGenerator
{
    public byte[] Generate(ReportContent reportContent, ReportType reportType)
    {
        if (reportType == ReportType.PDF)
        {
            // Logic to generate the PDF report
        }
        else
        {
           // Logic to generate the CSV report
        }
    }
}

As we can see in this code example, the ReportGenerator class receives two parameters. One of them (ReportContent) is the content that will be used to compose the report and the other (ReportType) indicates which type of report is desired by the user/actor.

The scenario is set up so that, whenever a new type of report arises, it will be necessary to include new conditions in this class. It may also happen (and it happens a lot) that all the dependencies necessary for each type of report are being imported in the same file, making the body of this class even more complex and with low cohesion.

But not everything is lost, because this article is here to help you solve and avoid this type of problem.

Applying the OCP

Considering the code from the previous example, let's start by removing the ReportType enum because it will no longer be necessary. In addition, we'll create an interface called IReportGenerator with the signature of the Generate method that receives only the class that represents the content of our report as a parameter.

public class ReportContent
{
    // Attributes related to the report content
}

public interface IReportGenerator
{
    byte[] Generate(ReportContent reportContent);
}

Done this, let's create a class for each type of report and both must implement our new interface and apply the necessary logic to generate the report.

public class PdfReportGenerator : IReportGenerator
{
    public byte[] Generate(ReportContent reportContent)
    {
        // Logic to generate the PDF report
    }
}

public class CsvReportGenerator : IReportGenerator
{
    public byte[] Generate(ReportContent reportContent)
    {
        // Logic to generate the CSV report
    }
}

Now let's create a class called ReportService and it needs to receive our abstraction (interface) as a parameter in its constructor:

public class ReportService(IReportGenerator reportGenerator)
{
    public byte[] GenerateReport(ReportContent reportContent)
    {
        return reportGenerator.Generate(reportContent);
    }
}

Let's finish by updating the code of our endpoints:

// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Endpoint to return PDF
app.MapGet("/download/pdf", async () =>
{
    var reportService = new ReportService(new PdfReportGenerator());
    
    var bytes = reportService.GenerateReport(new ReportContent());

    return Results.File(bytes, "application/pdf", "xpto.pdf");
});

// Endpoint to return CSV
app.MapGet("/download/csv", () =>
{
    var reportService = new ReportService(new CsvReportGenerator());

    var bytes = reportService.GenerateReport(new ReportContent());

    return Results.File(csvBytes, "text/csv", "xpto.csv");
});

app.Run();

With this, any new type of report will not force you and your developer colleagues to make changes from end to end in the software, because implementing a new extension is more than sufficient.

Still not convinced? Let's now implement an XLSX report:

// New class related to the XLSX file type
public class XlsxReportGenerator : IReportGenerator
{
    public byte[] Generate(ReportContent reportContent)
    {
        // Logic to generate the XLSX report
    }
}

//--

// New endpoint to return XLSX
app.MapGet("/download/xslx", () =>
{
    var reportService = new ReportService(new XlsxReportGenerator());

    var bytes = reportService.GenerateReport(new ReportContent());

    return Results.File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xpto.xlsx");
});

Implementation completed.

This is the essence of OCP: classes that generate reports remain closed for modifications and the software remains open to new extensions.

Reflect on how to apply this principle in your context.


Thanks for reading. To deepen: Clean Architecture — A Craftsman's Guide to Software Structure and Design (Robert C. Martin, 2019).