Practical Object-Oriented Design Book 心得&整理 (7)
不管DDD、TDD、Design Pattern、Refactoring都是大師們淬鍊出來的結晶,如果我們連原料都沒有的話要如何淬鍊出跟大師們一樣的結晶呢?話不多說就讓本篇文章承接上一回繼續介紹物件導向設計,讓大家對於原料 (物件)的使用有更深入的認知。這樣一來不管是DDD、TDD、Design Pattern、Refactoring都能更容易上手。
未來文章根據下方依序介紹:
- What is Object-Oriented Design
- Designing Classes with a Single Responsibility
- Managing Dependencies
- Creating Flexible Interfaces
- Reducing Costs with Duck Typing
- Acquiring Behavior through Inheritance
- Sharing Role Behavior with Module (*)
- Combining Object with Composition
- Designing Cost-Effective Tests
在上一篇文章中,我們介紹了繼承的概念:繼承透過automatic message delegation的方式讓子類別能接收父類別的訊息 (共享行為)、繼承屬於specialization,讓我們能夠針對穩定的抽象化進行部分細節的修改。
在更之前的文章,我們介紹了Duck Type的概念:針對sender所需要的行為來定義介面 (DIP),並且關注What而不是How,能讓我們在不影響sedner的情況底下替換不同的類別 (OCP)。如果當不同類別想要共享Duck Type的行為時應該怎麼辦呢?能否使用繼承的方式呢?還是有另外的方式呢?
這篇文章我們將會介紹Role與Module這兩個概念來回答上述問題,並且透過Role與Module讓遵守Duck Type的類別共享行為。
Role
Some problems require sharing behavior among otherwise unrelated objects. This common behavior is orthogonal to class; it’s a role an object plays
作者在書中提到:有時我們需要讓不相干的物件共享行為,這些行為跟物件的類別沒有太大的關聯性,我們關注的是物件所扮演的角色。當物件能接收特定訊息時就能扮演特定角色。
上面的描述可能有點抽象,換成生活化的例子就是:
- 如果能撥打電話、傳送簡訊,便能夠扮演電話這個角色。
- 如果能看時間、調整時間,便能夠扮演手錶這個角色。
- 如果能炒菜、備料,便能夠扮演廚師這個角色。
決定能夠扮演角色與否的關鍵在於行為,而不是實作類別。像是只要能炒菜、備料 (message),不管男生 (Class)、女生 (Class)都能夠扮演廚師 (Role)這個角色。
之前的文章中提到了英雄吃補品的概念:
private static class Hero {
private int healthPoint;
private int magicPoint;
private int combatEffectiveness; public Hero() {
this.healthPoint = 100;
this.magicPoint = 100;
this.combatEffectiveness = 10;
} public void eatGoods(Goods goods) {
goods.applyEffect(this);
} public void addHealthPoint(int healthPoint) {
this.healthPoint += healthPoint;
} public void addMagicPoint(int magicGoods) {
this.magicPoint = magicGoods;
} public void addCombatEffectiveness(int combatEffectiveness) {
this.combatEffectiveness += combatEffectiveness;
}
}private interface Goods {
void applyEffect(Hero hero);
}private static class HealthGoods implements Goods {
private int healthPoint; public HealthGoods(int healthPoint) {
this.healthPoint = healthPoint;
} @Override
public void applyEffect(Hero hero) {
hero.addHealthPoint(this.healthPoint);
}
}private static class MagicGoods implements Goods {
private int magicPoint; public MagicGoods(int magicPoint) {
this.magicPoint = magicPoint;
} @Override
public void applyEffect(Hero hero) {
hero.addMagicPoint(this.magicPoint); }
}private static class CombatEffectivenessGoods implements Goods {
private int combatEffectivenessPoint; CombatEffectivenessGoods(int combatEffectivenessPoint) {
this.combatEffectivenessPoint = combatEffectivenessPoint;
} @Override
public void applyEffect(Hero hero) {
hero.addCombatEffectiveness(this.combatEffectivenessPoint);
}
}
範例程式中的Goods為一種Role,它定義了applyEffect行為,想要扮演Goods的物件都要能接收applyEffect訊息。
從上述程式碼我們看到不同類別的物件對於applyEffect實作的行為皆不相同,他們只需要共享了Role的method signature即可而不是實作細節。
但有時我們不僅僅需要共享method signature,我們還需要共享行為,但如果為了共享Role的行為而使用繼承時,會有以下缺點:
- 繼承只能使用一次,物件沒辦法扮演多個角色。
- 繼承會導致不相干的類別耦合在一起,增加不必要的複雜度。
因此我們需要另外的方法來共享行為,並且避免繼承帶來的耦合。那這時候該如何共享呢?讓我們繼續看下去….
Module
Ruby讓我們可以將特定的行為綑綁起來,並且將這些行為加入到不同的類別。當物件接收到訊息時,便會使用automatic message delegation的方式呼叫這些行為。
這樣的機制解決了Role共享行為的問題,能夠讓不相干類別的物件透過加入module扮演同樣的角色
範例程式碼:
module X // 定義module X擁有兩個行為
def functionA
// do somethings
end
def functionB(arg1, arg2)
// do somethings
end
endclass A // class A為了扮演X這個角色,include module X
include X
endclass B // class B為了扮演X這個角色,include module X
include X
enda = A.new
a.functionA()
a.functionB(arg1, arg2)b = B.new
b.functionA()
b.functionB(arg1, arg2)
因為Java沒有module的概念,如果想要共享Role的行為,只能使用interface inheritance and object composition的方式。
interface X {
void functionA();
void functionB(String arg1, String arg2);
class XImp implements X {
@Override
public void functionA() {
// do somethings
}
@Override
public void functionB(String arg1, String arg2) {
// do somethings
}
}
}
class A implements X {
private final X x = new XImp();
@Override
public void functionA() {
x.functionA();
}
@Override
public void functionB(String arg1, String arg2) {
x.functionB(arg1, arg2);
}
}
class B implements X {
private final X x = new XImp();
@Override
public void functionA() {
x.functionA();
}
@Override
public void functionB(String arg1, String arg2) {
x.functionB(arg1, arg2);
}
}
我們可以發現,由於Java只有繼承有automatic message delegation的機制,如果不使用繼承,我們需要宣告介面,並且提供基礎實作,然後讓需要扮演角色的類別使用delegate的方式來共享行為。
相較於Ruby的module,Java的方式有以下缺點:
- 需要額外撰寫delegate的程式碼,導致重複程式碼。
- 需要定義介面與基礎實作。
- 當介面修改時,每個扮演角色的類別,皆需要修改。
結論:
- Role的定義在於行為,只要物件能接收特定訊息,便能扮演特定角色,與實作類別無關。
- 繼承能夠讓相似的類別共享行為。
- 繼承會導致不相干類別互相耦合,增加複雜度。
- 在Ruby中,為了讓不同物件扮演角色並共享行為,能夠使用module。
- 在Java中,為了讓不同物件扮演角色並共享行為,只能使用interface inheritance and object composition。