iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 25
1
Modern Web

I Want To Know React系列 第 25

I Want To Know React - Composition vs Inheritance

當初學者剛接觸 React 時,常常會想要以 inheritance(繼承)的方式來實作 React component。然而 React 官方建議實作 component 時,只應該使用 composition(組合),而不應該使用 inheritance(繼承)。

在這個篇章中,我們將簡介何為 composition 與 inheritance,並說明為何 React 官方建議使用 composition 取代 inheritance。最後將介紹一些使用 composition 取代 inheritance 的技巧。

React Composition or Inheritance

在開始比較兩個方法前,先來簡介一下 composition 與 Inheritance 為何。

何謂 composition(組合)

Composition 是組合的意思。如果一個 class B 中有使用到另一個 class A 創建出來的 instance 的話,就是使用了組合的概念。這時候可以說使用 class A 組合出了 class B 的一部分。

更白話的講,composition 代表著 has a(擁有) 的概念。舉例來說,車子擁有引擎,這就是一個經典的 composition 範例。

Compoistion 各種程式語言中都十分常用的一個技巧。當我們試圖把一個複雜的 class 分割成許多小的區塊時就會常常會使用到 composition 的概念。

何謂 inheritance(繼承)

Inheritance 是繼承的意思。如果 class B 是基於 class A 衍伸出來(擁有全部 class A 的特性、功能)的話,就是使用了繼承的概念。這時候可以說 class B 繼承了 class A,而 class A 就會被稱為 base class(基礎類別),class B 會被稱為 derived class(衍伸類別)。

更白話的講,inheritance 也代表著 is a(是) 的概念。舉例來說,車子交通工具,這就是一個經典的 inheritance 範例。

Compoistion 在物件導向(Object Oriented)的語言中十分常見。Inheritance 能夠很好的描繪真實世界的情境,像是貓一種動物,手機是一部機器等,都是很好描述現實狀況的方式。

除此之外,Inheritance 也可以做到依賴反轉的功能,且可以衍伸出多型等強大的樣式供開發者使用。

Inheritance 的缺點

然而,撰寫 Inheritance 的時候常常會遇到幾個問題。

Class 可以想成是一組功能 / 介面的綜合體。當我們使用繼承時,就代表 derived class 必須要完全繼承 base class 的所有功能。當專案剛起步,繼承會看似是一個好選擇,然而隨著需求的演進,會很難保證之後的 derived class 真的會完全符合 base class 的所有介面。如果這時候發現 base class 不夠基本,不符合新的 derived class 設計的話,則會出現兩個解法:

  • 修改 base class,其他對應的 derived class 也需要一起修改
  • 做出不符合 base class 的 derived class,導致怪異的繼承結構

而這兩種方法都不是我們樂見的。

舉例來說,如果要實作一個聊天室系統。目前已知的功能會有單人聊天室與多人聊天室。

一種可行的繼承架構會是設計一個聊天室 base class,此 class 有以下功能:

  • 傳訊息
  • 收訊息

而單人 / 多人聊天室皆為 derived class,分別繼承聊天室 base class,並實作自己的特殊行為內容。

然而出現了一個新需求,要做出廣播型聊天室。此聊天室只允許接收訊息,不允許使用者傳送訊息。因此,我們需要修改聊天室 base class 讓其功能變成只有傳訊息而已。而因為 base class 的修改,單人 / 多人聊天室 derived class 實作也會需要一起更新,甚至需要新增一個 "可傳訊息的聊天室 base class ****來符合需求"。這絕不是一個理想的狀態。

簡單來講,實作繼承時,base class 很容易提供過多的介面 / 功能,導致後續 derived class 難以實作。而因為需求千變萬化的原因,在開發初期就預測並識別出 base class "最基本" 的功能是非常困難的。這就是 Inheritance 的缺點。

React Composition 的優點

相較於繼承,composition 的彈性更大,不會被 "is a" 的概念限制,可以只使用自己需要的功能就好,而不必連不需要的功能都一併繼承。

除此之外,React 本身提供了很強的 composition 功能。多個 component 可以很輕易的組合成更複雜的 component。

其中一個讓 React 有強大 compoisition 功能的原因就是 props 的自由度很高。JavaScript 的各種 primitive value、object、function 乃至 React element 都可以被作為 props 傳給下層的 component 作為顯示 / 執行 / 使用之用。

接著,React composition 強大的功能可以把多個任意的 component 組合成一個更複雜的 component。反過來說,也可以很輕易的把一個過於複雜的 component 拆解成較簡單的小 component。因此,藉由善用 composition ,整個 React 專案可以變得更具彈性。這也是為何 React 官方建議使用 composition 而非 inheritance 的原因。

以下我們將繼續介紹一些 React render 畫面時,用 composition 取代 inheritance 的技巧。

React 畫面 composition 技巧

本章節介紹的 composition 技巧主要會有兩個:

  • 包含
  • 特化

首先先介紹包含這個技巧。

包含

包含(Containment) 即代表一個 component 是另一個 component 的外框狀況。

一些骨架或外框型的 component(e.g. LayoutSidebarDialog) 常會有使用包含的需求。

此問題可以使用 composition 來解決。解決的技巧就是:

  • 在外框 component 中提供 children prop,並把傳入的 children React element 當作內容顯示在外框內

舉例來說,現在有個需求想讓不同的對話框外都包裹固定樣子的外框,則如下實作。

首先會有個外框 component FancyBorder,代表我們要的外框樣子:

function FancyBorder(props) {
  return (
    <div className={"FancyBorder FancyBorder-" + props.color}>
      {props.children}
    </div>
  );
}

FancyBorder 內提供了 children 這個 prop,而 FancyBorder 會把這個 prop 當作內容放到外框之內顯示。

接著就可以使用 FancyBorder 外框與任意的 children 組合出其他的 Dialog component:

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">Welcome</h1>
      <p className="Dialog-message">Thank you for visiting our spacecraft!</p>
    </FancyBorder>
  );
}

function GoodbyeDialog() {
  return (
    <FancyBorder color="red">
      <h1 className="Dialog-title">Goodbye</h1>
      <p className="Dialog-message">See you next time in our spacecraft!</p>
    </FancyBorder>
  );
}

讀者也可以參考 CodePen 詳細範例。

包含多個 component

在一些比較特殊的狀況下,外框 component 會需要挖多個 "洞" 來支援放入複數的 component。

這個狀況同樣可以使用 composition 可以解決,其解法概念與單一 containment 相似:

  • 在外框 component 中提供多個 prop,並把傳入的 React element prop 當作內容顯示在外框內對應的地方中

舉例來說,如果要做一個應用程式的骨架。此骨架會包含 Header、Sidebar、Content 這三個部分,其中這三個部分的內容是可以被任意客製化的,則可以這樣實作:

function Layout(props) {
  return (
    <div className="layout">
      <div className="layout-header">{props.header}</div>
      <div className="layout-sidebar">{props.sidebar}</div>
      <div className="layout-content">{props.content}</div>
    </div>
  );
}

function App() {
  return (
    <Layout header={<Header />} sidebar={<Sidebar />} content={<Content />} />
  );
}

詳細範例也可以到 CodePen 中查看。

範例做出了一個 Layout component 作為外框,並接受 headersidebarcontent 這三個 React element prop。這些 prop 會分別放進外框的上、左、右部分。

這時候,外部就能夠將任意的 component 塞入headersidebarcontent prop 中來組合出一個完整的應用程式了。

特化

另外一個會讓初學者想使用繼承的情況是特化

特化(Specialization)代表一個 component 是另一個通用 component 的特殊型態的狀況。

此情形也可以使用 composition 解決。解決的技巧就是:

  • 讓特殊型態的 component render 出帶有對應 props 的通用 component

接續修改剛剛的 Dialog 範例。現在改成先製作一個通用的 Dialog component,並把 WelcomeDialogGoodbyeDialog 分別當作 Dialog 的特話 component 來使用,則可以這樣實作。

Dialog component 的內容如下,內容包含了可客製化的 titlemessage

function Dialog(props) {
  return (
    <FancyBorder color={props.color}>
      <h1 className="Dialog-title">{props.title}</h1>
      <p className="Dialog-message">{props.message}</p>
    </FancyBorder>
  );
}

接著,只要分別傳入客製化的 titlemessage props 就可以使用 Dialog 來組合出WelcomeDialogGoodbyeDialog 了:

function WelcomeDialog() {
  return (
    <Dialog
      color="blue"
      title="Welcome"
      message="Thank you for visiting our spacecraft!"
    />
  );
}

function GoodbyeDialog() {
  return (
    <Dialog
      color="red"
      title="Goodbye"
      message="See you next time in our spacecraft!"
    />
  );
}

這樣就完成特化的目的了。讀者也可以到 CodePen 上參考範例。

React 邏輯 Composition 技巧

以上的段落主要針對在 UI 層面上使用 composition 取代 inheritance 的方式做說明。

但在很多時候,我們想要使用繼承來解決的問題是邏輯(而非純 UI 顯示)的複用。

在這個狀況下則可以考慮採取以下幾種策略:

其中,HOCHooks 的部分將在之後的章節中進一步介紹。

小結

在本章節中,我們介紹了 Composition 與 Inheritance

  • Composition:組合
  • Inheritance:繼承

而 React 官方推薦 component 使用 composition,並盡量避免 Inheritance。

這是因為 React component 提供了許多強大的 composition 功能,像是 props、HOC、Hooks 等等。

另外,我們也說明了一些 UI 上可以使用 composition 取代 inheritance 的一些技巧,包括:

  • 包含
  • 特化

相信透過本章節,讀者可以更了解 React composition 的使用方式。

參考資料


上一篇
I Want To Know React - 提升 state 練習
下一篇
I Want To Know React - 初探 Context
系列文
I Want To Know React30

尚未有邦友留言

立即登入留言