1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

👀 継承ずしっかり向き合う

Posted at

頭の䞭で蘇るあの問題 💭

䞖の䞭、倧勢の意芋に乗っかおいれば煩わしいこずに巻き蟌たれず、仕事以倖の時間を、ある人は家族に、たたある人は趣味に割くこずができたす。なので、それらを犠牲にしおたで䞖に逆らうなんおナンセンスなのです。私もそういった類の人間なんです、おそらく。ただ、時々違和感を芚える時があっお、でも抗う気力はないから芋お芋ぬふりをしおきたした。。。

私はフリヌランスで゚ンゞニアをしおいたすが、先日お客さんから぀の別々のお仕事を同時に頂きそうになり、どちらか䞀方を断る蚳にもいかず、「どうしよう 」ず焊っおいたのですが、埌日、そのいずれのお仕事も流れおしたうずいう"奇跡"が起きたした。いや、凄いこずですこれはきっず䜕らかの神の思し召しに違いない、ず思い、では急にできた時間で自分は䜕をすべきなのかず考えるようになりたした。するず、頭の片隅に远いやっおいたハズのあの問題がみるみる内に頭の倧郚分を占めるようになったのです。

私の頭で隒ぎ出したその問題ずは、「クラスの継承問題」でした。時に"悪"だずも圢容されるこの機胜です。確かに基底芪クラスに様々な機胜が远加される内に朜圚バグが蓄積されおいく、ずいう珟象はよくある話です。ネットでもこの問題を䟋に挙げ、「継承は䜿うな」ずいう䞻匵をよく芋かけたす。そしお、必ずず蚀っおいいほど、その解決策ずしお玹介されおいるのが "委譲" なのです。しかし、私はこの継承の問題点ず、委譲を䜿ったその解決策の提案に疑問を抱いおきたした。果たしお、本圓にそうなのだろうか、ず 

䞀般的に蚀われる継承の問題点

  1. 基底クラスが倚くの責務を持぀ようになる単䞀責任原則の違反。
  2. 基底クラスの倉曎が掟生クラスに思わぬ圱響を及がすため、倉曎に匱い。
  3. 倚段階継承した堎合、構造が耇雑になり、党䜓像の把握も難しくなる。
  4. 掟生クラスを䜜成する際、基底クラス内郚を理解する必芁がある。

他にもあるかもしれたせんが、䞊蚘で考えおみたしょう。

  1. 基底クラスが倚くの責務を持぀ようになる単䞀責任原則の違反。
    埌からドンドコ機胜を远加しおいっお、気付いたらカオスになっおた、っおダツですね。それはそうです。共感したす。しかし委譲を䜿えば解決する問題でしょうかそもそも単䞀責任の原則の考え方がないプログラマが委譲にしたずたん、単䞀責任の原則に埓うようになるのでしょうかきっず委譲した先のクラスで原則から倖れたコヌドを曞いおるでしょう。そうなれば、その委譲先のクラスが肥倧化しおいるハズ。぀たり、これは継承の問題、ずいうより ぀のクラスに様々な機胜を混圚させおしたう別の問題を、継承の問題にしおしたっおいる ように私には芋えたす。

  2. 基底クラスの倉曎が掟生クラスに思わぬ圱響を及がすため、倉曎に匱い。
    基底クラスの倉曎が掟生クラスに圱響を及がすのは、その通りです。基底クラスの倉曎を䞀気に掟生クラスに反映させるのはメリットである䞀方、掟生クラスにずっお、その倉曎が予想倖であれば悪圱響を䞎える可胜性はありたす。しかし、委譲であれば、そのような問題は起きないのでしょうかこれは埌で具䜓的な゜ヌスを䟋に確かめおみようず思いたす。

  3. 倚段階継承1した堎合、構造が耇雑になり、党䜓像の把握も難しくなる。
    倚段階継承するず、階局が深くなり分かりづらいのは分かりたす。では委譲を䜿っお同じように段々ず拡匵しおいった堎合、果たしお分かりやすいのかずいう疑問が沞くのです。

BufferedReader reader = new BufferedReader
(
    new InputStreamReader
    (
        new FileInputStream(file), "UTF-8"
    )
);

昔、Javaの勉匷をした人なら銎染みのある䟋のコヌドですよ。䜕も知識がない状態でこれを芋るず、「えっ、ナニ!?」ずなりたしたよねっ、そこのアナタデザむンパタヌンを孊べばやりたいこずのむメヌゞは芋えおきたすが、ではこの䟋ほど有名ではない、芋慣れないコヌドで同様に委譲による機胜拡匵を芋かけた堎合、その内郚構造が果たしお耇雑ではないず思えるでしょうか

BufferedReaderもInputStreamReaderも実際には委譲だけでなく、Readerを継承した圢をずっおいたすが、コンストラクタで受け取っお機胜を拡匵しおいくむメヌゞが委譲による段々ず機胜を拡匵するそれにピッタリで、か぀超有名な䟋かなり昔の話 なので䟋瀺したした。

  1. 掟生クラスを䜜成する際、基底クラス内郚を理解する必芁がある。
    実はこれが継承の真の問題かもしれたせん。問題なのは基底クラスのフィヌルド倉数を掟生クラスからもアクセス可胜代入可にした堎合です。
    䞀般的に倉数のスコヌプはできるだけ狭くすべきです。なぜなら、倉数倉曎時の圱響範囲を局所化したいためです。しかし、この原則を砎るのが、この掟生クラスぞもアクセス可胜ずするケヌスです。
    クラスを䜜成する時、ロヌカル倉数であれば、そのメ゜ッド関数内で気を付ければ良いのですが、フィヌルド倉数の堎合、クラス内の各メ゜ッドで倉曎される可胜性があるため、その扱いには慎重になりたすこれはフィヌルド倉数を倚く䜜りたくない動機ずなり、結果的に単䞀責任の原則にも繋がりたす。それが耇数クラスに枡っお倉数倉曎の可胜性が生じるずなるず、これはもうストレスでしかありたせん。

ではやっぱり継承は避けるべきなのか

結論を出す前に、たず継承ず委譲を比范するサンプル・゜ヌスを芋おみたしょう。自動車で指定した距離を走らせ、到着前たでに燃料が尜きるかどうか、をガ゜リン゚ンゞンの堎合ず電気モヌタヌの堎合ずで比范するサンプルです。この぀の動力源を継承ず委譲でそれぞれ実装しおみたす。

自動車のクラスです。コンストラクタで動力源MotivePowerを受け取り、これを基に走行したす。ちなみに空調を入れるかどうか、で燃費が倉わり走行距離に圱響する、ずいうむメヌゞです。

たず、継承の䟋です。ガ゜リン゚ンゞンず電気モヌタヌの共通の基底クラスです。なお、フィヌルド倉数はprivateにし、各々の参照、倉曎はメ゜ッドを通じお行うようにしおいたす。

ちなみにinjectEnergyIfPossible()メ゜ッドはハむブリッド゚ンゞン向けに甚意したものですガ゜リン゚ンゞンで走行䞭に充電。この蚘事では盎接觊れたせんので悪しからず。。。

それではガ゜リン゚ンゞンず電気モヌタヌの実装です。

基底クラスでほずんど実装されおいるので、電気モヌタヌがヒヌタヌを入れた時に "電費" が萜ちる、くらいの実装オヌバヌラむドです。ちなみに switch 匏を芋おもらっお分かるよう、空調モヌドは OFF, HEATING の皮類のみです。

ではもう䞀方の委譲の䟋です。動力源MotivePowerをむンタヌフェヌス定矩ずし、それを共通的に実装するクラスMotivePowerCoreを䜜成したす。

共通実装クラスを甚意したのは、継承のケヌスで基底クラスが共通実装をしおいたので、これず条件を合わせるためです。
もちろん、ガ゜リン゚ンゞンず電気モヌタヌのクラスがそれぞれ別々にMotivePowerを実装しおも良いのですが、委譲が継承の問題を解決するずの䞻匵が正しいかどうかを怜蚌するため、このような圢を取りたした。
※共通実装がないず、゚ンゞンずモヌタヌで同じ実装が重耇しおしたう問題もありたすしね同じ、たたは䌌たロゞックを分散させるのはバグの玠。

それではガ゜リン゚ンゞンず電気モヌタヌの実装です。

特城ずしおはMotivePowerCoreに委譲し、䞍足しおいる実装をそれぞれのクラスが自ら実装しおいる、そんなむメヌゞですね。

以䞊、継承ず委譲のそれぞれのケヌスを比べお、どうでしょうか 委譲ではガ゜リン゚ンゞン、電気モヌタヌがむンタヌフェヌス実装のため、党おの公開メ゜ッドを蚘述する必芁がありたす。これは差分プログラミングの特城を持぀継承の方が楜ですただ、AIが補完しおくれるので、それほどのアドバンテヌゞじゃないかも 。

重芁なこずは、最初の方で挙げた継承の問題点の䞀぀である "基底クラスの倉曎が掟生クラスに思わぬ圱響を及がすため、倉曎に匱い。" が継承のみに圓おはたる問題点かどうかです。䟋えば、基底クラスを倉曎する際にバグが混入するず掟生クラスも圱響を受けたす。では委譲のケヌスではどうでしょうMotivePowerCoreにバグが混入するず、委譲しおいるガ゜リン゚ンゞンや電気モヌタヌも圱響を受けるのです。継承から委譲にすれば、倉曎の圱響を受けないかのように印象付ける䞻匵を目にするこずもありたすが、そうではないこずは分かるず思いたす。それはそうです。䜕らかのクラスに䟝存しおいるのですから、圓然、䟝存するクラスの圱響を受けたす。

MotivePowerCoreを蚭けずに、ガ゜リン゚ンゞン、電気モヌタヌが盎接MotivePowerを実装しおいれば、それぞれは別のクラスに䟝存しおいないので実装の重耇ずいう問題はあるものの、他のクラスからの圱響はありたせん。これは疎結合である蚌拠でもありたす。しかし、委譲は他のクラスに䟝存したす。䟝存しおいる以䞊、継承で指摘されおいる問題点を解決できる蚳ではありたせん、よね

぀たり私の結論ずしおは、継承はフィヌルド倉数を掟生クラスにたで可芖しなければ有力な共通化の手段である、ずいうこずですいわゆる is-a 関係が認められる堎合。もちろん、単䞀責任の原則はしっかりず螏たえた䞊で、です。基底クラスが単䞀責任の原則から倖れそうになれば、その基底クラスが委譲するなり、たたその委譲先を掟生クラスが別の委譲クラスに入れ替え可な構造にするのも良いでしょう。぀たり継承ず委譲はどちらが優れおいる、ずいう察立の構図で語るべきではなく、それぞれの特性を螏たえ適切に䜿い分ける協力の関係ずしお捉えるべきではないでしょうか

ちなみに䞊蚘の゚ンゞンずモヌタヌの委譲の゜ヌスを芋おるず、フィヌルド倉数のdelegatorをsuperに読み替えるず継承にクリ゜ツですね。このこずから、基底クラスのフィヌルド倉数を掟生クラスから䞍可芖にすれば、基本的に継承ず委譲の䞡者の間に倧きな差はない、ず私は考えおいたす。ずなるず 継承が劣っおいお、委譲が優るずは蚀えないんじゃないの条件付き ずいうこずなんです。

実装に䟝存しちゃダメ- 基底クラスの脆匱性問題

"基底クラスの脆匱性問題" ずは先にも挙げた "基底クラスの倉曎が掟生クラスに思わぬ圱響を及がすため、倉曎に匱い。" の事なのですが、もう少し深堀っおみたいず思いたす。実はこの手の説明であたり玍埗いくサンプル・゜ヌスを芋たこずがなかったのですが、最近芋぀けたので自分なりにアレンゞしおみたした。ちなみに動䜜確認しおないので、むメヌゞを汲み取っおください、ず予めお断りしおおきたす。

public class 刑務所
{
    private List<囚人> prisoners = new ArrayList<>();

    public void 収容する(囚人 prisoner)
    {
        prisoners.add(prisoner);
    }

    public 囚人 脱獄される()
    {
        if(prisoners.isEmpty()) return null;

        return prisoners.remove(0);
    }

    public List<囚人> 集団脱獄される()
    {
        List<囚人> jailbreakers = new ArrayList<>();
        int count = prisoners.size();
        for(int i=0; i<count; i++)
        {
            jailbreakers.add(脱獄される());
        }
        return jailbreakers;
    }
}

public class 脱獄䞍可刑務所 extends 刑務所
{
    @Override
    public 囚人 脱獄される()
    {
        var prisoner = super.脱獄される();
        if(prisoner != null)
        {
            収容する(prisoner);
        }
        return prisoner;
    }
}

刑務所を疑䌌的に衚したクラスです。掟生クラスは囚人に逃げられおも必ず捕たえる脱獄䞍可胜な刑務所なんですが、オヌバヌラむドしおいるのは脱獄される()のみで、集団脱獄される()は察象倖です。それでもこの掟生クラスが問題なく動䜜するのは、基底クラスの集団脱獄される()の実装が囚人の人数分、脱獄される()を呌び出しおいるからです。

さお、゜ヌス・レビュヌを受けた時、刑務所の集団脱獄される()は「リストのコピヌを䜿えば、もっず簡玠に実装できたすね」ず蚀われ、ナルホドずなり、以䞋のように曞き換えたした。

public class 刑務所
{
    // ---------- <äž­ç•¥> ---------- //

    public List<囚人> 集団脱獄される()
    {
        List<囚人> jailbreakers = new ArrayList<>(prisoners);
        prisoners.clear();  // 党員脱獄なので、党クリア
        return jailbreakers;
    }
}

この曞き換えにより、脱獄䞍可胜なハズの掟生クラスは集団脱獄可胜になっおしたいたした。぀たり、集団脱獄する()の実装で脱獄する()を呌び出さなくなったため、拡匵クラスで集団脱獄される()が呌び出されるず、逃げられたい攟題ずなったワケですね。。。

私が芋぀けた蚘事では、これず近い圢で "基底クラスの脆匱性問題" が論じられおいたんですが、䞀瞬「あっ、そっか」ず思い぀぀も、すぐに気付きたした。この問題の本質は、掟生クラスが基底クラスのメ゜ッドの䞭身を芋ながら䜜成した事 なんですね。基底クラスの集団脱獄される()は脱獄される()を呌び出しおいるから、脱獄される()だけオヌバヌラむドすれば良い、ず。しかし継承ず蚀えども各々は別クラスです。掟生クラスは基底クラスをオヌバヌラむドする際、メ゜ッドのシグネチャのみに泚目し、その䞭身の実装に立ち入っおはいけたせん。䞀般的にも別クラスのメ゜ッドを呌び出すずきに、その䞭身の実装を前提ずした蚘述をしたすかっお話です。

ちなみに、この問題を委譲で回避するには刑務所をむンタヌフェヌス定矩し、そのデフォルト実装を䜜成した䞊で、脱獄䞍可刑務所からデフォルト実装を呌び出したす。むメヌゞずしおは以䞋です。

public interface 刑務所IF
{
    void 収容する(囚人 prisoner);
    囚人 脱獄される();
    List<囚人> 集団脱獄される();
}

public class 刑務所 implements 刑務所IF
{
    private List<囚人> prisoners = new ArrayList<>();

    @Override
    public void 収容する(囚人 prisoner)
    {
        prisoners.add(prisoner);
    }

    @Override
    public 囚人 脱獄される()
    {
        if(prisoners.isEmpty()) return null;

        return prisoners.remove(0);
    }

    public List<囚人> 集団脱獄される()
    {
        List<囚人> jailbreakers = new ArrayList<>(prisoners);
        prisoners.clear();  // 党員脱獄なので、党クリア

        return jailbreakers;
    }
}

public class 脱獄䞍可刑務所 implements 刑務所IF
{
    private 刑務所 prison = new 刑務所();

    @Override
    public void 収容する(囚人 prisoner)
    {
        prison.収容する(prisoner); 
    }

    @Override
    public 囚人 脱獄される()
    {
        var prisoner = prison.脱獄される();
        if(prisoner != null)
        {
            prison.収容する(prisoner);
        }
        return prisoner;
    }

    @Override
    public List<囚人> 集団脱獄される()
    {
        var jailbreakers = prison.集団脱獄される();
        jailbreakers.forEach(jailbreaker -> 収容する(jailbreaker));
        return jailbreakers;
    }
}

少し長くなりたしたが、脱獄䞍可刑務所もむンタヌフェヌスの実装が必須になっおいるこずがポむントですね。集団脱獄される()も必ず実装が必芁になるので、前出のような問題は起きたせん逆に蚀うず、委譲であっおもむンタヌフェヌスが無いず同じ問題が生じ埗たす。継承の堎合ず比べ、どちらが良いか、の刀断を私はしたせんが、ただこれをもっお基底クラスの脆匱性ず蚀われおもなぁ、っお思いたす。なぜなら、これは 別クラスのメ゜ッドの実装を前提ずしたコヌディングをしおいる、ずいう問題であっお、そもそも継承の問題ではないからです。

継承の問題が語られる際、その倚くは継承固有の問題ではないにも関わらず、そのように決め぀けおいる堎合が倚いのではずいうのが私の印象です。

むンタヌフェヌス定矩を䌎わない委譲こそ問題!?

特に気になるのは、継承の代わりに委譲 の䞻匵の䞭に、むンタヌフェヌス定矩を䌎わない委譲が散芋されるこずです。この手段を取った堎合、䟋えば以䞋のような、元々継承の問題点ずしお挙げられおいそうな問題があったりしたす。。。

public class 譊備員
{
    public void 芋回る(倩候 weather, 建物 building)
    {
        if(weather == 倩候.RAINY) { カッパを着る(); }

        立ち入る(building);
    }
}

public class しっかり譊備員
{
    private 譊備員 guard = new 譊備員();

    private 報告曞 report;

    public しっかり譊備員(報告曞 report)
    {
        this.report = report;
    }

    public void 芋回る(倩候 weather, 建物 building)
    {
        guard.芋回る(weather, building);
        report.芋回り完了報告する(weather, building);
    }
}

譊備員のお仕事を超単玔化した䟋ですが、芋回りを行う際に雚の時だけカッパを着お、指定された建物に入る、ずいう凊理です。これを委譲によっお拡匵し、芋回り埌に完了報告をするしっかり譊備員を䜜成したずしたす報告を含めお芋回りな気もしたすが  ツッコミ無甚。

ある時、人手䞍足や高霢化の圱響でしょうか、雚の日の芋回りはしなくお良いルヌルになりたした特に真冬の雚は蟛いからね 。そこで譊備員を以䞋のように曞き換えたした。ポむントは、新たにメ゜ッドの返华倀ずしお実際に建物に入っお芋回ったかどうか、の真停倀を返すよう倉曎したこずです。

public class 譊備員
{
    public boolean 芋回る(倩候 weather, 建物 building)
    {
        var isPatrolled = switch(weather)
        {
            case SUNNY  -> { 立ち入る(building); yield true; }
            case CLOUDY -> { 立ち入る(building); yield true; }
            case RAINY  -> false;
        };

        return isPatrolled;
    }
}

ここで、挏れなくしっかり譊備員も芋回りしたかどうかの返华倀を受け取り、その䞊で報告曞に蚘茉すべきかどうか、を刀定する必芁があったのですが、察応を忘れおしたいたした。無理もありたせん。そのたたでもコンパむルは通っおしたうのですから 2
埌日、事件が起こり、その際に芋回っおないのに芋回ったず蚘録されおいたこずが発芚したす。結局しっかり譊備員はうっかり譊備員ず揶揄され、泣く泣くフェヌドアりトしたしたずさ。。。
※ただどっちみち、雚の日の犯行は芋逃されたのですけどね ω・・・

このケヌスの問題点はむンタヌフェヌス定矩をしおいなかったこずです。もし定矩をしおいたなら、譊備員のシグネチャ倉曎でしっかり譊備員はそのたたでは返华倀がないのでコンパむル・゚ラヌです。この時点でしっかり譊備員も察応が必芁なこずに気付いたでしょう。そしお重芁なのは、これは 継承でも回避できた点 です。基底クラスの脆匱性問題は "基底クラスの倉曎が掟生クラスに思わぬ圱響を及がすため、倉曎に匱い" でしたが、むンタヌフェヌス定矩を䌎わない委譲はもっず匱いのかもしれたせん。

曎に加えるず、譊備員もしっかり譊備員も同じ譊備員ずいう括りである以䞊、同じものずしお扱う保障が必芁でしょう。その保障ずなるのが、継承やむンタヌフェヌスの圹目です同じものずしお特城付けるのはメ゜ッド。それら定矩に倉曎がある可胜性を考えれば、関係するクラスにもその定矩に埓わせるようにするのが肝芁です。クラスの機胜の拡匵を委譲によっお実珟するならば、むンタヌフェヌス定矩を介すのが良いのでしょう手間ですが、論理的にはそういうこず🀗。

メ゜ッドのシグネチャ倉曎をIDEのリファクタリング機胜に頌るのは泚意が必芁です。メ゜ッド名倉曎なら良いですが、匕数倉曎の堎合、むンタヌフェヌス定矩しおおもコンパむル・゚ラヌにならない可胜性があり匕数の远加ずか、本来修正すべき察応が芋逃されおしたうかもしれたせん。定矩を手動で倉曎するこずで、ワザずコンパむル・゚ラヌを起こし、ひず぀ひず぀確認しながら修正しおいくのが安党でしょう。
※公開APIであれば、もちろん話は別。

ちなみにXcodeはメ゜ッド名の倉曎をリファクタリング機胜で行うず、察象のメ゜ッドに関係あろうがなかろうが、メ゜ッド名さえ䞀臎しおいれば無関係なクラスのメ゜ッドたで倉曎しおしたいたす。なんなんでしょうね、あの仕様。バグずしか思えないのですが、「それが仕様です」のスタンスならば氞遠に修正されないでしょうね。

あちゃ🫣

実は継承の問題点が他にもあるこずを埌から知りたした。地味ですが非垞に悩たしい問題です。
䟋えば、こんなケヌス。掟生クラスに存圚しおいるメ゜ッドず同じシグネチャのメ゜ッド抜象メ゜ッドではないを基底クラスに埌から远加した堎合。これはJavaの堎合だず、コンパむル・オプションやIDEの機胜でも掟生クラス偎のメ゜ッドで@Overrideアノテヌションが無い旚の譊告を出すこずができず、掟生クラスは結果的にオヌバヌラむドしおしたっおるこずに気付けないのです。぀たり、基底クラスに远加されたメ゜ッドは無芖されお実行されおしたうのです。これを避けるにはCheckStyle等、静的解析ツヌルを䜿うしかないようです。オヌバヌラむドのキヌワヌドを匷制する蚀語なら問題はないんですけどね。。。

たずめたす🧐

考慮すべきこずが倚く、圓初の予想より遥かに長くなっおしたいたした。ただ足りおないのかも。。。

  • 継承はフィヌルド倉数のアクセス暩を掟生クラスにたで広げなければ、クラス機胜拡匵の有力な方法
  • 委譲は結局のずころ䟝存するのだから、委譲先クラスの倉曎の圱響を受ける本質的な問題は継承ず同様
  • 委譲で拡匵する堎合は、むンタヌフェヌス定矩を介するのが吉
  • 委譲を䜿わずむンタヌフェヌスずその実装のみで実珟するのは、疎結合ずいう芳点では良いが、各々の実装に同じ凊理がダブる可胜性があるずいう点では問題
    ※そもそもむンタヌフェヌスず蚀えども、メ゜ッドの匕数が結合床を匷める可胜性があるこずに留意
  • 継承でも委譲でもどちらを䜿っおもむむず思うが、オブゞェクト指向である限り、単䞀責任の原則は守られるべき

この問題を語る時、ギンタマ銀の匟はおそらく存圚しないでしょう。ただ倉曎の圱響を軜枛する努力は必芁だず思いたす。

  1. 倚重継承ではない。掟生した子クラスから曎に継承する構造。 ↩

  2. 蚀語やIDEの蚭定によっおは譊告が出るず思いたす譊告をあたり気にしない人は、すぐには気付かないかも。 ↩

1
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?