The Open/Closed Principle
This principle is considered probably the most important of object-oriented design. The open/closed principle states that your code (classes, modules, functions, etc.) should be open for extension, but closed to modification.
What does that mean?
It means that you should be able to add new functionality without changing your existing code. It uses interfaces to allow different implementations of your code that you can substitute without changing the code that uses them.
The benefit of using an interface is that it introduces an additional level of abstraction, which enables loose coupling. Implementing interfaces are independent of each other and don’t need to share any code.
Example 1: Not Closed to Modification
Let’s consider an example of figuring out the area of a rectangle. First, you have a rectangle that has width and height.
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
}
Next, create an Area
calculator that calculates the total area for a collection of rectangles:
public class AreaCalculator
{
public double Area(Rectangle[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
area += shape.Width * shape.Height;
}
return area;
}
}
Great! It works! But, what happens if you want to know the area of a circle? Hmmm. You’ll have to modify the Area
method to handle circles.
So, modify the Area
to accept a collection of objects instead of a Rectangle
type. Then you can check the type of each object to figure out if it’s a rectangle or circle and calculate the area accordingly.
public double Area(object[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
if (shape is Rectangle)
{
var rectangle = (Rectangle)shape;
area += rectangle.Width * rectangle.Height;
}
else
{
var circle = (Circle)shape;
area += circle.Radius * circle.Radius * Math.PI;
}
}
return area;
}
OK, now that’s working. But, what happens if you want to find the area of triangles? Ugh. You're going to have to modify the Area
method again!
As you can see, this is breaking the open/closed principle, which states that your method should be closed to modification. Unfortunately, the Area
method is not closed to modification and not open for extension.
Example 2: Utilize Contracts
Fix this by creating a contract, which requires a class that inherits it to utilize all the items defined. In C#, contracts are interfaces and abstract classes.
Abstract Classes
With an abstract class, you can hide internal details and show only the functionality.
An abstract class is never instantiated.
Must contain at least one abstract method.
Typically used to define a base class that other classes inherit.
It can contain constructors.
It can implement functions with non-abstract methods.
Cannot support multiple inheritance.
Can’t be static.
Here's an example of a C# program utilizing an abstract class:
using System;
public abstract class MyAbstract
{
public abstract void myTest();
}
public class MyClass1 : MyAbstract
{
public override void myTest()
{
Console.WriteLine("This is myTest from MyClass1.");
}
}
public class MyClass2 : MyAbstract
{
public override void myTest()
{
Console.WriteLine("This is myTest from MyClass2.");
}
}
Interfaces
Like a class, interfaces can have methods, properties, indexers, and events. But they will only contain the declaration of the members. The implementation of an interface’s members is done by the class that implements the interface.
For our example, we are going to use an interface.
Let’s create an interface for shape:
public interface IShape
{
double Area();
}
Now, our classes for Rectangle
and Circle
inherit the IShape
interface:
public class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public double Area()
{
return Width * Height;
}
}
public class Circle : IShape
{
public double Radius { get; set; }
public double Area()
{
return Radius * Radius * Math.PI;
}
}
In doing so, we moved the responsibility of calculating the area away from the AreaCalculator’s Area
method to each class.
How would I implement these classes in my application?
One way is to use the concept of dependency injection. Injection is the idea that an object is given (or "injected") an object to use, instead of making the object itself. You have the object that has the injection and a method that takes an interface as a parameter. That way, some other place in the code can instantiate an object that implements the interface and then uses that object for what gets injected.
In our application, instead of instantiating one of the two types of IShape
, we will inject the one we want. Let's say our application has an MVC controller called ShapeController. We can add a parameter to the controller constructor, which is of type IShape
. Then, we can pass in the correct specific implementation as that parameter.
public ShapeController : Controller
{
private IShape _shape;
public ShapeController(IShape shape)
{
_shape = shape;
}
}
We still have to deal with one more detail. We need to create the actual IShape
object to be injected into the ShapeController
. You will see how to do that in a later section of the course.
Let’s Recap!
The open/closed principle states that your code (classes, modules, functions, etc.) should be open for extension, but closed to modification.
Using contracts like interfaces enables you to adapt the functionality of your application without changing existing code.
An abstract class can’t be instantiated and can implement functions.
An interface contains only the declaration of methods, properties, indexers, and events.
Let’s move on the third letter in SOLID, “L,” for the Liskov substitution principle.