Overview
“Prefer composition over inheritance” is familiar advice, but it is a little vague. This video tests it on the code of an image library. Inheritance and composition are tools for the same goal: reusing code you have already written.
The video shows where inheritance breaks down, what the same design looks like with composition, and when inheritance is still worth using.
Key Points
- An Image class with resize and flip, plus abstract save and load methods that JpgImage, PngImage and BmpImage implement, works until users want an image they can draw on that does not come from a file. DrawableImage is forced to implement meaningless save and load, and inserting a new FileImage parent breaks existing code.
- With composition, Image only represents an image in memory, the file-format classes take an image as an argument, and a new ImageDraw class can be added without touching anything else.
- Inheritance bundles two abilities: code reuse and abstraction. An ImageFile interface provides the abstraction as a contract only, and passing that interface into ImageApp from outside is dependency injection.
- Composition has costs, such as boilerplate and wrapper methods, but it keeps the contact surface between objects small, so changes hurt less.
- Inheritance helps when you need a quick fix in a large system full of duplicated code. If you use it, design for it: don’t expose fields as protected, offer an explicit protected API for overriding, and make everything else private, final or sealed.
Chapters
- 0:00 Introduction
- 0:29 The pitfalls of inheritance
- 3:06 Composition
- 4:40 Abstraction through inheritance
- 5:51 Interfaces and dependency injection
- 7:06 When to use inheritance
- 8:20 Summary
概要
「継承よりコンポジションを選べ」はよく聞く格言ですが、少し曖昧な助言でもあります。この動画では、画像ライブラリのコードを使ってこの助言を確かめます。継承もコンポジションも、書いたコードを再利用するという同じ目的のための道具です。
継承がどこで破綻するのか、同じ設計をコンポジションで書くとどうなるのか、そして継承が今でも役に立つのはどんなときかを解説します。
ポイント
- リサイズと反転を持ち、保存と読み込みを抽象メソッドにして JpgImage・PngImage・BmpImage に実装させる Image クラスは、ファイルに由来しない、ユーザーが絵を描ける画像が必要になるまではうまく動きます。DrawableImage は意味のない save と load の実装を強いられ、新しい親 FileImage を間に挟むと既存のコードが壊れます。
- コンポジションでは、Image はメモリ上の画像だけを表し、ファイル形式のクラスは画像を引数として受け取ります。新しい ImageDraw クラスも、他のクラスに一切手を加えずに追加できます。
- 継承は、コードを再利用する能力と抽象化を作る能力の2つをまとめたものです。ImageFile インターフェースは抽象化を契約だけとして提供し、そのインターフェースを ImageApp の外から渡すのが依存性の注入です。
- コンポジションにも、定型コードやラッパーメソッドが増えるといった欠点があります。それでもオブジェクト同士の接触面が小さくなるため、変更したときの痛みが減ります。
- 継承は、重複だらけの巨大なシステムをすぐに1か所だけ直したいときには有効です。使うなら継承される前提で設計し、変数を安易に protected にせず、上書き用には明示的な protected の API だけを用意して、それ以外は private、final、sealed にします。