Skip to content

Working example with detailed commit history on the "replace subclass with delegate" refactoring based on Fowler's "Refactoring" book

License

Notifications You must be signed in to change notification settings

kaiosilveira/replace-subclass-with-delegate-refactoring

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Continuous Integration

ℹ️ This repository is part of my Refactoring catalog based on Fowler's book with the same title. Please see kaiosilveira/refactoring for more details.


Replace Subclass With Delegate

Before After
class Order {
  get daysToShip() {
    return this._warehouse.daysToShip;
  }
}

class PriorityOrder extends Order {
  get daysToShip() {
    return this._priorityPlan.daysToShip;
  }
}
class Order {
  get daysToShip() {
    return this._priorityDelegate ? this._priorityDelegate.daysToShip : this._warehouse.daystoShip;
  }
}

class PriorityOrderDelegate {
  get daysToShip() {
    return this._priorityPlan.daysToShip;
  }
}

Inheritance is at the core of Object-Oriented Programming, and it's the "go to" approach for most of the slight specific behaviors we want to isolate out of general, default ones. It has it's downsides, though: you can only vary in one dimension. Delegation helps in cases where we need to vary in multiple axis, and brings with it the benefit of a more structured separation of concerns, with reinforced indirection.

Working examples

The book brings us two examples: one related to regular and premium bookings, and one related to birds and their different species and specificities.

Example 01: Bookings

In this working example, we have a regular Booking class, with standard behavior, and a PremiumBooking class, which inherits from Booking and overrides some of its behaviors. We want to break down this inheritance and use delegation instead, so we can better control the variations from Booking itself.

Test suite

Since it's a relatively complex example, the test suite was ommited for brevity. Please check the initial commit which sets up the scenario for the actual implementation.

Steps

Our goal is to make Booking incorporate the details of premium bookings, via delegation. To accomplish that, we need a way to tell a Booking instance to be premium. As always, a good way to start is by introducing a layer of abstraction so we can have more control over the initializations. In this case, we add a factory for Booking creations:

diff --git top level...
+export function createBooking(show, date) {
+  return new Booking(show, date);
+}

and then we update the regular booking client to the factory:

diff --git top level...
-const booking = new Booking(show, date);
+const booking = createBooking(show, date);
 console.log(`Booking details`);
 console.log(

Same process goes for premium bookings. First the factory:

diff --git top level...
+export function createPremiumBooking(show, date, extras) {
+  return new PremiumBooking(show, date, extras);
+}

then the update:

diff --git top level...
-const booking = new PremiumBooking(show, date, extras);
+const booking = createPremiumBooking(show, date, extras);

Now, on to the delegate itself. We first create it:

diff --git PremiumBookingDelegate.js
+export class PremiumBookingDelegate {
+  constructor(hostBooking, extras) {
+    this._host = hostBooking;
+    this._extras = extras;
+  }
+}

and then take a moment to add a "private" _bePremium at Booking. This will be our bridge between regular booking behavior and premium functionality:

diff --git Booking.js
export class Booking {
+  _bePremium(extras) {
+    this._premiumDelegate = new PremiumBookingDelegate(this, extras);
+  }
 }

Now, back to the createPremiumBooking factory function, we can promote the booking to premium:

diff --git top level...
 export function createPremiumBooking(show, date, extras) {
-  return new PremiumBooking(show, date, extras);
+  const result = new PremiumBooking(show, date, extras);
+  result._bePremium(extras);
+  return result;
 }

Sharp eyes will notice that We are still initializing an instance of PremiumBooking, and that's because there's still behavior in the subclass. Our goal now is to start moving functionality to the delegate and, when we're finished, desintegrate the PremiumBooking for good.

On to moving functionality, we start by adding hasTalkback to the delegate:

diff --git PremiumBookingDelegate.js
export class PremiumBookingDelegate {
+  get hasTalkback() {
+    return this._host._show.hasOwnProperty('talkback');
+  }
 }

and then simply delegating the calculation at PremiumBooking:

diff --git PremiumBooking.js
export class PremiumBooking extends Booking {
   get hasTalkback() {
-    return this._show.hasOwnProperty('talkback');
+    return this._premiumDelegate.hasTalkback;
   }

We can also make Booking aware of the delegate, by modifying hasTalkback. Our assumption here is that whenever there's a premiumDelegate present, then it's a premium booking:

export class Booking {
   get hasTalkback() {
-    return this._show.hasOwnProperty('talkback') && !this.isPeakDay;
+    return this._premiumDelegate
+      ? this._premiumDelegate.hasTalkback
+      : this._show.hasOwnProperty('talkback') && !this.isPeakDay;
   }

With all the above in place, there's no difference between the behavior in PremiumBooking versus in Booking, so we remove hasTalkback from PremiumBooking:

export class PremiumBooking extends Booking {
     this._extras = extras;
   }
-  get hasTalkback() {
-    return this._premiumDelegate.hasTalkback;
-  }

Moving the price is trickier, and the path we find to do that is by extension. PremiumBookingDelegate now has a method to extend the base price to include the premium fee:

export class PremiumBookingDelegate {
+  extendBasePrice(base) {
+    return Math.round(base + this._extras.premiumFee);
+  }
 }

And all we need to do in Booking is to apply the premium fee if it's a premium booking:

export class Booking {
   get basePrice() {
     let result = this._show.price;
     if (this.isPeakDay) result += Math.round(result * 0.15);
-    return result;
+    return this._premiumDelegate ? this._premiumDelegate.extendBasePrice(result) : result;
   }

With all the above, we can now remove basePrice from PremiumBooking:

export class PremiumBooking extends Booking {
-  get basePrice() {
-    return Math.round(super.basePrice + this._extras.premiumFee);
-  }

Last one is hasDinner, a method that was implemented only at PremiumBooking. We first nove it to PremiumBookingDelegate:

export class PremiumBookingDelegate {
+  get hasDinner() {
+    return this._extras.hasOwnProperty('dinner') && !this._host.isPeakDay;
+  }

Then implement hasDinner at Booking, using the same thought process as above:

export class Booking {
+  get hasDinner() {
+    return this._premiumDelegate ? this._premiumDelegate.hasDinner : false;
+  }

And, finally, we remove hasDinner from PremiumBooking:

export class PremiumBooking extends Booking {
-  get hasDinner() {
-    return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
-  }

Now, the behavior of the superclass is the same of the subclass, so we can create regular Booking instances before promotion at createPremiumBooking:

 export function createPremiumBooking(show, date, extras) {
-  const result = new PremiumBooking(show, date, extras);
+  const result = new Booking(show, date, extras);
   result._bePremium(extras);
   return result;
 }

And, finally, delete PremiumBooking:

-export class PremiumBooking extends Booking {
-  constructor(show, date, extras) {
-    super(show, date);
-    this._extras = extras;
-  }
-}

And that's it! We ended up with a single Booking class that eventually delegates particular behavior to a specialized PremiumBookingDelegate.

Commit history

Below there's the commit history for the steps detailed above.

Commit SHA Message
b6bd73d add factory for creating a Booking
10adc0a update regular booking client to use createBooking factory fn
05667f1 introduce createPremiumBooking factory fn
5b36b3b update premium booking client to use createPremiumBooking
78c9449 introduce PremiumBookingDelegate
d0409f5 implement _bePremium at Booking
f6308e3 promote booking to premium at createPremiumBooking
2c7d737 move hasTalkback to PremiumBookingDelegate
6da9026 delegate hasTalkback to PremiumBookingDelegate at PremiumBooking
cfd3976 add conditional premium logic at Booking.hasTalkback
38bdb96 remove hasTalkback from PremiumBooking
7f20218 extend base price with premium fee at PremiumBookingDelegate
7b83061 apply premium fee if booking is premium
e2b7d37 remove basePrice from PremiumBooking
898a827 implement hasDinner at PremiumBookingDelegate
ebf529f implement hasDinner at Booking
001d4ca remove hasDinner from PremiumBooking
b9ed371 create regular Booking before promotion at createPremiumBooking
724b334 delete PremiumBooking

commit history for this project, check the Commit History tab.

Example 02: Birds

In this working example, we're dealing with birds. Our class hierarchy was created around species, but we want to add yet another dimension to the mix: whether birds are domestic or wild. This will require a replacement of inheritance with composition.

Test suite

Since it's a relatively complex example, the test suite was ommited for brevity. Please check the initial commit which sets up the scenario for the actual implementation.

Steps

Our main goal is to have a series of delegates, one for each species, that are resolved and referenced in runtime by Bird. We start by introducing a EuropeanSwallowDelegate and adding a _speciesDelegate to Bird, with custom delegate selection based on the bird type:

diff --git european-swallow/delegate
+export class EuropeanSwallowDelegate {}

diff --git Bird.js
 export class Bird {
   constructor(data) {
     this._name = data.name;
     this._plumage = data.plumage;
+    this._speciesDelegate = this.selectSpeciesDelegate(data);
   }

+  selectSpeciesDelegate(data) {
+    switch (data.type) {
+      case 'EuropeanSwallow':
+        return new EuropeanSwallowDelegate();
+      default:
+        return null;
+    }
+  }
 }

We can then start moving logic around. We start by moving airSpeedVelocity to EuropeanSwallowDelegate, which is easy since it has a fixed value:

-export class EuropeanSwallowDelegate {}
+export class EuropeanSwallowDelegate {
+  get airSpeedVelocity() {
+    return 35;
+  }
+}

Then, Bird can now make use of airSpeedVelocity from the delegate:

export class Bird {
   get airSpeedVelocity() {
-    return null;
+    return this._speciesDelegate ? this._speciesDelegate.airSpeedVelocity : null;
   }

With that, all the specific behavior of EuropeanSwallow is now covered by Bird, via the delegate. So we can remove it from the createBird factory:

 export function createBird(data) {
   switch (data.type) {
-    case 'EuropeanSwallow':
-      return new EuropeanSwallow(data);
     case 'AffricanSwallow':
       return new AffricanSwallow(data);
     case 'NorwegianBlueParrot':

And delete the subclass:

-export class EuropeanSwallow extends Bird {
-  constructor(data) {
-    super(data);
-  }
-
-  get airSpeedVelocity() {
-    return 35;
-  }
-}

We repeat the process for AfricanSwallow. First the delegate:

+export class AfricanSwallowDelegate {
+  constructor(data) {
+    this._numberOfCoconuts = data.numberOfCoconuts;
+  }
+}

... then the airSpeedVelocity implementation:

export class AffricanSwallowDelegate {
+  get airSpeedVelocity() {
+    return 40 - 2 * this._numberOfCoconuts;
+  }
 }

... then the type resolution:

export class Bird {
     switch (data.type) {
       case 'EuropeanSwallow':
         return new EuropeanSwallowDelegate();
+      case 'AffricanSwallow':
+        return new AffricanSwallowDelegate(data);
       default:
         return null;
     }

... then the delegation of the call:

export class AffricanSwallow extends Bird {
   get airSpeedVelocity() {
-    return 40 - 2 * this._numberOfCoconuts;
+    return this._speciesDelegate.airSpeedVelocity;
   }
 }

... then the removal from the factory:

 export function createBird(data) {
   switch (data.type) {
-    case 'AffricanSwallow':
-      return new AffricanSwallow(data);
     case 'NorwegianBlueParrot':
       return new NorwegianBlueParrot(data);
     default:

... and, finally, the deletion:

-export class AffricanSwallow extends Bird {
-  constructor(data) {
-    super(data);
-    this._numberOfCoconuts = data.numberOfCoconuts;
-  }
-
-  get airSpeedVelocity() {
-    return this._speciesDelegate.airSpeedVelocity;
-  }
-}

And the same goes to NorwegianBlueParrot. First the delegate:

+export class NorwegianBlueParrotDelegate {
+  constructor(data) {
+    this._voltage = data.voltage;
+    this._isNailed = data.isNailed;
+  }
+}

... then the airSpeedVelocity migration:

export class NorwegianBlueParrotDelegate {
+  get airSpeedVelocity() {
+    return this._isNailed ? 0 : 10 + this._voltage / 10;
+  }
 }

... then the type resolution:

 export class Bird {
  selectSpeciesDelegate(data) {
    switch (data.type) {
       case 'AffricanSwallow':
         return new AffricanSwallowDelegate(data);
+      case 'NorwegianBlueParrot':
+        return new NorwegianBlueParrotDelegate(data);
       default:
         return null;
     }

... then the call delegation:

export class NorwegianBlueParrot extends Bird {
   get airSpeedVelocity() {
-    return this._isNailed ? 0 : 10 + this._voltage / 10;
+    return this._speciesDelegate.airSpeedVelocity;
   }
 }

And here things change a bit. We have plumage, which is difficult to get rid of. We first add it to the delegate:

 export class NorwegianBlueParrotDelegate {
-  constructor(data) {
+  constructor(data, bird) {
     this._voltage = data.voltage;
     this._isNailed = data.isNailed;
+    this._bird = bird;
   }
   get airSpeedVelocity() {
     return this._isNailed ? 0 : 10 + this._voltage / 10;
   }
+
+  get plumage() {
+    if (this._voltage > 100) return 'scorched';
+    return this._bird._plumage || 'beautiful';
+  }
 }

But, since it needs info from the bird itself, we provide a back reference to Bird at NorwegianBlueParrotDelegate:

export class Bird {
       case 'AffricanSwallow':
         return new AffricanSwallowDelegate(data);
       case 'NorwegianBlueParrot':
-        return new NorwegianBlueParrotDelegate(data);
+        return new NorwegianBlueParrotDelegate(data, this);
       default:
         return null;
     }

And now plumage can be delegated at NorwegianBlueParrot:

export class NorwegianBlueParrot extends Bird {
   }
   get plumage() {
-    if (this._voltage > 100) return 'scorched';
-    return this._plumage || 'beautiful';
+    return this._speciesDelegate.plumage;
   }

But, since the other subclasses don't have this method implemented, if we modify the Bird class to invoke the call on the delegate, we'll have some serious errors. The solution to that is by introducing delegate... inheritance!

+export class SpeciesDelegate {
+  constructor(data, bird) {
+    this._bird = bird;
+  }
+
+  get plumage() {
+    return this._bird._plumage || 'average';
+  }
+}

And update all other delegates to extend the base class. We start with AffricanSwallowDelegate:

+export class AffricanSwallowDelegate extends SpeciesDelegate {
+  constructor(data, bird) {
+    super(data, bird);
     this._numberOfCoconuts = data.numberOfCoconuts;
   }

... thenEuropeanSwallowDelegate:

+export class EuropeanSwallowDelegate extends SpeciesDelegate {
+  constructor(data, bird) {
+    super(data, bird);
+  }

... and, finally, NorwegianBlueParrotDelegate:

+export class NorwegianBlueParrotDelegate extends SpeciesDelegate {
   constructor(data, bird) {
+    super(data, bird);
     this._voltage = data.voltage;
     this._isNailed = data.isNailed;
     this._bird = bird;

And, now, we can safely delegate plumage to speciesDelegate at Bird:

export class Bird {
   get plumage() {
-    return this._plumage || 'average';
+    return this._speciesDelegate.plumage;
   }
 }

And since we have a base species class in place, we can move default behavior, such as airSpeedVelocity, there as well:

export class SpeciesDelegate {
+  get airSpeedVelocity() {
+    return null;
+  }
 }

Finally, we can stop resolving the NorwegianBlueParrot subclass at createBird:

 export function createBird(data) {
   switch (data.type) {
-    case 'NorwegianBlueParrot':
-      return new NorwegianBlueParrot(data);
     default:
       return new Bird(data);
   }

And delete it:

-export class NorwegianBlueParrot extends Bird {
-  constructor(data) {
-    super(data);
-    this._voltage = data.voltage;
-    this._isNailed = data.isNailed;
-  }
-
-  get plumage() {
-    return this._speciesDelegate.plumage;
-  }
-
-  get airSpeedVelocity() {
-    return this._speciesDelegate.airSpeedVelocity;
-  }
-}

And that's it! Now all species-related behavior is well encapsulated into each of the delegates, with a base delegate class providing default behavior.

Commit history

Below there's the commit history for the steps detailed above.

Commit SHA Message
82926e2 introduce _speciesDelegate at Bird with custom selection
a69e88e set airSpeedVelocity to 35 at EuropeanSwallowDelegate
8fc679d use airSpeedVelocity from delegate if present at Bird
a0cf2ae remove EuropeanSwallow from createBird factory
b1fbd71 delete EuropeanSwallow subclass
a4d9600 add AffricanSwallowDelegate
81bfbaa implement airSpeedVelocity at AffricanSwallowDelegate
adb92cb add AffricanSwallow to speciesDelegate resolution
f550605 delegate airSpeedVelocity to speciesDelegate at AffricanSwallow
b76480c stop using AffricanSwallow subclass at createBird factory
00b266d delete AffricanSwallow subclass
0d75aa4 add NorwegianBlueParrotDelegate
e921119 add airSpeedVelocity to NorwegianBlueParrotDelegate
cd55b49 add NorwegianBlueParrot to speciesDelegate resolution
fbef8ff delegate airSpeedVelocity to speciesDelegate at NorwegianBlueParrot
2394673 add plumage to NorwegianBlueParrotDelegate
9d6dabc provide self ref to NorwegianBlueParrotDelegate
cec3388 delegate plumage at NorwegianBlueParrot
f328d93 introduce SpeciesDelegate superclass
a39f5dc make AffricanSwallowDelegate extend SpeciesDelegate
15659a1 make EuropeanSwallowDelegate extend SpeciesDelegate
a69ba4d make NorwegianBlueParrotDelegate extend SpeciesDelegate
3a85cfc delegate plumage to speciesDelegate at Bird
e6553b2 implement default airSpeedVelocity at Bird
02a8dc5 stop resolving NorwegianBlueParrot subclass at createBird factory fn
1cc7794 delete NorwegianBlueParrot subclass

For the full commit history for this project, check the Commit History tab.

About

Working example with detailed commit history on the "replace subclass with delegate" refactoring based on Fowler's "Refactoring" book

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project