Srikanth Sastry
•
24 January 2019
•
5 mins
Object Composition is a very powerful and pervasive software design technique. Yet, paradoxically, it is an underutilized design pattern whose lack of usage is the root of many anti-patterns in software development. One that I continue to come across regularly has to do with not using composition to test and migrate a piece of software from one service to another.
Briefly, Object Composition is combining two or more objects to create more complex objects while keeping definitions of the constituent objects unchanged (unlike inheritance, which extends these definitions)
Say you have an existing backend service that your code currently uses. It has evolved over time to become a chimera that needs replacing, and you have a brand new implementation of that service that can replace your old service.
Say, the two client implementations looks something like the following:
class OldAndBusted implements ServiceClient {
@override
Response process(Request request) {
// Hacky code.
if (request.type == A) {
// Ugly code.
} else if (request.type == B) {
// Even uglier code.
} else {
// A monstrosity that needs to be killed with fire
}
return response;
}
}
class NewHotness implements ServiceClient {
@override
Response process(Request request) {
// Best code ever written.
return response;
}
}
The goal is to migrate your code from using OldAndBusted to NewHotness. There are several ways to do this wrong. So it is easier if I demonstrate a right way to do this using Object Composition.
There are really four steps to such a migration.
The goal here is to ensure that before we start migration, the new service is functionally identical to the old service. We accomplish this through composition of old and new service as sketched out next.
class ClientWithShadow implements ServiceClient {
ClientWithShadow(ServiceClient oldAndBusted,
ServiceClient newHotness) {
this.oldAndBusted = oldAndBusted;
this.newHotness = newHotness;
}
@override
Response process(Request request) {
oldResponse = oldAndBusted.process(request);
if (shouldShadow(request)) {
newResponse = newHotness.process(request);
if (!oldResponse.equals(newResponse)) {
logMismatch(oldResponse, newResponse);
}
}
return newResponse;
}
The pseudocode code above simply delegates calls to the old service, and if shadowing is requires, it additionally delegates to the new service as well and compares the two outputs. It logs any mismatches it sees so that the developer can then take a look at it to ensure that it is addressed.
You simply replace all calls to OldAndBusted with calls to ClientWithShadow.
After you have determined that the two services are indeed functionally alike, we can then prep for migration. Again, object composition helps us set this up cleanly.
Pseudocode for setting up such a migration follows next. Here, I assume that there is a Config object that contains the migration related config.
class MigrationClient implements ServiceClient {
MigrationClient(ServiceClient oldAndBusted,
ServiceClient newHotness,
Config migrationConfig) {
this.oldAndBusted = oldAndBusted;
this.newHotness = newHotness;
this.migrationConfig = migrationConfig;
}
@override
Response process(Request request) {
if (migrationConfig.useNewService(request)) {
return newHotness.process(request);
}
return oldAndBusted.process(request);
}
You simply replace all instances of ClientWithShadow with MigrationClient. Yes, it really is that simple! The migration config has all the info it needs to figure out whether a given request should use the new service or the old service.
Here, we do the actual migration. We set up the config to slowly start shifting some of the load from the old service to the new one, while monitoring to make sure everything is going well. We can always roll back the migration by editing the config without actually modifying the code, which is a big deal here.
After migration to the new service a 100%, you can simply replace MigrationClient instances with NewHotness instances, and delete all the old code (OldAndBusted, ClientWithShadow, and MigrationClient). And you are all cleaned up. Profit!
Unfortunately, I have seen this done in way too many wrong ways.
So, object composition is useful, it is powerful, and please use it more!
Like it? Share it!