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:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
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 that has two methods SaveFile()
and SaveDb()
. These methods save an invoice to a file and to 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)
For simplicity, 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
class by adding one more method to the InvoiceRepository
class like this:InvoiceRepository
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 software that is more modular, flexible, and maintainable.