角色介面

2006 年 12 月 22 日

角色介面是透過檢視供應者與消費者之間的特定互動來定義的。供應者元件通常會實作多個角色介面,每個介面對應一種互動模式。這與HeaderInterface形成對比,其中供應者只會有一個介面。

我們來看一個範例。考慮一個用於PERT風格專案規劃的程式。在這個架構中,我們將專案分解成一組活動。然後我們將這些活動排列成一個網路(嚴格來說是一個有向非循環圖),以顯示任務之間的相依性。因此,如果「吃早餐」是一個任務,那麼「煮咖啡」和「沖麥片」可能是前置活動。這表示我必須等到所有前置活動都完成後才能開始吃早餐。

每個活動都有持續時間,也就是我們預期的執行時間。有了持續時間,再加上網路中的關係,我們就可以找出其他資訊。我們可以計算活動的最早開始時間,作為其前置活動的最晚最早完成時間。我們計算活動的最早完成時間,作為其最早開始時間加上其持續時間。我們也可以計算出最晚完成時間和最晚開始時間。程式碼看起來會像這樣。

  private int duration;

  public MfDate earliestStart() {
    MfDate result = MfDate.PAST;
    for (?TYPE? p : predecessors())
      if (p.earliestFinish().after(result))
        result = p.earliestFinish();
    return result;
  }

  public MfDate earliestFinish() {
    return earliestStart().addDays(duration);
  }

   public MfDate latestFinish() {
    MfDate result = MfDate.FUTURE;
    for (?TYPE? s : successors())
      if (s.latestStart().before(result))
        result = s.latestStart();
    return result;
  }

  public MfDate latestStart() {
    return latestFinish().minusDays(duration);
  }

你會注意到上述程式碼中有一個漏洞,也就是?TYPE?。如果我們詢問活動的前置活動和後續活動,我們應該預期會得到哪種類型的物件?(精確來說,我們預期會傳回一個集合,因此真正的問題是傳回集合的元素類型應該是甚麼?)

如果你使用標頭介面,傳回的介面會是一個活動,並且會反映我們活動類別的公開方法,以建立InterfaceImplementationPair

public interface Activity ...
  MfDate earliestStart();
  MfDate earliestFinish();
  MfDate latestFinish();
  MfDate latestStart();

class ActivityImpl...
  List<Activity> predecessors() ...
  List<Activity> successors() ...

然而,透過角色介面,我們會檢視協作物件實際的用途。在此情況下,繼承者僅用於其 latestStart,而前身僅用於其 earliestFinish。因此,我們會建立兩個僅包含我們實際使用的函式的介面。

public interface Successor {
  MfDate latestStart();
}
public interface Predecessor {
  MfDate earliestFinish();
}

class Activity
  List<Predecessor> predecessors() ...
  List<Successor> successors() ...

我們可以將繼承者視為協作物件相對於此物件所扮演的角色。這種思考物件及其在與他人協作中所扮演角色的方法在物件導向的世界中已行之有年。

角色介面的優點在於,它清楚傳達活動與其繼承者之間的實際協作。通常,類別並不會使用所有類別的函式,因此顯示實際需要的函式非常重要。如果您稍後需要替換它,這項功能會特別有用。標頭介面會強制您實作每個函式,即使您不需要這些函式;但使用角色介面,您只需要實作真正需要的部分。

角色介面的缺點是,由於您需要檢視每個協作才能形成角色介面,因此需要花費更多心力。使用標頭介面,您只需要複製公開函式,無需思考。這也會讓您對消費者產生依賴感。我說「依賴感」,是因為這並非正式的依賴關係,但仍足以讓許多人感到不安。他們偏好標頭介面,因為他們認為您不應該在意誰使用您的服務,或如何使用。您會發布介面,而他們可以在覺得有用的時候使用它。

總的來說,我比較喜歡角色介面,因此我建議您盡可能朝這個方向努力。這需要花費一些功夫,但我一直相信,您應該只在真正需要可替換性時才使用介面,如果您確實需要介面,您應該仔細思考該介面的消費者需要什麼。

如果您在使用類似於網路服務的遠端呼叫背景下思考這一點,會出現一個有趣的轉折。如果我們要向遠端服務詢問前身詳細資料,我們應該期待什麼回饋?有些人可能會認為,要成為角色介面,它應該只傳回包含最早完成資料的文件。我不同意 - 我認為傳回包含比我要求更多資料的文件完全合理。重點是,任何涉及的類型檢查都應該只檢查最早完成資料是否已存在。如果可以忽略額外資料,提供它並非罪過;就像類別可以實作多個介面一樣。這種思考方式符合 消費者驅動合約 的哲學,這是我認為消費者驅動合約如此引人入勝的原因之一。

正如我所指出的,這個概念已經存在很長一段時間。Trygve Reenskaug 寫了一本 方法論書籍,內容圍繞著分析角色並將其綜合到類別中。Robert Martin 將這個主題稱為 介面隔離原則:角色介面遵循該原則,但標頭介面則不遵循。