The second principle of SOLID is the Open/Closed principle: ‘Classes and other entities should be open for extension but closed for modification’. This article is about how in Delphi you keep a class closed for modification, but open for extension.
Let me emphasise one part; achieving this goal via inheritance or overriding classes is a bad idea in my opinion. If you want to read more about this, check out what Bertrand Mayer and Robert C. Martin have to say on this subject.
Okay, let’s take the following example:
type TShip = class // This is the actual ship end; TPartList = class // This class holds all the needed parts for building a ship end; TShipLayout = class // This class provides the blueprint of the ship to build function GetBlueprint: TList<TPoint>; // just as an example end; // This is the factory class to build a ship TShipBuilder = class private FParts: TPartList; FShipLayout: TShipLayout; public procedure LoadParts(APartList: TPartList); procedure LoadShipLayout(ALayout: TShipLayout); function BuildShip: TShip; end;
As you can see, these are simple classes, without interfaces and without the possibility to extend them easily. The ShipBuilder class is however already (partly) closed for modification; the internal operation is separated from the outside world because the local variables are made private. In Delphi however, you can still access these variables when you access this class from the same file. Therefore, a simple improvement is to make the private a strict private.
However, the inner workings of this class itself is still open for modification. One can easily call on the BuildShip function before even providing the necessary parts and ship layout.
But how do we ensure that these classes are open for extension, but remain closed for modification?
The answer? Interfaces!
Why interfaces? This has to do with another principle: program against interfaces and not against implementations. The example above is the implementation of the class TShipBuilder. As soon as you start working with this implementation, you are automatically ‘stuck’ with the TPartList and TShipLayout implementations as well. Let’s see how we can improve this. The first step is to create several interfaces and modify the TShipBuilder class to implement those new interfaces. The second step is to use dependency injection to open this class for extension, but close it for modification. If you do that, you will end up with something like this:
type TShip = class // This is the actual ship class end; IPartList = interface // Necessary info to provide the parts end; TPartList = class(TInterfacedObject, IPartList) // This class holds all the needed parts for building a ship end; IShipLayout = interface // Necessary info to provide the blueprint function GetBlueprint: TList<TPoint>; end; TShipLayout = class(TInterfacedObject, IShipLayout) // This class implements the blueprint function of the ship to build function GetBlueprint: TList<TPoint>; end; TShipBuilder = class strict private FParts: IPartList; FShipLayout: IShipLayout; procedure LoadParts; procedure LoadShipLayout; public constructor Create(APartList: IPartList; ALayout: IShipLayout); function BuildShip: TShip; end;
So, what exactly did we do? As you can see, we now have a TShipBuilder class which requires the two interfaces via a constructor. Both the LoadParts and LoadShipLayout procedures are made strict private, which ensures that the class is closed for modification, so we use these procedures only within the BuildShip function for example.
Additionally, our class is open for extension. It is possible to extend this ShipLayout functionality for example by just creating another implementation of the IShipLayout interface, as long as we implement the GetBlueprint function. We just have to make sure that we will provide a class with the IShipLayout interface implemented when calling the constructor of the TShipBuilder class.
As with most examples I’ve showed earlier, be aware that you shouldn’t put both the interfaces and classes all in the same file. It’s just for the sake of readability that these are in one place right now.
So, to summarise: to use the Open/Closed principle in Delphi you need interfaces to make a class open for extensions and make use of the (strict) private keyword to keep the class from modifications. Use as little inheritance as possible, because with inheritance, you risk opening the class for modifications. And remember: always program against interfaces instead of implementations.
Become a GDK Delphi hero! Subcribe here to our Delphi mailing list!