青ポスの部屋

旅と技術とポエムのブログ

デザインパターンの原則

デザインパターンとは

大規模コードを書くときに見られるパターン。いわゆるGoF本と呼ばれるごついテキストで提唱されている。

https://www.amazon.co.jp/dp/4797311126

デザインパターンは言語によらない根本思想の話なので勉強しようという人も多いが、正直自分は最初で述べられる2つの原則さえ押さえれば、すべてのパターンを細部まで読んだり暗記したりする必要はないと思っている。

これさえ押さえればよい原則

「継承ではなく委譲を用いよ」

継承オブジェクト指向プログラミングで用いられる、他のクラスの機能を持ったサブクラスを作ること。

しかしメンバやコンストラクタの呼ばれ方がよくわからないことになるので、多用することはバッドプラクティスとされる。多重継承なんか使ったりすると名前が衝突したりするので地獄。

例外的に、抽象クラスについては継承して使うことを目的にしているのでよい。

class hoge{
public:
   uint32_t hoge_func(int xx);
};

// new_funcを実装したくなった
class new_hoge: public hoge{
public:
   uint32_t new_func(int xx);
//...
};

// さらにadvanced_funcを実装したくなった
class advanced_hoge: public new_hoge{
public:
   uint32_t advanced_func(double xx);
//...
};

このコードでadvanced_hogeインスタンスを生成したときにどのコンストラクタがどういう順番で呼ばれるかイメージが瞬時にわかる人は、この記事を読む必要はないと思う。

既存のクラスの機能を利用して拡張したいときは、継承より委譲を使うべきだGoF本にも述べられている。具体的に、新しいクラスのメンバに流用したいクラスのインスタンスをメンバとして持たせる。上記の例なら書き換えると下記のような感じ。

class hoge{
public:
   void hoge_func(int xx);
};

class new_hoge{
private:
   hoge myhoge;
public:
   uint32_t hoge_func(int xx);
};

// 実装:継承ではなくprivateメンバなのでちゃんと外からアクセスできるように実装する
uint32_t new_hoge::hoge_func(int xx){
   return (*this).hoge_func(int xx);
}

「継承はis-a、委譲はhas-aの関係」という説明があるが、自分でコードを構成していくときはそんなにきれいなイメージができるとは必ずしも限らない。迷ったら委譲を使い、外から操作できる必要のあるものはラッパを書いておく。

「インターフェースに対してプログラミングせよ」

これは、「実装に対してプログラミングするな」ということ。インターフェースとは、外部のスコープに公開されている仕様のこと。インターフェースに含まれないプライベートなメンバに外部のコードからアクセスしたり*1、内部の実装を意識して上位コードを書いたりしてはならない

インターフェースを使ったカプセル化については以前述べた。 bluepost69.hatenablog.com

インターフェースが定義されてそれを継承しているクラスに対して実装を読んでコーディングしてしまうと、そのコードはそのクラスに対してしか機能することを保障できなくなる。同じインターフェースを継承する別のクラスに差し替えた時、作ったコードが機能するとは限らない。

ちゃんとインターフェースに対してコーディングしておけば差し替えても機能する。

逆にインターフェースを考えるとき、便利だからと全部publicで宣言してはならない。インスタンス変数や外部からアクセスする必要のない関数はちゃんとprivateにして必要に応じてゲッタを書き、無制限に代入される状態は避ける。

このとき、「必要を感じてゲッタもセッタも書いちゃった場合は結局publicと同じなのでは」という疑問が生じる。しかしセッタは後から必要が出てきたときに引数を適切に評価するコードを付加したりできるので、無制限なアクセス、書き換えが可能なpublicメンバとは異なる。

*1:もっともそのようなことができる言語は少ないが。