Practical Object-Oriented Design Book 心得&整理 (7)

Z-xuan Hong
9 min readJun 30, 2021

--

不管DDD、TDD、Design Pattern、Refactoring都是大師們淬鍊出來的結晶,如果我們連原料都沒有的話要如何淬鍊出跟大師們一樣的結晶呢?話不多說就讓本篇文章承接上一回繼續介紹物件導向設計,讓大家對於原料 (物件)的使用有更深入的認知。這樣一來不管是DDD、TDD、Design Pattern、Refactoring都能更容易上手。

未來文章根據下方依序介紹:

在上一篇文章中,我們介紹了繼承的概念:繼承透過automatic message delegation的方式讓子類別能接收父類別的訊息 (共享行為)、繼承屬於specialization,讓我們能夠針對穩定的抽象化進行部分細節的修改。

在更之前的文章,我們介紹了Duck Type的概念:針對sender所需要的行為來定義介面 (DIP),並且關注What而不是How,能讓我們在不影響sedner的情況底下替換不同的類別 (OCP)。如果當不同類別想要共享Duck Type的行為時應該怎麼辦呢?能否使用繼承的方式呢?還是有另外的方式呢?

這篇文章我們將會介紹RoleModule這兩個概念來回答上述問題,並且透過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
end
class A // class A為了扮演X這個角色,include module X
include X
end
class B // class B為了扮演X這個角色,include module X
include X
end
a = 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的方式來共享行為。

相較於Rubymodule,Java的方式有以下缺點:

  • 需要額外撰寫delegate的程式碼,導致重複程式碼。
  • 需要定義介面與基礎實作。
  • 當介面修改時,每個扮演角色的類別,皆需要修改。

結論:

  • Role的定義在於行為,只要物件能接收特定訊息,便能扮演特定角色,與實作類別無關。
  • 繼承能夠讓相似的類別共享行為。
  • 繼承會導致不相干類別互相耦合,增加複雜度。
  • Ruby中,為了讓不同物件扮演角色並共享行為,能夠使用module
  • Java中,為了讓不同物件扮演角色並共享行為,只能使用interface inheritance and object composition。

--

--

Z-xuan Hong
Z-xuan Hong

Written by Z-xuan Hong

熱愛軟體開發、物件導向、自動化測試、框架設計,擅長透過軟工幫助企業達到成本最小化、價值最大化。單元測試企業內訓、導入請私訊信箱:t107598042@ntut.org.tw。

No responses yet