The Open / Closed Principle
Brain Surgery is Not Required when Putting on a Hat
The Open / Closed Principle in the Modern Developer’s Design Series is a natural extension of the Single Responsibility Principle.
The Single Responsibility Principle uses Open / Closed Principle for compliance.
The Open / Closed Principle States:
A Software Entity Should be Open for Extension but Closed for Modification
… Helpers Methods abstract Method Dependencies to support SRP and OCP
The responsibility of a behavior action, a Method, will rarely change in a Class.
If different behavior is required a new Method is created.
However, the way that a method consumes its dependencies could easily change during the life cycle of the application.
The Class Method’s Dependencies refactored as a Private Class Helper Methods or Public Utility Helper Methods complies with the OOP Practice of Encapsulation, Data Hiding.
Methods that violate Single Responsibility Principle create issues that the Open / Closed Principle corrects
… Blurs the Real Responsibility of the Method from the Dependencies Required to support the Responsibility
…… Forces the Just In Time (JIT) Compiler to assign Managed Heap Memory Blocks to Code that may never be called during Run-time
The Open / Closed Principle Deep Dive
To understand OCP you must first internalize the Single Responsibility Principle (SRP) and the concept of the Responsibility and Dependency Principle (RDP).
A Class or Method is, by nature, created to perform some application or assembly work. This Class of Method generally has a requirement that the calling Method expects to be supported:
The Requirement is its Responsibility
The Calling Software Entity is generally asking for a very specific response
The body of the called Methods ONLY Responsibility is to consume the Request Parameters
… And Deliver the Expected Response Object with all the requires child objects and properties
The Dependencies should be abstracted from the Responsibility using Private Class Helper Methods if the dependency is a One and Only One relationship to the Class.
If other Software Entities can reuse the helper method, it should be refactored into a Common Utility Class as a public Static Helper Method
This concept is defined in The Modern Developer as the Responsibility and Dependency Principle (RDP):
Do Your Job and Only Your Job
… Let Every Else do Their Job
Using RDP for the understanding and identification of the Responsibilities and the Dependencies in Software Entities supports SRP, DRY and OCP natively.
When complying with Single Responsibility Principle your objective is to create Classes and Methods that keeps its focus on the outcome of the behavior the “What”, not the details of “How” that behavior is accomplished.
The “What” will not change throughout the life cycle of the Software Entity but the “How” very well may change.
Using private Class level and public Common Assembly level Helper Methods that define the “How” will abstract away the details of the outcome from the responsibility of the software entity.
These are the areas of change. Helpers that are used for code reuse are great for encapsulating functionality that is Open for extension but allows the consuming type to be closed for modification.
There is a major performance advantage to this Run-time philosophy as well.
The Just In-Time Compiler (JIT) looks for short and simple methods that are frequently used, helper methods, and can “In-lined” for better performance.
In-lining substitutes the body of a function for the Signature of the function call only when Required
The JIT uses Registers and Pointers to catalog the signatures of small methods such as those used to abstract away the details of a Class’s responsibility.
Smaller and simpler functions make it easier for the JIT compiler to support “Enregistration”
This is the process of selecting which local variables can be stored in registers rather than on the stack.
This register is a single block of Managed Heap memory that can access through pointers possibly hundreds or thousands of discrete method variables.
It will only compile for the Common Language Run-time (CLR) when actually called.
This means if a Case statement or Nested If conditionals are never used
… The CLR will never see the code. This saves memory and CPU cycles.
If you cannot understand the method’s contract with the calling class method quickly then the method has blurred its “What” with the “How”.
You should be able to Define what the Method is Doing
… Simply by its Method Parameters and its Response Object
The process it uses to complete the request defines its dependencies as the implementation of the request; this is NOT its responsibility
Process Details should be Abstracted Code that can Easily Change Over Time
- Lengthy Methods – Code that take more than thirty seconds to understand its function is generally a OCP code smell
- Nested If Statements – Conditionals that do not directly support the method’s responsibility is always a OCP code smell.
- Logic such as Switch Cases Code Blocks– Case statements are Process Logic. These activities support the “How” of the Methods Responsibility and is a candidate for Helper Method abstraction
- Algorithms inside the Called method – These Code statements are prime candidates for change over the life cycle of the method. All calculations and complex algorithms should always be abstracted into private or public helper methods for reuse.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
using System; namespace SRP.PrintDocsLegacy { public class PrintData { private static void Main() { // Responsibility #1: Choose a Format - Hard coded for Landscape Only var printFormat = "11 x 17"; // Responsibility #2: Select a Device Type to Print Data - Only a Console is Available var printDevice = "I am Printing to the Console"; // Responsibility #3: Acquire the Print Data - The Class is Named 'PrintData' not 'Get and Print Data' var printData = "This is the Data from the Legacy Code"; // Responsibility #4: This is the True One and Only One Responsibility of the 'PrintData' Class Console.WriteLine(""); Console.WriteLine("The SRP Legacy Print Data Demo:n"); Console.WriteLine("tSelected Printer Format: {0}n", printFormat); Console.WriteLine("tSelected Printer Device: {0}n", printDevice); Console.WriteLine("tLegacy Print Data: {0}n", printData); Console.WriteLine("All Demo Data Display Complete"); Console.ReadKey(); } } } |
Issues with this code:
- Solution is not Extensible – Only supports the current requirement
- The Methods has Four Responsibilities – Three of the responsibilities need OCP to comply with SRP
- Class Name Is PrintData – The Format Selection dependency, the Device Type Selection dependency, and the Data Acquisition dependency are not part of the Print Data request. The Console Display code is the only responsibility of the class: All others are OCP Dependency refactoring
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using System; using SRP.PrintDocs.Refactored.Enums; using SRP.PrintDocs.Refactored.ViewModels; namespace SRP.PrintDocs.Refactored { public class PrintData { static void Main() { var printFormat = PrinterFormatter.FormatData(PrintFormat.Landscape); var printDevice = PrinterOutputDevice.SelectOutputDevice(OutputDevices.Console); var printData = PrinterOutputData.GetDataFromFile(); PrintDataToOutputDevice(printFormat, printDevice, printData); } #region Print Helper public static void PrintDataToOutputDevice(string printFormat, string printDevice, string printData) { Console.WriteLine(""); Console.WriteLine("The SRP Refactored Print Data Demo:n"); Console.WriteLine("tSelected Printer Format: {0}n", printFormat); Console.WriteLine("tSelected Printer Device: {0}n", printDevice); Console.WriteLine("tRefactored Print Data: {0}n", printData); Console.WriteLine("All Demo Data Display Complete"); Console.ReadKey(); } #endregion } } |
|
1 2 3 4 5 6 7 8 9 |
namespace SRP.PrintDocs.Refactored.Enums { public enum OutputDevices { Printer, XmlFile, Console } } |
|
1 2 3 4 5 6 7 8 |
namespace SRP.PrintDocs.Refactored.Enums { public enum PrintFormat { Landscape, Portrait } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace SRP.PrintDocs.Refactored.ModelDTOs { public class SamplePrintDataDTO { public string PrintData { get; set; } public SamplePrintDataDTO() { PrintData = "This is the Data from the Refactored Code"; } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System.Collections.Generic; namespace SRP.PrintDocs.Refactored.ModelDTOs { public class SamplePrintFormattingTypesDTO { public Dictionary<string, string=""> PrintFormatters { get; set; } public SamplePrintFormattingTypesDTO() { PrintFormatters = new Dictionary<string, string=""> { {"Portrait", "8.5 x 11"}, {"Landscape", "11 x 17"} }; } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using SRP.PrintDocs.Refactored.Enums; using SRP.PrintDocs.Refactored.ModelDTOs; namespace SRP.PrintDocs.Refactored.ViewModels { public class PrinterFormatter { public static string FormatData(PrintFormat format) { var formattingTypes = new SamplePrintFormattingTypesDTO(); return format == PrintFormat.Landscape ? formattingTypes.PrintFormatters["Landscape"] : formattingTypes.PrintFormatters["Portrait"]; } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
using SRP.PrintDocs.Refactored.ModelDTOs; namespace SRP.PrintDocs.Refactored.ViewModels { public class PrinterOutputData { public static string GetDataFromFile() { return new SamplePrintDataDTO().PrintData; } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using SRP.PrintDocs.Refactored.Enums; namespace SRP.PrintDocs.Refactored.ViewModels { public class PrinterOutputDevice { public static string SelectOutputDevice(OutputDevices outputDevice) { switch (outputDevice) { case OutputDevices.Printer: return "I am Printing to the Printer"; case OutputDevices.Console: return "I am Printing to the Console"; case OutputDevices.XmlFile: return "I am Printing to an XmlFile"; default: return "I can not print as I do not have a valid device"; } } } } |
The key take away from the above code is the first console application class.
Every line of code has a direct relationship to the Single Responsibility of the class:
Print Information to the Console Screen
The region holds the code for the responsibility method: PrintDataToOutputDevice()
All the dependencies have been extended and encapsulated in their own responsibilities using the three method on lines 11 – 13.
The refactored Version Complies with the
SRP, OCP and RDP Principles
and follows Best Practices for OOP
The Benefits of Compliance with the OCP Principle:
-
Creates code that is easier to understand
-
Allows the Maintenance Developer to work more efficiently
-
It fosters the creation of Public Utility Classes that support development process consistency
-
It identifies candidates for the Request / Response Design Pattern
-
Fosters a Team Paradigm of Code Reuse
-
Improves performance the better JIT code for the CLR
The First Five Benefits are the Same as DRY Principle!
The Next Principle Design Series Post:
The Liskov Substitution Principle
… If it looks like a Duck, quacks like a Duck
…… But needs Batteries
… You might need a better Abstraction!
Latest posts by Brad Huett (see all)
- DevOps: A Bridge to Your DevOps Culture - March 25, 2016
- Embracing Test Driven Development (TDD) - March 25, 2016
- DevOps: Delivering Agile Projects - March 25, 2016


