Take advantage of the Open Closed Principle to design classes that are open for extensions but closed for modifications The Open Closed Principle states that classes should be open for extension but closed for modification. In essence, you need not change the current code base when a new functionality needs to be incorporated — you should instead be able to extend the types and implement the functionality. Conformance to the Open Closed Principle facilitates building applications that are reusable and can be maintained easily. The intent of the Open Closed Principle is ensuring that once the classes, modules, and functions in your application are defined, they shouldn’t change over time. Note that a module implies a class, a group of classes, or a component that represents a feature. So if you have any new requirement that has come in, you should be able to address that change by extending the types; you should never change the types once they are defined. The Decorator, Factory, and Observer design patterns are good examples of design patterns that enable you to design applications that help you to extend the existing code base sans the need of changes to them. The easiest way to implement the Open Closed Principle is to define your types in such a way that they adhere to the Single Responsibility Principle. In doing so, the concerns in the application’s code are separated. The Single Responsibility Principle states that a class should have one and only one reason for change, i.e., a subsystem, module, class, or a function shouldn’t have more than one reason for change. Once done, you should be able to represent these concerns using abstractions and enable the consumers of your class access these concerns through these defined abstractions. So, you should be able to extend the types defined in your application without modifying them. Note that abstraction is the key to realize the Open Closed Principle. The derivatives that are created from this abstraction are closed for modification since the abstraction is fixed. However, you can extend the behavior by creating new derivatives of the abstraction that has already been defined. A bit of code Let’s dig into some code. Refer to the following code snippet. It illustrates a simple logger class that can log data to a log target. The LogTarget enum defines two log targets, i.e., log sources. public enum LogTarget { File, Database }; public class Logger { public void Log(string message, LogTarget logTarget) { if (logTarget == LogTarget.File) { LogDataToFile(); } else //if log target is database { LogDataToDB(); } } private void LogDataToFile() { //Code to write log messages to a file } private void LogDataToDB() { //Code to write log messages to a database table } } The private methods LogDataToFile and LogDataToDB are called based on the log target specified as parameter to the Log method of the logger class. This code violates the Open Closed Principle and is an example of bad design. What happens when you need to incorporate another log target, i.e., you want your logger class to be able to log data to a new log target? Well, you would then need to modify your Log method in the Logger class, write another if statement and a private method that would correspond to the new log target. Of course, you should also update the LogTarget enum with the new log source. Anyway, this seems weird, doesn’t it? The following code listing shows how you can modify your Log class to ensure that the Open Closed Principle is not violated. public enum LogTarget { File, Database }; public abstract class Logger { public abstract void Log(string message); } public class FileLogger : Logger { public override void Log(string message) { //Code to write log messages to a file } } public class DBLogger : Logger { public override void Log(string message) { //Code to write log messages to a database table } } public class ObjectFactory { public static Logger GetLogger(LogTarget logTarget) { switch (logTarget) { case LogTarget.File: return new FileLogger(); case LogTarget.Database: return new DBLogger(); default: return null; } } } If you need to incorporate a new log target, all you need to do is define it in the enum and create a class that corresponds to the new log target — you no longer need to change the Logger base class. Note that the Logger class is now abstract and declares the Log method — the Log method needs to be defined in the respective logger classes. Note that the ObjectFactory class is used to create an instance of the logger class that corresponds to the log target passed as parameter to the GetLogger() method. The following code snippet illustrates how you can create an instance of the FileLogger class using the ObjectFactory class and invoke the Log method on the new instance. static void Main(string[] args) { Logger logObj = ObjectFactory.GetLogger(LogTarget.File); logObj.Log("Hello:"); } Related content feature 14 great preprocessors for developers who love to code Sometimes it seems like the rules of programming are designed to make coding a chore. Here are 14 ways preprocessors can help make software development fun again. By Peter Wayner Nov 18, 2024 10 mins Development Tools Software Development feature Designing the APIs that accidentally power businesses Well-designed APIs, even those often-neglected internal APIs, make developers more productive and businesses more agile. By Jean Yang Nov 18, 2024 6 mins APIs Software Development news Spin 3.0 supports polyglot development using Wasm components Fermyon’s open source framework for building server-side WebAssembly apps allows developers to compose apps from components created with different languages. By Paul Krill Nov 18, 2024 2 mins Microservices Serverless Computing Development Libraries and Frameworks news Go language evolving for future hardware, AI workloads The Go team is working to adapt Go to large multicore systems, the latest hardware instructions, and the needs of developers of large-scale AI systems. By Paul Krill Nov 15, 2024 3 mins Google Go Generative AI Programming Languages Resources Videos