👾

継承にいたる病

2023/02/14に公開

継承の話

みんな大好き「継承」の話です。
この記事での「継承」には、クラスの継承だけでなく、 interface などによる共通化も含みます。

最終的に何が言いたいかというと、「継承とか共通化はほどほどにしようね」「データ設計はちゃんとしようね」という話です。

継承にいたる病

なんか(厨二病的な)かっこいいこと言いたくて、このようなタイトルをつけました。
ニュアンスは伝わるかと思いますが、この病に罹患すると、様々なものに「 is-a の関係 」を見出してしまいます。
大変恐ろしい病です。

以下のような組織の評価制度をシステム化することになりました。
評価は個人の業績や勤務態度、人望などの他、部署全体を通した活躍なども含めて評価されます。また、この組織では部署ごとに評価制度が異なり、部長とその他の一般社員でも評価制度が異なります。
どのように設計したら良いでしょうか。

〇〇株式会社
├───営業部
│   ├───山田(部長)
│   ├───高橋
│   └───田中
├───総務部
│   ├───佐藤(部長)
│   ├───中村
│   └───小山
├───製造部
│   └ ...
:

設計のはじめに

どう考えてもこんな設計をする奴はいないだろう、という形で話を進めますが、本記事のタイトルを思い出していただき、どうか読み進めていただきたく思います。

組織図からわかることを挙げてみましょう。

  • 〇〇株式会社は複数の「部署」がある
  • 各「部署」には複数の「社員」がいる
  • 「社員」には「業績」「勤務態度」「人望」などの「評価」基準がある
  • 「部署」全体の「活躍」も「評価」対象になる
  • 部署ごとに「評価」制度が異なる
  • 「部長」と「一般社員」で「評価」制度が異なる
  • 「山田」「高橋」「田中」は「営業部」の「社員」である
  • 「佐藤」「中村」「小山」は「総務部」の「社員」である

少なくともこのようなことがわかります。

「 is-a の関係 」の発見

設計が得意な読者の皆様は、継承に必要な条件の「 is-a の関係 」をすでに見出しているかと思います。

  • 「山田」は「営業部」の「社員」である

これは間違いありません。「 is-a の関係 」です。
しかし、少し修飾が邪魔なような気がします。
よく確認しましょう。

  • 「山田」は「社員」である → 〇
  • 「山田」は「営業部」である → △ 🤔

「山田」が「社員」だということは間違いないですが、「山田」は人間なので「営業部」かと言われると疑問が残ります。ここを解決せずに良い設計はできません。
次のように考えてみてはどうでしょうか。

  • 「山田」は「社員」である → 〇
  • 「山田」は「営業部の社員」である → 〇
  • 「営業部の社員」は「社員」である → 〇

なんの疑問も残らない関係を見出すことができました。
早速クラスで表現してみましょう。

// 社員インターフェース
interface Empolyee { ... }

// 営業部の社員インターフェース
interface SalesDepartmentEmpolyee: Empolyee { ... }

// 山田クラス
class Yamada: SalesDepartmentEmpolyee { ... }
// 高橋クラス
class Takahashi: SalesDepartmentEmpolyee { ... }
// 田中クラス
class Tanaka: SalesDepartmentEmpolyee { ... }

部長とその他社員

前述の形で Yamada Takahashi Tanaka を実装していくと、どうも TakahashiTanaka の実装が似通っていることに気づきました。
「山田」は「部長」ですが、「高橋」「田中」は「一般社員」でしたね。
ここにも「 is-a の関係 」の匂いがします。

  • 「営業部部長」は「営業部の社員」である
  • 「営業部一般社員」は「営業部の社員」である

この関係性を見つけたのなら、 TakahashiTanaka のふるまいを共通化できるのではないでしょうか。

// 営業部の社員インターフェース
interface SalesDepartmentEmpolyee: Empolyee { ... }

// 営業部部長クラス
open class SalesDepartmentManager: SalesDepartmentEmpolyee { ... }
// 営業部一般社員クラス
open class SalesDepartmentMember: SalesDepartmentEmpolyee { ... }

// 山田クラス
class Yamada: SalesDepartmentManager { ... }
// 高橋クラス
class Takahashi: SalesDepartmentMember { ... }
// 田中クラス
class Tanaka: SalesDepartmentMember { ... }

これで「一般社員」の「高橋」と「田中」は SalesDepartmentMember として共通化することができました。「 DRY 原則 」に基づく設計です。
TakahashiTanaka で評価基準となる値さえ変えてしまえば、その後の評価結果は共通のメソッドとできるようになりました。
素晴らしい設計ですね。

この設計の欠点

賢明な読者の皆様は、「こんな設計するわけない」と思いながら読まれているかと思います…。ここからは、この設計の欠点を挙げていきたいと思います。

一言で言うと、頻繁に変更が要求される こと予想できるにもかかわらず、 致命的に変更に弱い点 が一番の欠点です。つまり、課題を解決できていない設計というわけですね。

評価基準値の変更

実際に評価の時期がやってきました。
各社員の評価項目に値を入力していきます。入力は誰が行えるでしょうか。
「山田」の「業績」や「勤務態度」などは Yamada クラスに実装されていますので、 Yamada クラスを理解している開発者が値を入力する必要があります。
おそらく「人事部」の誰かが各項目の値を決定し、「開発部」の誰かに変更の依頼をすることになるのでしょう。
評価の時期が来るたびに、ソースコードを変更し、ビルドをし直す必要がありますが、最近はマシンスペックも向上してますので、リビルドくらい屁でもないでしょう(棒)。

名前の変更

なんと「高橋」が結婚して名字が変わることになりました。おめでたいことです。
Takahashi クラスのままではわからなくなってしまうので、ソースコード内のクラス名も現実に合わせてリネームしましょう。
Takahashi クラスがどの程度利用されていたかはわかりませんが、最近は IDE も優秀ですので、リネームくらい屁でもないですね(棒)。

名前の衝突

なんと「高橋」の結婚後の姓は「佐藤」だということがわかりました。
「総務部」にすでに「佐藤」がいたため、 Sato クラスはすでに存在します。
仕方ないので Sato2 クラスとすることにしましょう。
社員のフルネームを使えば衝突可能性も減らせるので、今後のリファクタリングで提案するのも良いかもしれませんね(棒)。

部署異動

期の途中であるにも関わらず、「総務部」の「小山」が「営業部」に異動になりました。
現実の通りにクラスとして表せば良いので簡単ですね。

class Koyama: AdministrationDepartmentMember { ... }
// ↓
class Koyama: SalesDepartmentMember { ... }

部署異動にも柔軟に対応できる設計ができていたと感じたのもつかの間、なんと「総務部」にいた期間は「総務部」としての評価を、「営業部」に異動になった後の期間は「営業部」としての評価を出してほしいと言われてしまいました。
さすがにそのようなことまでは想定していませんでした。「 YAGNI の精神 」ですよね(違う)。
現状の設計では対応できないので、例外的に Koyama クラスで評価メソッドを override して対応することにしました。来期は override したメソッドを消すだけできちんと「営業部」の評価が出せるので問題ないでしょう(棒)。

共通化の弊害

Koyama クラスでメソッドを例外的に override したことをきっかけに、一部の開発メンバーから「一般社員」として共通化したことが問題だったのではないかと声が上がりました。「小山」と「高橋」「田中」は同じ「営業部」の「一般社員」ではありますが、確かに「高橋」と「田中」は別人です。共通化してはいけない概念を共通化してしまっていたのでしょうか。
プログラミングや設計が少しできるようになったがために、「 DRY 原則 」を誤解していたのかもしれませんね(棒)。

何がダメだったのか

バカバカしい話を続けてきましたが、ここまでお付き合いいただきありがとうございます。
最初の組織図を見たとしても、このような設計をすることはないかと思います。
むやみに「 is-a の関係 」を見出してもロクなことになりません。

データ設計

組織図を組織図のまま見てしまったので余計なことを考えてしまったのです。
社員をクラス化するときによくある形としては以下のような表だと思います。
最初のような組織図を見ても、以下のような表形式で考えた方が良い設計ができるかと思います。

名前 部署 役職 その他の値...
山田 営業部 部長 ...
高橋 営業部 一般 ...
田中 営業部 一般 ...
佐藤 総務部 部長 ...
中村 総務部 一般 ...
小山 総務部 一般 ...
... ... ... ...

この形ならむやみに「 is-a の関係 」を見出すこともないでしょう。
以下のような data class をイメージするのではないでしょうか。

data class Empolyee (
  val name: String,
  val department: Department,
  val position: Position,
  ...
)

情報のライフサイクル

設計は、現実世界を抽象化した形で行われることも多いかと思いますが、現実世界で変化しやすいものまでソースコードに表現してしまうと上記の例のように苦労することになります。
人の名前や、所属部署、評価の値など、変化しやすいものとそうでないものをきちんと見分けて、ソースコードで表現するのか、データとして表現するのかを考えなければなりません。

DRY 原則を見誤る

共通化できる、と思ったことでも、別の概念をモデリングしていた場合は、共通化しない方が良いこともあります。逆に共通化すれば良いのに、別の概念だと思い込んでしまって、共通化できないこともあります。この見誤りも「継承」まわりで起こることが多いように思います。
こればかりはその都度判断するしかありません。見極める目を養っていきましょう。

結論

「継承」はうまく使えば便利なテクニックではありますが、本当に必要なときにだけ使うくらいが良いでしょう。
あれもこれも「 is-a の関係 」を見出しても、何も良いことはありません。

また、データの見方を変えることで、設計方針が大きく変わることもあります。最初に目にした形のまま設計を始めるのではなく、より良いデータの形を見定めてから、設計を行うよう心がけるのが良いでしょう。

バカバカしい内容でしたが、設計に携わる方の何らかの助けになれたら幸いです。

おまけ

上記のような社員の例は極端すぎて、本記事のような設計を考える人はほとんどいないでしょう。しかし、似たような形でも設計を迷う場合があるかと思います。
以下のような体系の魔法が登場するゲームの設計を考えてみましょう。

魔法
├───火属性魔法
│   ├───ファイヤー
│   ├───スーパーファイヤー
│   └───ハイパーファイヤー
├───水属性魔法
│   ├───ウォーター
│   ├───スーパーウォーター
│   └───ハイパーウォーター
├───雷属性魔法
│   └ ...
:

人によって答えは違うと思います。設計は難しいですが、楽しいですね。

Discussion