カプセル化となんちゃってカプセル化のアンチパターン

著者:杉浦

カプセル化となんちゃってカプセル化のアンチパターン

 カプセル化はオブジェクト指向の中で述べられている手法の一つです。オブジェクトに関する詳しい処理や情報をオブジェクト内部に閉じ込めておき、外部からは必要な分だけのまとまった簡単な部分にのみにアクセスできるようにする方法です。よくある例えに車の例えがあります。運転手はアクセルを踏めるしギアも変えられるが、エンジンのシリンダー一つ一つや空気の流入量を操作することはできない、というものです。これは動力のカプセル化で、車の内部を深く知らず瞬時に燃料の爆発に最適な環境を計算できない多くの人間からエンジン内部の詳しい動作を隠し、まとまった操作であるギアとアクセルにだけアクセスできる様にしています。こうすることで誤った操作を減らし、操作の混乱を防ぎます。
 コーディングにおけるカプセル化は例えば次の様になります。

/**
 * @property-read int    $id    読み取りのみ可能
 * @property      string $mail  読み取りと特定形式の書き込みのみ可能
 * @property      string $name  読み書き自由
 */
class User
{
    private $id;
    private $mail;
    public  $name;
    private $secretProperty;  // 内部処理の何かしらにだけ使う完全に外から隠れるプロパティ

    /**
     * 主キーで検索.
     * @param int $id
     * @return User
     */
    public static function find(int $id): User
    {
        return DB::select()
            ->from('users')
            ->where('id', $id)
            ->first();
    }

    /**
     * 任意のプロパティにまとめて代入. 代入後の$thisと代入予定の引数が一致すればtrue, しなければfalse.
     * @param array $values
     * @return boolean
     */
    public function fill(array $values): bool
    {
        $this->name = $values['name'] ?? $this->name;
        isset($values['mail']) ? $this->setMail($values['mail']) : null;

        return $values === array_intersect_key(['name' => $this->name, 'mail' => $this->mail], $values);
    }

    /**
     * データベースに保存.
     * @return boolean
     */
    public function save(): bool
    {
        return DB::select()
            ->from('users')
            ->where('id', $this->id)
            ->updete([
                'mail' => $this->mail,
                'name' => $this->name,
            ]);
    }

    /**
     * プロパティの読み取り範囲を通常の可視性を超えて操作.
     * @param $propertyName
     * @return mixed
     */
    public function __get($propertyName)
    {
        return $propertyName === 'secretProperty' ? null : $this->$propertyName;
    }

    /**
     * setterがあるプロパティならばprivateでも書き込める.
     * setterがないプロパティならば新規追加も許さない.
     * @param $propertyName
     * @param $value
     * @return bool
     */
    public function __set($propertyName, $value): bool
    {
        $setMethodName = 'set'.ucfirst(camel_case($propertyName));

        return $this->$setMethodName($value);
    }

    /**
     * 存在しないメソッドが呼ばれたらfalseを返す.
     * @param $methodName
     * @param $arguments
     * @return bool
     */
    public function __call($methodName, $arguments):bool
    {
        return false;
    }

    /**
     * 存在するprivateプロパティに対してisset()をかまされた時にtrueを返すために必要.
     * @param $propertyName
     * @return bool
     */
    public function __isset($propertyName): bool
    {
        return isset($this->$propertyName);
    }

    /**
     * メールとして認められる文字列ならばsetして良し.
     * @param $value
     * @return bool
     */
    private function setMail($value): bool
    {
        if(preg_match('/メールアドレスを表す適当な正規表現/u', $value)){
            $this->mail = $value;

            return true;
        }

        return false;
    }
}

 id, mail, nameの三つの要素を持つユーザクラスです。それぞれの可視性と代入時の操作を設定することでカプセル化の利点の誤った操作の軽減を実現しています。プロパティの増減まで縛っているのはわかりやすさの点で微妙ですが。大体のフレームワークのモデル周りの機能にもっと洗練したものが載っているので手書きで作るよりそちらを使った方が良いです。
 カプセル化をやりたかった時に起きるアンチパターン対策として重要なのは上記の内のmailプロパティのset機能です。mailプロパティに代入する際にバリデーションが働き、mailプロパティに入っている文字列はメールアドレスとして認められる文字列だと整合性を保っています。データベースの型のみではできないバリデーションで特に有効です。この手のプロパティにアクセスする際にわざわざメソッドを増やす意味を無視するとただただカプセル化処理のめんどくささだけが残ります。
 ありがちなカプセル化っぽいことをするアンチパターンが次です。

class Engine
{
    private $gear
    
    public function setGear($value)
    {
        $this->gear = $value;
    }

    public function getGear($value)
    {
        return $this->gear;
    }
}

 ただprivateプロパティを書き換えてるだけで、こんなことをするぐらいならpublicで宣言すれば良いです。set, getメソッドの長さの分むしろ読み難くなります。これがあるあるの間違いになるのはset, getを自動で生やす機能がIDEには大体備わっているためです。適切に使えば便利なのですが、とりあえずメソッドを介してプロパティにアクセスすればカプセル化になると考える人とこの機能が組み合わせると上記アンチパターンが発生し、後に続く人がコードを読む際に苦しみます。

 正しくオブジェクト内部に留めておくと変更も楽です。これはオブジェクト内部しか影響しない、とわかりきっているコード部が増えるためです。

  • この記事いいね! (1)

著者について

杉浦 administrator