The Open Closed Principle
The open closed principle of object oriented design states:
Software entities like classes, modules and functions should be open for extension but closed for modifications.
The Open Close Principle encourages software developers to design and write code in a fashion that adding new functionality would involve minimal changes to existing code. Most changes will be handled as new methods and new classes. Designs following this principle would result in resilient code which does not break on addition of new functionality.
In this article, we will explore the the open closed principle via an example of a resource allocator.
Original Code (Violates the Open Closed Principle)
The code below shows a resource allocator. The resource allocator currently handles timeslot and spaceslot resource allocation. It is clear from the code below that it does not follow the Open Closed Principle. The code of the resource allocator will have to be modified for every new resource type that needs to be supported. This has several disadvantages:
- The resource allocator code needs to be unit tested whenever a new resource type is added.
- Adding a new resource type introduces considerable risk in the design as almost all aspects of resource allocation have to be modified.
- Developer adding a new resource type has to understand the inner workings for the resource allocator.
Thus we can say that extending the design involves considerable code change.
Resource Allocator (Violates Open Closed Principle)
class ResourceAllocator | |
{ | |
public: | |
int Allocate(int resourceType) | |
{ | |
int resourceId; | |
switch (resourceType) | |
{ | |
case TIME_SLOT: | |
resourceId = FindFreeTimeslot(); | |
MarkTimeslotBusy(resourceId); | |
break; | |
case SPACE_SLOT: | |
resourceId = FindFreeSpaceSlot(); | |
MarkSpaceslotBusy(resourceId); | |
break; | |
default: | |
Trace(ERROR, "Attempted to allocate invalid resource\n"); | |
break; | |
} | |
} | |
int Free(int resourceType, int resourceId) | |
{ | |
switch (resourceType) | |
{ | |
case TIME_SLOT: | |
MarkTimeslotFree(resourceId); | |
break; | |
case SPACE_SLOT: | |
MarkSpaceslotFree(resourceId); | |
break; | |
default: | |
Trace(ERROR, "Attempted to allocate invalid resource\n"); | |
break; | |
} | |
} | |
}; |
Code Modified to Support Open Closed Principle
The problems with the above design is that it violates the Open Closed Principle. The following code presents a new design where the resource allocator is completely transparent to the actual resource types being supported. This is accomplished by adding a new abstraction, resource pool. The resource allocator directly interacts with the abstract class resource pool.
This has several advantages:
- The resource allocator code need not be unit tested whenever a new resource type is added.
- Adding a new resource type is fairly low risk as adding a new resource type does not involve changes to the resource allocator.
- Developer adding a new resource type does not need understand the inner workings for the resource allocator
- Further abstractions can be developed to group together resources that use
similar algorithms to allocate resources. A few examples are:
- FreeQueueResourcePool: Base class for all resource pools that are implemented with free/busy queue.
- BookingResourcePool: Base class for all resource pools that are implemented as timebound bookings (similar to ticket booking in a movie theater).
Resource Allocator (Follows Open Closed Principle)
class ResourceAllocator | |
{ | |
ResourcePool *m_pResourcePool[MAX_RESOURCE_POOLS]; | |
public: | |
int Allocate(int resourceType) | |
{ | |
int resourceId; | |
resourceId = m_pResourcePool[resourceType]->FindFree(); | |
m_pResourcePool[resourceType]->MarkBusy(resourceId]; | |
} | |
int Free(int resourceType, int resourceId) | |
{ | |
m_pResourcePool[resourceType]->MarkBusy(resourceId]; | |
} | |
void AddResourcePool(int resourceType, ResourcePool *pPool) | |
{ | |
m_pResourcePool[resourceType] = pPool; | |
} | |
}; | |
class ResourcePool | |
{ | |
public: | |
virtual int FindFree() = 0; | |
virtual int MarkBusy() = 0; | |
virtual void Free(int resourceId) = 0; | |
}; | |
class TimeslotPool : public ResourcePool | |
{ | |
public: | |
int FindFree(); | |
int MarkBusy(); | |
Free(int resourceId); | |
}; | |
class SpaceslotPool : public ResourcePool | |
{ | |
public: | |
int FindFree(); | |
int MarkBusy(); | |
Free(int resourceId); | |
}; |
Open and Closed
The above design follows the open and closed principle. The Resource Pool is open for extension as new resource pools can be added without much impact on the rest of the system. The Resource Allocator is closed for change, as no changes need to be made to it for enhancing the system.
As you can see the above has been achieved by using two techniques:
- A base class was defined for Resource Pool. This base class captures the high level interfaces.
- An array of Resource Pool pointers was defined in the Resource Allocator. This array is indexed by the resource id.
If this principle is followed during the design, most changes to the system would be in terms of adding new classes/methods. Changes to existing classes/methods would be minimized.
Another Example
Consider another example of a Call_Manager object. The Call_Manager manages a Call abstract class. The Call_Manager design is open for extension but closed for modification. Addition of a new call type requires writing a new class that inherits from Call. No changes are needed in the Call_Manager.
C# code for the example is presented below:
Open Closed Principle Example
using System; | |
using System.Collections.Generic; | |
using System.Text; | |
namespace Open_Closed_Principle | |
{ | |
public class Call_Manager | |
{ | |
protected Call[] _calls; | |
// Creates a call. Returns the assigned call_id. Returns -1 if the call | |
// could not be created. | |
public int Create_Call(Call call) | |
{ | |
int found_call_id = -1; | |
for (int call_id = 0; call_id < _calls.Length; call_id++) | |
{ | |
if (_calls[call_id] == null) | |
{ | |
found_call_id = call_id; | |
_calls[call_id] = call; | |
_calls[call_id].Handle_Create(); | |
break; | |
} | |
} | |
return found_call_id; | |
} | |
// Deletes the call specified with the call_id. | |
// Returns true if a call is deleted | |
public bool Delete_Call(int call_Id) | |
{ | |
Call found_call = null; | |
if (_calls[call_Id] != null) | |
{ | |
found_call = _calls[call_Id]; | |
found_call.Handle_Delete(); | |
_calls[call_Id] = null; | |
} | |
return (found_call != null); | |
} | |
public void Dump() | |
{ | |
foreach (Call call in _calls) | |
{ | |
if (call != null) | |
{ | |
call.Dump(); | |
} | |
} | |
} | |
} | |
abstract public class Call | |
{ | |
public abstract void Handle_Create(); | |
public abstract void Handle_Delete(); | |
public abstract void Dump(); | |
} | |
public class ISUP_Call : Call | |
{ | |
public override void Handle_Create() | |
{ | |
// Create an ISUP call | |
} | |
public override void Handle_Delete() | |
{ | |
// Delete an ISUP Call | |
} | |
public override void Dump() | |
{ | |
// Dump the contents on an ISUP call | |
} | |
} | |
public class ISDN_Call : Call | |
{ | |
public override void Handle_Create() | |
{ | |
// Create an ISDN call | |
} | |
public override void Handle_Delete() | |
{ | |
// Delete an ISDN Call | |
} | |
public override void Dump() | |
{ | |
// Dump the contents on an ISDN call | |
} | |
} | |
public class GSM_Call : Call | |
{ | |
public override void Handle_Create() | |
{ | |
// Create an GSM call | |
} | |
public override void Handle_Delete() | |
{ | |
// Delete an GSM Call | |
} | |
public override void Dump() | |
{ | |
// Dump the contents on an GSM call | |
} | |
} | |
} | |