Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
25
Help us understand the problem. What is going on with this article?
@ksrnnb

PHPで読む「オブジェクト指向設計実践ガイド」

背景

オブジェクト指向設計実践ガイドを読んでみて、非常に良い内容でしたが、Rubyに馴染みがあまりないのでPHPでまとめました。

まとめ

まずは以下の項目を意識づけるようにして、細かい点は後ほど学べば良さそう。

  • 単一責任
  • 依存オブジェクトの注入
  • デメテルの法則
  • ダックタイピング
  • フックメソッド

はじめに

オブジェクト指向設計とは何か

オブジェクト間の依存関係を管理すること

オブジェクト指向設計のメリット

オブジェクト指向設計の手法に従えば、

  • 楽しくコーディングできる。
  • 効率よくソフトウェアを生産できる

楽しくってのが個人的に好き。

なぜオブジェクト指向設計が必要か?

ソフトウェア開発は、仕様の変更や拡張など、変更がつきもの。
依存関係を管理することで、変更が容易になる。テストもしやすくなる。

自転車の構造

今回、自転車のクラスをみていくので、分かりにくいパーツ名を簡単に説明しておく。
自転車に詳しくないのでざっくりと。

  • チェーンリング:ペダルをこぐと回るやつ
  • コグ:チェーンを通じて後輪で回るやつ
  • リム:タイヤをはめる金属の輪っか

単一責任のクラスをつくる

単一責任の原則。
英語で言うと、SRP (Single Responsibility Principle)

なぜ単一責任が重要なのか

変更が簡単なアプリケーションをつくるには、クラスを再利用しやすくする必要がある。
クラスの責任を1つにすることで、クラスを再利用しやすくする。

どうやるか

やることは3つ。

  • インスタンス変数の隠蔽
  • メソッド単位で単一責任にする
  • 余計な責任を隔離する

まずはダメなコードの例。これを修正していく。

<?php
class Gear
{
    /**
     * チェーンリングの直径
     */
    private $chainring;

    /**
     * コグの直径
     */
    private $cog;

    /**
     * リムの直径
     */
    private $rim;

    /**
     * タイヤの厚さ
     */
    private $tire;

    public function __construct($chainring, $cog, $rim, $tire)
    {
        $this->chainring = $chainring;
        $this->cog = $cog;

        // rimとtireの情報をGearに聞くのはおかしい
        $this->rim = $rim;
        $this->tire = $tire;
    }

    public function ratio()
    {
        // アクセサメソッドで隠蔽するべき
        return $this->chainring / $this->cog;
    }

    // ペダルを1回こいだときに、どれだけ進むかの計算
    public function gearInches()
    {
        // 直径の計算は他のメソッドに分離するべき
        return $this->ratio() * ($this->rim + ($this->tire * 2));
    }
}

$gear = new Gear(52, 11, 26, 1.5);
echo $gear->gearInches();
// -> 137.09090909091

インスタンス変数の隠蔽

インスタンス変数は、常にアクセサメソッドで包んで、直接参照しないようにする。
インスタンス変数が変更になった場合でも、変更箇所は1箇所だけ。

<?php

class Gear
{
    private $chainring;
    private $cog;
    private $rim;
    private $tire;

    public function __construct($chainring, $cog, $rim, $tire)
    {
        $this->chainring = $chainring;
        $this->cog = $cog;

        // rimとtireの情報をGearに聞くのはおかしい
        $this->rim = $rim;
        $this->tire = $tire;
    }

    public function getChainring()
    {
        return $this->chainring;
    }

    public function getCog()
    {
        return $this->cog;
    }

    public function getRim()
    {
        return $this->rim;
    }

    public function getTire()
    {
        return $this->tire;
    }

    public function ratio()
    {
        return $this->getChainring() / $this->getCog();
    }

    public function gearInches()
    {
        // 直径の計算は他のメソッドに分離するべき
        return $this->ratio() * ($this->getRim() + ($this->getTire() * 2));
    }
}

$gear = new Gear(52, 11, 26, 1.5);
echo $gear->gearInches();
// -> 137.09090909091

ゲッターをいちいち定義するのが面倒な場合は、__callメソッドを定義すると良さそう。

メソッド単位で単一責任にする

メソッド単位でも単一の責任を持つようにする。
以下のメソッドは、wheelsを繰り返し処理する、直径を計算するの2つの責任をもってしまっている。

public function diameters()
{
  return array_map(function ($wheel) {
    return $wheel->getRim() + ($wheel->getTire() * 2);
  }, $wheels);
}

これを単一責任にすると、以下のようになる。

public function diameters()
{
  return array_map(function ($wheel) {
    return $this->diameters($wheel);
  }, $wheels);
}

public function diamter($wheel)
{
  $wheel->getRim() + ($wheel->getTire() * 2);
}

余計な責任を隔離する

タイヤの直径の計算をGearから隔離する。Wheelクラスをつくるとよい。
これで単一責任のクラスといえる。

<?php

class Gear
{
    private $chainring;
    private $cog;
    private $wheel;

    public function __construct($chainring, $cog, $wheel)
    {
        $this->chainring = $chainring;
        $this->cog = $cog;

        // wheelインスタンスを受け取る
        $this->wheel = $wheel;
    }

    public function getChainring()
    {
        return $this->chainring;
    }

    public function getCog()
    {
        return $this->cog;
    }

    public function getWheel()
    {
        return $this->wheel;
    }

    public function ratio()
    {
        return $this->getChainring() / $this->getCog();
    }

    public function gearInches()
    {
        return $this->ratio() * $this->getWheel()->diameter();
    }
}

class Wheel
{
    private $rim;
    private $tire;

    public function __construct($rim, $tire)
    {
        $this->rim = $rim;
        $this->tire = $tire;
    }

    public function getRim()
    {
        return $this->rim;
    }

    public function getTire()
    {
        return $this->tire;
    }

    public function diameter()
    {
        return $this->getRim() + ($this->getTire() * 2);
    }
}

$wheel = new Wheel(26, 1.5);
$gear = new Gear(52, 11, $wheel);

echo $gear->gearInches();
// -> 137.09090909091

依存関係を管理する

ダメな例から。
Gearが知っていることは4つある。

  • ほかのクラスの名前(Wheel)
  • self以外に送ろうとするメッセージの名前(diameter)
  • メッセージが要求する引数(rimとtire)
  • それら引数の順番(rimとtireの順番)
<?php

class Gear
{
    private $chainring;
    private $cog;
    private $rim;
    private $tire;

    public function __construct($chainring, $cog, $rim, $tire)
    {
        $this->chainring = $chainring;
        $this->cog = $cog;
        $this->rim = $rim;
        $this->tire = $tire;
    }

    public function gearInches()
    {
        // Gearが自分以外のことを多く知り過ぎている。
        $wheel = new Wheel($this->getRim(), $this->getTire());
        return $this->ratio() * $wheel->diameter();
    }

    public function ratio()
    {
        return $this->getChainring() / $this->getCog();
    }
    // ...
}

依存オブジェクトの注入

引数にオブジェクトを渡すことで、依存を減らす。これがDI(Dependency Injection)
依存性注入とよく訳されるが、Dependencyとはオブジェクトのこと。
つまり、オブジェクトを注入する(->コンストラクタの引数にオブジェクトを渡す)

<?php

class Gear
{
    private $chainring;
    private $cog;
    private $wheel;

    public function __construct($chainring, $cog, $wheel)
    {
        $this->chainring = $chainring;
        $this->cog = $cog;

        // wheelインスタンスを受け取る
        $this->wheel = $wheel;
    }

    public function gearInches()
    {
        // rim, tireやWheelクラスの名前は知らない。
        // diameterに応答するものであれば良い。
        return $this->ratio() * $this->getWheel()->diameter();
    }

    public function ratio()
    {
        return $this->getChainring() / $this->getCog();
    }

    // ...
}

依存の隔離

現在、gearInchesメソッドはwheelに依存している。以下のように前後に計算が入ってくると、変更するのが大変になってくる。

public function gearInches()
{
  // ... 複雑な計算が何行かある
  $this->ratio() * $this->getWheel()->diameter();
  // ... 複雑な計算がさらに何行かある
}

対策として、diameterメソッドを定義することで、gearInchesから依存を取り除く。

public function gearInches()
{
  // ... 複雑な計算が何行かある
  $this->ratio() * $this->diameter();
  // ... 複雑な計算がさらに何行かある
}

// 依存を隔離
public function diameter()
{
  return $this->getWheel()->diameter();
}

依存方向の管理

今回のgearInchesメソッドは、GearクラスではなくWheelクラスがもっていても、問題はない。
自身より変更の少ないクラスに依存させることが重要。
変更が少ないクラスとは

  • interfaceabstractのような抽象的なもの
  • intstringなどの基本クラス

柔軟なインターフェースをつくる

クラスに基づいた設計ではなく、メッセージに基づいた設計へ移行することが重要。
メッセージに基づいて、「なにを」頼むのかをポイントとして考えれば、必要となるインターフェースがみえてくる、ということだと解釈した。

デメテルの法則

最小知識の原則とも呼ばれる。
「直接の隣人にのみ話しかけよう」や「ドットは1つしか使わないようにしよう」と言うとわかりやすい。
つまり、以下のようなコードはやめようね、という話。

$customer->bicycle->wheel->tire;
$customer->bicycle->wheel->tire;
$customer->bicycle->wheel->rotate;

ダックタイピングでコストを削減する

以下はダメな例。
prepareメソッドの引数のクラス名に応じて処理を振り分けている。変更するたびにクラス、処理の追加が必要となってしまっている。

<?php

class Trip
{
    private $bicycles;
    private $customers;
    private $vehicle;

    public function prepare($preparer)
    {
        switch (get_class($preparer)) {
            case Mechanic::class:
                $preparer->prepare_bicycles($bicycles);
            case TripCoordinator::class:
                $preparer->buy_food($customers);
            case Driver::class:
                $preparer->gas_up($vehicle);
                $preparer->fill_water_tank($vehicle);

        }
    }
}

こういう場合は、ダックタイプを使う(ポリモーフィズム)

class Trip
{
    public function prepare($preparer)
    {
        // ダックタイプ
        $preparer->prepareTrip($this);
    }
}

class Mechanic
{
    public function prepareTrip($trip)
    {
        // ...
    }
}

class TripCoordinator
{
    public function prepareTrip($trip)
    {
        // ...
    }
}

class Driver
{
    public function prepareTrip($trip)
    {
        // ...
    }
}

継承

フックメソッドを使ってサブクラスを疎結合にする

具象としてリカンベントバイクを実装するので、先にその説明。
以下の写真のようなもので、flagプロパティは文字通り旗のことを表している。

recumbent.jpg

以下の例では、サブクラスがスーパークラスのコンストラクタを実行する必要があるが、実行し忘れている。
このようなバグが混入するおそれがあるため、結合を弱くする必要がある。

<?php

abstract class Bicycle
{
    private $size;
    private $chain;
    private $tireSize;

    public function __construct($args)
    {
        $this->size = $args['size'];
        $this->chain = $args['chain'] ?: $this->defaultChain();
        $this->tireSize = $args['tireSize'] ?: $this->defaultTireSize();
    }

    public function spares()
    {
        return [
            'tireSize' => $this->tireSize,
            'chain' => $this->chain,
        ];
    }

    public function defaultChain()
    {
        return '10-speed';
    }

    abstract public function defaultTireSize();
}

class RecumbentBike extends Bicycle
{
    private $flag;

    public function __construct($args)
    {
        $this->flag = $args['flag'];    // <- parent::__construct()を忘れている
    }

    public function spares()
    {
        return array_merge(
            parent::spares(),
            ['flag' => $this->flag]);
    }

    public function defaultChain()
    {
        return '9-speed';
    }

    public function defaultTireSize()
    {
        return '28';
    }
}

$bent = new RecumbentBike(['flag' => 'tall and orange']);
echo json_encode($bent->spares());
// -> {"tireSize":null,"chain":null,"flag":"tall and orange"}

フックメソッドの利用

ここで、フックメソッドを使用する。
フックメソッドとは、共通処理をスーパークラスに記述し、のこりの独自処理をサブクラスに記述したもの。
今回の例では、スーパークラスの__constructメソッドに共通処理を記述し、独自処理をサブクラスのpostConstructメソッドで定義する。
こうすることで、スーパークラスのコンストラクタを知らなくて済み、依存が弱くなる。

sparesメソッドも同様。

<?php

abstract class Bicycle
{
    private $size;
    private $chain;
    private $tireSize;

    public function __construct($args)
    {
        $this->size = $args['size'];
        $this->chain = $args['chain'] ?: $this->defaultChain();
        $this->tireSize = $args['tireSize']?: $this->defaultTireSize();

        $this->postConstruct($args);
    }

    public function spares()
    {
        return array_merge([
            'tireSize' => $this->tireSize,
            'chain' => $this->chain,
        ], $this->localSpares());
    }

    // フックメソッド
    public function postConstruct($args)
    {
        return;
    }

    // フックメソッド
    protected function localSpares()
    {
        return [];
    }

    public function defaultChain()
    {
        return '10-speed';
    }


    abstract public function defaultTireSize();
}

class RecumbentBike extends Bicycle
{
    private $flag;

    public function postConstruct($args)
    {
        $this->flag = $args['flag'];
    }

    protected function localSpares()
    {
        return [
            'flag' => $this->flag,
        ];
    }

    public function defaultChain()
    {
        return '9-speed';
    }

    public function defaultTireSize()
    {
        return '28';
    }
}

$bent = new RecumbentBike(['flag' => 'tall and orange']);
echo json_encode($bent->spares());
// -> {"tireSize":"28","chain":"9-speed","flag":"tall and orange"}
25
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ksrnnb
バックエンドエンジニア(PHP)

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
25
Help us understand the problem. What is going on with this article?