Try   HackMD

なぜ継承を使いたくなるのか?

結論、とても手軽だから。

継承を使う動機になりそうなこと

「継承はダメだ」と唱え続けても意味があんまりないなーと思ったので、改めて継承を使いたくなるときを考えてみる話。

基底クラスにそこに別の機能を追加するみたいなシーンという前提。

プログラマの気持ちを想像してみよう。インセンティブから理解していこう。

1. 作業量を減らしたい

ちょっと継承して、差分だけ書けば良い。それだけ。やることは少ない。

圧倒的な作業量の少なさ。この誘引力はものすごい。

むしろ、眼の前の問題を解けるソリューションをすぐに見抜けた的な気持ちが、継承の利用を後押しするかもしれない。その瞬間は誇らしくなるのかもしれない。「言語の仕組みにあるわけだろう? じゃあそれを使ってダメなわけないだろう?」

その場の仕事を終わらせたい、終わらせないければいけないというプレッシャーが存在する環境だったら継承を使うインセンティブはものすごく高まる。

ここは本当に馬鹿にできないところだと思う。プログラミング言語というものが未成熟なのが悪いとさえ思う。(「差分プログラミングのためだけに継承するな!」という主張は賛成)

2. 型の関係性(型の階層)を表現したい

型の意味的な関係をコードで表現を表現したいとき。「シベリアンハスキーはDogだ」みたいな。さすがにそんな実装は稀だと思う。実際は、型の恩恵を受けたいから。コンパイルエラーになるとかね。Lintでもよい。とにかくなるべく早くマズイことを検知したいってこと。

共通するダメポイント

結局、結合しまくる。

1のような差分を継承で攻略する場合、影響範囲はものすごく広がる。つらい。

2も型の階層をどんどん作っていったら、認知負荷が高まってしまう。つらい。

どちらもよくある話だと思う。特に1の差分の話はよくある。飽きた。

もっと良いソリューションがあるって話

それが、インタフェースだとか委譲だとかって話。(あんまりわかってないけど、mixinとtraitもたぶん、そう?)

やりたいことは、各モジュールを交換可能にしたいって話。ポリモフィズムの実現。

インタフェース

// 『達人プログラマー』p.206

public class Car implements Drivable, Locatable {
    // ...
}

public interface Drivable {
    double getSpeed();
    void   stop()
}
public interface Locatable {
    Coordinate getLocation();
    boolean    locationIsValid();
}

委譲

継承元にある2つのメソッドだけ欲しいときにどうするか?

継承すれば間違いなくその2つのメソッドは利用できる。

でも、継承元に100個メソッドがあったら、もれなく不要な98個も一緒についてくるね!

# 『達人プログラマー』p.208

# 委譲すれば、不要なメソッドを知らなくてもOK!(余計な98個はいらんのじゃ)
class Account
    def initialize(...)
        @repo = Persister.for(self)
    end
    
    def save
        @repo.save()
    end
end

比喩表現や言葉の曖昧さも原因だと思う。語彙を正確に。

ところで、委譲の理解のしづらさは語彙不足もあるかもしれないことに気づいた。

1. 日本語にすると区別が無理

「BinaryExprはNodeだよね」という表現は、is-a(継承)か、has-a(委譲)かは区別できない。

かといって、「BinaryExprはNodeインタフェースを満足しているよね」と言うのは長すぎるので面倒に感じる。面倒だから省略したくなるのは自然。

でも、そうするとコミュニケーションで事故る。

Aさん 「XがYとして振る舞うように実装してね(具象クラスXは、インタフェースYを満たすようにね)」
Bさん 「はい。XがYとして振る舞うように実装すれば良いんですね。(よし、XはYを継承すれば良いんだな)」

語彙は正確に。

2. コンポジションの「所有」っぽい感覚とか、委譲の「依頼する」感覚をイメージするのは難しい

ここに、身の回りの日常生活のイメージを持ち込むと逆に混乱する。慣れるまでは比喩表現を使うべきでないと思う。

日本語として「所有する」とか「依頼する」という語彙を使うときは、だいたい人間であることが前提。でも、プログラミングで登場するものはだいたい無生物。

分からない状態においてイメージだけで理解しようとする(させようとする)のはダメだよ。

イメージが分からないなら、イメージが分かるまでプログラミングで示すべきだと思う。

語彙は正確に。

関連する話

例: Goは型を階層的に表現できない設計(そもそも継承が言語の仕組みに存在しない)

それでも、十分に型の関係性を表現できているっていうか、型の恩恵を受けることができている。ってか、全然困ってない。

// https://pkg.go.dev/go/ast // インタフェース type Node interface { Pos() token.Pos End() token.Pos } type Expr interface { Node exprNode() } type Stmt interface { Node stmtNode() } // AST上の二項演算式(BinaryExpression)を表現した構造体 type BinaryExpr struct { X Expr // left operand OpPos token.Pos // position of Op Op token.Token // operator Y Expr // right operand } func (x *BinaryExpr) End() token.Pos func (x *BinaryExpr) Pos() token.Pos // 型の恩恵を受けるためだけの Exprインタフェース の実装(中身は空っぽ) func (*BinaryExpr) exprNode() {} // AST上のif文を表現する構造体 IfStmt struct { If token.Pos // position of "if" keyword Init Stmt // initialization statement; or nil Cond Expr // condition Body *BlockStmt Else Stmt // else branch; or nil } func (s *IfStmt) Pos() token.Pos { return s.If } func (s *IfStmt) End() token.Pos { if s.Else != nil { return s.Else.End() } return s.Body.End() } // 型の恩恵を受けるためだけの Stmtインタフェース の実装(中身は空っぽ) func (*IfStmt) stmtNode() {}

以上(いじょう)。

https://twitter.com/mattn_jp/status/1594354661129347078

いただいた意見

1. 入門書に記載されていないから

継承は入門書で学ぶけど、移譲は学ばないからでは?

なるほど。広まってない、勉強していない(機会が少ない)というのは確かに。初学者のみに限定すれば成り立ちそう。

しかし、「入門書に記載があれば、その機能を使いこなせる」はちょっとストレートすぎると思う。初学者でない人も(良くない)継承を使っていたり、使いたくなるシーンはあると思うし、委譲を知っていたとしても意図的に継承を使うケースもありえそうだから。

いくつか分解できそうだ。

  • そもそも知らない
  • 知っていたとしても使うのが難しい、適用すべき箇所が見抜けない
  • 継承なり委譲を勘違いしている
  • 継承のマイナス面を理解するのが難しい?
  • などなどありそう?