3
4

More than 3 years have passed since last update.

三角形 is-a 多角形なんて嘘じゃん!―継承を利用する前にこの問題に正解してください【php】【オブジェクト指向】

Last updated at Posted at 2020-03-01

0. 初めに

本記事の結論は

各フィールドに対するgetter, setter, changerのオーバライドの有無は統一する必要がある

この一言に尽きます。

本記事は、僕がつい最近まで勘違いしていたことをまとめた記事です。
オブジェクト指向で「継承」の機能を利用する場合の細かいルールについてです。
(親クラスのメンバと子クラスのメンバは別物であるとか、子クラスでオーバライドしなかったメンバがアクセスするフィールドとか)
その細かいルールを知らないとどんな問題が起きるか、また、どのようなルールになっているのかをこの記事ではまとめます。

1. 問題

第5章※2のコードは、子クラス(三角形クラス)でsetter, getter, フィールドを変化させるメソッド(changer)の3種類をオーバライドしたりしなかったりを試し、三角形インスタンスのgetterがどのような値を返却するか実験するものです。実験結果を予測してみてください。
予測したら、実際にコードを実行して確認してみてください。
全問正解だった場合、本記事を読む必要はありません。
問題は全部で16問あります。

2. 継承の間違った利用法

まず、次のようなphpプログラムを書いてみましょう。

<?php

class 多角形
{
    //頂点の座標の配列
    private $座標配列 = [[]];

    function set座標配列(array $座標配列):void{$this->座標配列 = $座標配列;}

    function get座標配列():array{return $this->座標配列;}

    function move(int $dx, int $dy):void
    {
        $x = 0; $y = 1;
        $座標配列 = [];
        foreach($this->座標配列 as $座標)
            array_push($座標配列, [$座標[$x]+$dx, $座標[$y]+$dy]);
        $this->座標配列 = $座標配列;
        //nunulk様からご指摘をいただいたので修正
    }
}

class 三角形 extends 多角形
{
    //オーバライド
    function set座標配列(array $座標配列):void
    {
        /*
        parent::set座標配列($座標配列);
        /*/
        $this->座標配列 = $座標配列;
        //*/
        if(count($座標配列)!=3) throw new Exception("頂点の個数が多すぎます");
    }

    function get重心()
    {
        $x = 0; $y = 1;
        return [array_sum(array_column($this->座標配列, $x))/3.0, array_sum(array_column($this->座標配列, $y))/3.0];
    }
}

$三角形 = new 三角形();
$三角形->set座標配列
([
    [ 1,0],
    [0,1.7320508],
    [-1,0]
]);
$三角形->move(1,0);
print_r($三角形->get座標配列());
print_r($三角形->get重心());

このプログラムでは、まず親クラスである多角形クラスが$座標配列フィールド、およびそのgetterとsetterを定義しています。
また、moveメソッドでは$座標配列を書き換え、平行移動を実現する機能を持っています。以下、フィールドを書き換えるメソッドをchangerと呼ぶことにします。
また子クラスである三角形クラスでは、座標配列のgetterやchangerはオーバライドせず、親(多角形)クラスのものをそのまま用いています。
そして、三角形クラスのインスタンスを作り、座標を(1,0), (0, 1.7320508), (-1, 0)にセットして、x方向に1、y方向に0だけ平行移動して、その結果を表示させます。

うまくいったなら、
print_r($三角形->get座標配列())によって[[2,0], [1,1.7320508], [0,0]]が、
print_r $三角形->get重心()によって[0, 0.5773502]が表示されるはずですね。

しかし、実行してみると、
print_r($三角形->get座標配列())[[1,0]]を、
print_r $三角形->get重心()[0, 0.57735026666667]を返却します。
get重心はともかく、get座標配列がおかしいですね、、

(具体的には※1-1(第5章)のように表示されます。)

次にset座標配列を次のように切り替えます。

    function set座標配列(array $座標配列):void
    {
        //*
        parent::set座標配列($座標配列);
        /*/
        $this->座標配列 = $座標配列;
        //*/
        if(count($座標配列)!=3) throw new Exception("頂点の個数が多すぎます");
    }

すると今度は、※1-2のように表示されます。
get座標配列は想定通りになったのですが、今度はget重心がダメになってしまいましたね。

3. 継承とプロパティ(フィールド)の関係

第2章の間違いはなぜ発生したのでしょう。これを確認するために、実験をしてみます。

まず※2のようなコードを用意します。

冗長ですが、要はプロパティに対して子クラス(三角形クラス)でsetterやgetter、changerをオーバーライドした場合やしなかった場合、またオーバライドするとしてもparent::を用いた場合とそうでない場合、インスタンスのgetterは何を表示するのかを比較してみようというものです。

※2を実行すると、次のように表示されます。

まず、setter/getterについて(オーバライドの有無)


0:三角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドした場合

1:
↑setterをオーバライドしなかった場合 getterをオーバライドした場合

2:多角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドしなかった場合

3:三角形クラスのフィールドです
↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合



setter/getterについて(オーバライド時に「parent::」を使うか否か)

8:三角形クラスのフィールドです
↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使わずオーバライドした場合

9:
↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使わずオーバライドした場合

10:多角形クラスのフィールドです
↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使ってオーバライドした場合

11:三角形クラスのフィールドです
↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使ってオーバライドした場合



次にchangerについて。


0:三角形クラスのチェンジャが書き換えました
↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドした場合

1:三角形クラスのチェンジャが書き換えました
↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドした場合

2:多角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドした場合

3:三角形クラスのフィールドです
↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドした場合

4:三角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドしなかった場合

5:
↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドしなかった場合

6:多角形クラスのチェンジャが書き換えました
↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合

7:多角形クラスのチェンジャが書き換えました
↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合

3-1. setter/getterについて(オーバライドの有無)

0:三角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドした場合

1:
↑setterをオーバライドしなかった場合 getterをオーバライドした場合

2:多角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドしなかった場合

3:三角形クラスのフィールドです
↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合

0より、setter、getter共にオーバライドした場合、getterではsetterでsetした値を正しく取得できることがわかりました。
また3より、setter、getter共にオーバライドしない場合も同様に正しく値を取得できるとわかります。
問題なのはsetterまたはgetterのいずれか片方のみをオーバライドした場合です。
1より、setterのみをオーバライドした場合、なぜでしょうか。何も表示しません。(nullが返却されています)
また2より、setterのみをオーバライドした場合、親クラスである多角形クラスのフィールドを表示してしまうことがわかります。

これらのことをまとめると、

  • setterのみ、またはgetterのみをオーバライドすると、setterで設定した値をgetterで取得できない。
  • setterとgetterを両方オーバライドする、または両方オーバライドしないと、setterで設定した値をgetterで取得できる

となります。この事実から、次のような仮説が立てられます。

  • 子クラスのメンバと親クラスのメンバは別物である
  • 子クラスでオーバライドしなかったメンバは子クラスに自動的にコピーされるわけではない。子クラスにはメンバは存在しないままである。

このように考えると、setterやgetterのオーバライドの有無によって、取得できるフィールドの内容が異なるという事実を説明することができるようになります。

例えば0なら、三角形クラスはsetter、getter共にオーバライドしているので、setterやgetterは三角形クラスのメソッドです。したがって三角形クラスのフィールドにアクセスします。

また3なら、三角形クラスはsetterもgetterもオーバライドしていないので、setterやgetterは三角形の親である多角形クラスのメソッドです。したがって多角形クラスのフィールドにアクセスします。

0も3も、setterがアクセスしたフィールドにgetterがアクセスするから、setterの設定した値をgetterで取得できるというわけです。

1は、getterのみをオーバライドしているため、setterは多角形クラスのもので、getterは三角形クラスのものです。したがってsetterは多角形クラスのフィールドに値を書き込み、getterは三角形クラスのフィールド(存在しませんが)の値を読みます。したがって、setterの定義した値をgetterは取得できないわけです。そして、存在しない変数が返す値nullが取得されます。

2は、setterのみをオーバライドしているため、setterは三角形クラスのもので、getterは多角形クラスのものです。したがってsetterは三角形クラスのフィールド(存在しませんが)に値を書き込もうとし、getterは多角形クラスのフィールドの値を読みます。

3-2. setter/getterについて(オーバライド時に「parent::」を使うか否か)

0:三角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドした場合

1:
↑setterをオーバライドしなかった場合 getterをオーバライドした場合

2:多角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドしなかった場合

3:三角形クラスのフィールドです
↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合
8:三角形クラスのフィールドです
↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使わずオーバライドした場合

9:
↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使わずオーバライドした場合

10:多角形クラスのフィールドです
↑setterを「parent::」を使わずオーバライドした場合 getterを「parent::」を使ってオーバライドした場合

11:三角形クラスのフィールドです
↑setterを「parent::」を使ってオーバライドした場合 getterを「parent::」を使ってオーバライドした場合

0~3と8~11を比較しましょう。
結果が同じ順番で対応しています。
このことから、

「parent::」を使ってオーバライドしても、(何か別の処理を追加していない限り、)オーバライドしていないのと同じことになってしまう

ということがわかります。言い換えれば、

「parent::」は親クラスのメソッドのコピーではなく、親クラスのメソッドそのものである

というわけです。

3-3. changerについて

0:三角形クラスのチェンジャが書き換えました
↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドした場合

1:三角形クラスのチェンジャが書き換えました
↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドした場合

2:多角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドした場合

3:三角形クラスのフィールドです
↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドした場合

4:三角形クラスのフィールドです
↑setterをオーバライドした場合 getterをオーバライドした場合changerをオーバライドしなかった場合

5:
↑setterをオーバライドしなかった場合 getterをオーバライドした場合changerをオーバライドしなかった場合

6:多角形クラスのチェンジャが書き換えました
↑setterをオーバライドした場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合

7:多角形クラスのチェンジャが書き換えました
↑setterをオーバライドしなかった場合 getterをオーバライドしなかった場合changerをオーバライドしなかった場合

3-3節では、changerをオーバライドした場合とそうでない場合を比較します。
3-1節で立てた仮説の通りに説明できます。

0~1番はgetterとchangerをオーバライドしているため、changerを呼んでからgetterを呼ぶと、三角形クラスのchangerが設定した値をgetterはそのまま取得します。

2~3番は、changerをオーバライドしていますがgetterをオーバライドしていません。
したがって、三角形クラスのchangerが呼ばれ、三角形クラスのフィールドが変更されるのですが、getterは多角形クラスのフィールドを読みます。そのため、changerの結果を取得することができず、3-1節の2~3番と同様の結果になります。

4~5番は、getterをオーバライドしていますが、changerをオーバライドしていません。
したがって、多角形クラスのchangerが呼ばれ、多角形クラスのフィールドが変更されますが、getterは三角形クラスのフィールドを読むため、changerの結果を取得することができません。したがって、3-1節の0~1番と同様の結果になるわけです。

6~7番は、changerとgetterのどちらもオーバライドしていません。
したがって、多角形クラスのchangerが呼ばれ、getterも多角形クラスのフィールドを読みます。そのため、
「多角形クラスのチェンジャが書き換えました」と表示されるわけです。

4. 結論―「三角形 is-a 多角形」には無理がある

  • 子クラスのメンバと親クラスのメンバは別物である
  • 子クラスでオーバライドしなかったメンバは子クラスに自動的にコピーされるわけではない。子クラスにはメンバは存在しないままである。
  • 「parent::」は親クラスのメソッドのコピーではなく、親クラスのメソッドそのものである

以上のことより、「子クラスのインスタンスなのに親クラスのフィールドが関係してきて、思わぬバグを生む」ことを防ぐためには、子クラスにて次のような工夫をする必要があるといえます。

  • 各フィールドに対するgetterやsetterは、両方オーバライドするか、あるいは両方ともオーバライドしないようにする。
  • 親クラスがchangerを持つ場合で、そのchangerをオーバライドしない場合、getterやsetterもオーバライドしない
  • 親クラスがchangerを持つ場合で、そのchangerをオーバライドする場合、getterやsetterもオーバライドする。

さらに短くまとめれば、

  • 各フィールドに対するgetter, setter, changerのオーバライドの有無は統一する必要がある

ということです。

そのため「座標配列」というフィールドだけを持ち、これを書き換えるchangerメソッドが無数にあるような多角形クラスを考える場合、その子クラスが一つでもchangerメソッドをオーバライドするのなら、他のchanger、getter、setterもすべてオーバライドしなくてはならないのです。

これではOAOO原則や差分プログラミングどころではありません。「継承した」というのは名ばかりで、実際には同じコードを子クラスの数だけ何回も書かなければならず、継承のメリットを全く活かすことができません。

5. 付録

※1-1
Array
(
    [0] => Array
        (
            [0] => 1
            [1] => 0
        )

)
Array
(
    [0] => 0
    [1] => 0.57735026666667
)
※1-2
Array
(
    [0] => Array
        (
            [0] => 2
            [1] => 0
        )

    [1] => Array
        (
            [0] => 1
            [1] => 1.7320508
        )

    [2] => Array
        (
            [0] => 0
            [1] => 0
        )

)

Warning: array_column() expects parameter 1 to be array, null given in C:\fakepass\test.php on line 33

Array
(
    [0] => 0
    [1] => 0
)
※2

<?php


class 多角形
{

    private $field0 = "多角形クラスのフィールドです", $field1 = "多角形クラスのフィールドです", $field2 = "多角形クラスのフィールドです", $field3 = "多角形クラスのフィールドです", $field4 = "多角形クラスのフィールドです", $field5 = "多角形クラスのフィールドです", $field6 = "多角形クラスのフィールドです", $field7 = "多角形クラスのフィールドです", $field8 = "多角形クラスのフィールドです", $field9 = "多角形クラスのフィールドです", $field10 = "多角形クラスのフィールドです", $field11 = "多角形クラスのフィールドです";

    function set0(string $value){$this->field0 = $value;} function set1(string $value){$this->field1 = $value;} function set2(string $value){$this->field2 = $value;} function set3(string $value){$this->field3 = $value;} function set4(string $value){$this->field4 = $value;} function set5(string $value){$this->field5 = $value;} function set6(string $value){$this->field6 = $value;} function set7(string $value){$this->field7 = $value;} function set8(string $value){$this->field8 = $value;} function set9(string $value){$this->field9 = $value;} function set10(string $value){$this->field10 = $value;} function set11(string $value){$this->field11 = $value;}

    function get0(){return $this->field0;} function get1(){return $this->field1;} function get2(){return $this->field2;} function get3(){return $this->field3;} function get4(){return $this->field4;} function get5(){return $this->field5;} function get6(){return $this->field6;} function get7(){return $this->field7;} function get8(){return $this->field8;} function get9(){return $this->field9;} function get10(){return $this->field10;} function get11(){return $this->field11;}

    function changer0(){$this->field0 = "多角形クラスのチェンジャが書き換えました";} function changer1(){$this->field1 = "多角形クラスのチェンジャが書き換えました";} function changer2(){$this->field2 = "多角形クラスのチェンジャが書き換えました";} function changer3(){$this->field3 = "多角形クラスのチェンジャが書き換えました";} function changer4(){$this->field4 = "多角形クラスのチェンジャが書き換えました";} function changer5(){$this->field5 = "多角形クラスのチェンジャが書き換えました";} function changer6(){$this->field6 = "多角形クラスのチェンジャが書き換えました";} function changer7(){$this->field7 = "多角形クラスのチェンジャが書き換えました";}

}

/**
 * フィールドには0~15の番号が振られている。
 * 0~7番については、
 * 0→ changer〇, getter〇, setter〇
 * 1→ changer〇, getter〇, setter×
 * 2→ changer〇, getter×, setter〇
 * 3→ changer〇, getter×, setter×
 * のように、メソッドをオーバライド(「parent::」を使わない)したり(〇)しなかったり(×)を変化させている。
 * 8~11番についてはメソッドをオーバライドするが、
 * オーバライドで「parent::」を使わなかったり(〇)、使ったり(×)を変化させている。
 */
class 三角形 extends 多角形
{
    function set0(string $value){$this->field0 = $value;}                               function set4(string $value){$this->field4 = $value;}
    function get0(){return $this->field0;}                                              function get4(){return $this->field4;}
    function changer0(){$this->field0 = "三角形クラスのチェンジャが書き換えました";}      //function changer4(){$this->field0 = "三角形クラスのチェンジャが書き換えました";}

    //function set1(string $value){$this->field1 = $value;}                           //function set5(string $value){$this->field5 = $value;}
    function get1(){return $this->field1;}                                              function get5(){return $this->field5;}
    function changer1(){$this->field1 = "三角形クラスのチェンジャが書き換えました";}      //function changer5(){$this->field0 = "三角形クラスのチェンジャが書き換えました";}

    function set2(string $value){$this->field2 = $value;}                               function set6(string $value){$this->field6 = $value;}
    //function get2(){return $this->field2;}                                          //function get6(){return $this->field6;}
    function changer2(){$this->field2 = "三角形クラスのチェンジャが書き換えました";}      //function changer6(){$this->field0 = "三角形クラスのチェンジャが書き換えました";}

    //function set3(string $value){$this->field3 = $value;}                           //function set7(string $value){$this->field7 = $value;}
    //function get3(){return $this->field3;}                                          //function get7(){return $this->field7;}
    function changer3(){$this->field0 = "三角形クラスのチェンジャが書き換えました";}      //function changer7(){$this->field0 = "三角形クラスのチェンジャが書き換えました";}


    function set8(string $value){$this->field8 = $value;}
    function get8(){return $this->field8;}

    function set9(string $value){parent::set9($value);}
    function get9(){return $this->field9;}

    function set10(string $value){$this->field10 = $value;}
    function get10(){return parent::get10();}

    function set11(string $value){parent::set11($value);}
    function get11(){return parent::get11();}

}

$三角形 = new 三角形();
$三角形->set0("三角形クラスのフィールドです");$三角形->set1("三角形クラスのフィールドです");$三角形->set2("三角形クラスのフィールドです");$三角形->set3("三角形クラスのフィールドです");$三角形->set4("三角形クラスのフィールドです");$三角形->set5("三角形クラスのフィールドです");$三角形->set6("三角形クラスのフィールドです");$三角形->set7("三角形クラスのフィールドです");$三角形->set8("三角形クラスのフィールドです");$三角形->set9("三角形クラスのフィールドです");$三角形->set10("三角形クラスのフィールドです");$三角形->set11("三角形クラスのフィールドです");

print "まず、setter/getterについて(オーバライドの有無)\n";

for($i = 0; $i <= 3; $i++)
{
    $is_setter_overrided = !($i%2);
    $is_getter_overrided = !(floor($i/2)%2);
    print "\n\n$i:";
    eval("print \$三角形->get$i().\"\n\";");
    if($is_setter_overrided)
        print "setterをオーバライドした場合 ";
    else
       print "setterをオーバライドしなかった場合 ";
    if($is_getter_overrided)
        print "getterをオーバライドした場合";
    else
       print "getterをオーバライドしなかった場合";

}

print "\n\n\n\nsetter/getterについて(オーバライド時に「parent::」を使うか否か)\n\n";

for($i = 8; $i <= 11; $i++)
{
    $is_setter_overrided = !($i%2);
    $is_getter_overrided = !(floor($i/2)%2);
    print "\n\n$i:";
    eval("print \$三角形->get$i().\"\n\";");
    if($is_setter_overrided)
        print "setterを「parent::」を使わずオーバライドした場合 ";
    else
       print "setterを「parent::」を使ってオーバライドした場合 ";
    if($is_getter_overrided)
        print "getterを「parent::」を使わずオーバライドした場合";
    else
       print "getterを「parent::」を使ってオーバライドした場合";

}

print "\n\n\n\n次にchangerについて。\n";

$三角形->changer0(); $三角形->changer1(); $三角形->changer2(); $三角形->changer3(); $三角形->changer4(); $三角形->changer5(); $三角形->changer6(); $三角形->changer7();

for($i = 0; $i <= 7; $i++)
{
    $is_setter_overrided = !($i%2);
    $is_getter_overrided = !(floor($i/2)%2);
    $is_changer_overrided = !(floor($i/4)%2);

    print "\n\n$i:";
    eval("print \$三角形->get$i().\"\n\";");
    if($is_setter_overrided)
        print "setterをオーバライドした場合 ";
    else
        print "setterをオーバライドしなかった場合 ";
    if($is_getter_overrided)
         print "getterをオーバライドした場合";
    else
        print "getterをオーバライドしなかった場合";
    if($is_changer_overrided)
        print "changerをオーバライドした場合";
    else
        print "changerをオーバライドしなかった場合";

}

3
4
7

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
3
4