ℹ️ This repository is part of my Refactoring catalog based on Fowler's book with the same title. Please see kaiosilveira/refactoring for more details.
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.
The book brings us two examples: one related to regular and premium bookings, and one related to birds and their different species and specificities.
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.
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.
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
.
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.
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.
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.
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.
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.