C# Open-closed Principle

Summary: in this tutorial, you’ll learn about the C# open-closed principle and how to use it to write code that is open for extension and closed for modification.

Introduction to the C# Open-closed principle

The Open-closed Principle (OCP) is the second principle of the SOLID principles of object-oriented design:

The Open-closed principle states that software entities (classes, methods, functions, etc.) should be open for extension but closed for modification.

In simple terms, you should design a class or a method in such a way that you can extend its behavior without directly modifying the existing source code.

C# Open-closed principle example

The following program illustrates a violation of the open-closed principle:

public class Invoice
{
    public int InvoiceNo {  get; set; }
    public DateOnly IssuedDate {  get; set; }
    public string? Customer { get; set; }
    public decimal Amount {  get; set; }
    public string? Description { get; set;}
}

class InvoiceRepository
{
    public void SaveFile(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a file.");
    }
    public void SaveDB(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a database.");
    }
}

class Program
{
    public static void Main(string[] args)
    {
        // create a new invoice
        var invoice = new Invoice
        {
            InvoiceNo = 1,
            Customer = "John Doe",
            IssuedDate = new DateOnly(2023, 4, 1),
            Description = "Website Design",
            Amount = 1000
        };

        // save the invoice to a storage
        var invoiceRepository = new InvoiceRepository();
        invoiceRepository.SaveFile(invoice);

    }
}Code language: C# (cs)

How it works.

First, define an Invoice class that has properties InvoiceNo, IssuedDate, Customer, Amount, and Description:

public class Invoice
{
    public int InvoiceNo {  get; set; }
    public DateOnly IssuedDate {  get; set; }
    public string? Customer { get; set; }
    public decimal Amount {  get; set; }
    public string? Description { get; set;}
}Code language: C# (cs)

Second, define an InvoiceRepository class with two methods SaveFile() and SaveDb(). These methods save an invoice to a file and a database, respectively:

class InvoiceRepository
{
    public void SaveFile(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a file.");
    }
    public void SaveDB(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a database.");
    }
}Code language: C# (cs)

These methods output a message to the console instead of saving an invoice to a file and a database.

Third, create a new invoice and call the SaveFile() and SaveDB() methods to save the invoice into a file and database in the Main() method of the Program class:

class Program
{
    public static void Main(string[] args)
    {
        // create a new invoice
        var invoice = new Invoice
        {
            InvoiceNo = 1,
            Customer = "John Doe",
            IssuedDate = new DateOnly(2023, 4, 1),
            Description = "Website Design",
            Amount = 1000
        };

        // save the invoice to a storage
        var invoiceRepository = new InvoiceRepository();
        invoiceRepository.SaveFile(invoice);

    }
}Code language: C# (cs)

Suppose you need to save an invoice to a JSON file, then you need to modify the InvoiceRepository class by adding one more method to the InvoiceRepository class like this:

class InvoiceRepository
{
    public void SaveFile(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a file.");
    }
    public void SaveDB(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a database.");
    }

    public void SaveJSON(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a JSON file.");
    }
}Code language: C# (cs)

This violates the open-closed principle because adding a new function requires modifying the existing class.

Refactoring the program to follow the open-closed principle

To make the program conform to the Open-closed principle, you need to redesign the InvoiceRepository class so that when you want to save an invoice to a different storage, you don’t need to modify it.

First, define an IInvoiceRepository interface that has a Save() method that saves an invoice to storage:

interface IInvoiceRepository
{
    void Save(Invoice invoice);
}Code language: C# (cs)

Second, define three classes that implement the IInvoiceRepository interface. These classes are responsible for saving an invoice in various storages including Text Files, Databases, and JSON files:

class FileInvoiceRepository : IInvoiceRepository
{
    public void Save(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a file.");
    }
}

class DBInvoiceRepository : IInvoiceRepository
{
    public void Save(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into the database.");
    }
}

class JSONInvoiceRepository: IInvoiceRepository
{
    public void Save(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into the database.");
    }
}Code language: C# (cs)

Later, if you want to save an invoice in a different storage, you can create a new class that implements the IInvoiceRepository without modifying existing classes and interfaces.

Third, create an invoice and save it to a text file, a JSON file, and a database by calling the Save() methods:

class Program
{
    public static void Main(string[] args)
    {
        // create a new invoice
        var invoice = new Invoice
        {
            InvoiceNo = 1,
            Customer = "John Doe",
            IssuedDate = new DateOnly(2023, 4, 1),
            Description = "Website Design",
            Amount = 1000
        };

        // save the invoice a file
        IInvoiceRepository repo = new FileInvoiceRepository();
        repo.Save(invoice);

        // save the invoice to the DB
        repo = new DBInvoiceRepository();
        repo.Save(invoice);

        // save the invoice to the JSON file
        repo = new JSONInvoiceRepository();
        repo.Save(invoice);

    }
}Code language: C# (cs)

Put it all together.


public class Invoice
{
    public int InvoiceNo {  get; set; }
    public DateOnly IssuedDate {  get; set; }
    public string? Customer { get; set; }
    public decimal Amount {  get; set; }
    public string? Description { get; set;}
}


interface IInvoiceRepository
{
    void Save(Invoice invoice);
}

class FileInvoiceRepository : IInvoiceRepository
{
    public void Save(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into a file.");
    }
}

class DBInvoiceRepository : IInvoiceRepository
{
    public void Save(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into the database.");
    }
}

class JSONInvoiceRepository: IInvoiceRepository
{
    public void Save(Invoice invoice)
    {
        Console.WriteLine($"Saved the invoice #{invoice.InvoiceNo} into the database.");
    }
}


class Program
{
    public static void Main(string[] args)
    {
        // create a new invoice
        var invoice = new Invoice
        {
            InvoiceNo = 1,
            Customer = "John Doe",
            IssuedDate = new DateOnly(2023, 4, 1),
            Description = "Website Design",
            Amount = 1000
        };

        // save the invoice a file
        IInvoiceRepository repo = new FileInvoiceRepository();
        repo.Save(invoice);

        // save the invoice to the DB
        repo = new DBInvoiceRepository();
        repo.Save(invoice);

        // save the invoice to the JSON file
        repo = new JSONInvoiceRepository();
        repo.Save(invoice);

    }
}Code language: C# (cs)

Benefits of the Open-Closed Principle

The Open-closed principle brings the following benefits:

  • Code reusability: When you design a class to be open for extension, it becomes easier to reuse that code in other parts of the system. By creating reusable code, you can save time and effort in the long run. Also, it becomes easier to add new functionality to the system without impacting existing code
  • Reduced code complexity: By creating classes that are closed for modification, you can reduce the complexity of the codebase. Hence, you can make it easier to maintain and extend the system when you want to add new features or make changes.
  • Greater flexibility: The Open-closed principle promotes flexibility in the software system that allows you to easily and confidently add new functionality to the system without modifying existing code. This makes it easier to respond fast to changing requirements.

Summary

  • The Open-closed principle states that classes, methods, etc., should be open for extension but closed for modification.
  • Leverage the open-closed principle to design more modular, flexible, and maintainable software.
Was this tutorial helpful ?