再考: GoF デザインパターン

  • 941
    Like
  • 12
    Comment
More than 1 year has passed since last update.

本投稿は私の主観によって書かれています。コメントは大歓迎です。もし長くなるようでしたら別途記事に投稿し、リンクを張っていただけると嬉しいです。

概要

GoFのデザインパターンは適当すぎるから、いい加減、修正されるべき。
参考までに各パターンに対するコメントを書く。

GoFのデザインパターン

GoFのデザインパターンは適当であり、教科書通りに学ぶべきものではないように思う。
以下がGoFのデザインパターンの良くない原因だろう。

  • 本が出版されたのは1994年であり、Java(1995)が出てくるよりも前だった
    • オブジェクト指向が未成熟な時代にカタログ化された
    • 現代のプログラミングと合致しないものが多い
  • 「オブジェクト指向における~」と断っている以上、OOPに絡める必要があった
    • パターンのいくつかに「多態性を用いると便利」という蛇足がついている
  • 挙げたパターンに根拠がない
    • 「とりあえず、23個ほど列挙してみました」でしかない
    • 説明に目的と実装が入りまじっており、まともな定義になっていない
      • 人(書籍)によって言っていることが違う

デザインパターンに対するコメント

生成に関するパターン

Abstract Factory

Factoryクラスを切り替えることで生成するインスタンスを切り替えることができる。
OOPの良いデザインパターンだと言える。

しかし極めて普通のOOPの使い方であり、生成に限定したからこのような名前を付けただけ、とも取れなくない。

Builder

複雑なオブジェクトを生成するのに、生成専用のクラスを用意するのは有用である。
しかしこのパターンで多態性による切り替えに言及すべきではない。

Factory Method

コンストラクタの代わりに生成用関数を用意するのは有用である。
C++のコンストラクタでは複雑な処理や例外は避けるべきであるし、Javaでもコンストラクタから子クラスのインスタンスを返すことはできない。
このパターンも多態性による切り替えに言及すべきでない。

Factoryメソッドは生成したいクラスに用意すると良いと思う。

Prototype

複雑なインスタンスを生成したいのであればBuilderを作るべきである。
単にcloneしたいのであればそれはデザインパターンではなく、メソッドを用意しただけである。
このパターンは不要だろう。

Singleton

グローバルの免罪符として使われている現状をどうにかしないといけない。
「状態を持たず多態性を利用したい」という状況でのみ使うべきである。
多態性を利用しないのであれば、staticを利用したほうが単純性が増す。

構造に関するパターン

Adapter

インタフェースをそろえるためにラッパーを用意することはよくある。
普通のOOPの使い方である。

Bridge

interfaceの説明をしているだけに感じる。

Composite

GUIなどによくあるパターンである。
これはデザインパターンにふさわしい内容だろう。

一応「委譲による継承」も参考に。

Decorator

「委譲による継承」としてまとめるべきである。

Facade

よく使われそうなメソッドだけ、わかりやすいところに出しておく。
これは良い設計であり、デザインパターンにふさわしいだろう。

Flyweight

JavaのIntegerクラスなどが実例。
OOPでなくても使うが、省メモリのためのパターンとして良いだろう。

Proxy

直接触る代わりに、中間層を置くのは悪くない手法である。
汎用的過ぎて、デザインパターンと言えるのかは微妙。

例えば、直接Stringを触る代わりにHtmlStringRawStringにWrapして使おう。というのも中間層を置いていることに変わりはない。
また、マルチスレッドで使われるFutureパターンも同様である。
中間層はあらゆる問題の解決に利用される。

振る舞いに関するパターン

Chain of Responsibility

「委譲による継承」としてまとめるべきである。

Command

このパターンで複数のことを説明しすぎである。
まず、関数そのものをオブジェクトとし「切り替えて使える」ことはStrategyの説明に押し付けるべきであった。

命令を抽象化しクラスにするのは良い設計である。
例えばcanExecuteexecuteを持つようなクラスを用意することは凝集度が高まってよい。
migraterollbackなどでも同様である。

Interpreter

「構文木もオブジェクトで表現できる」以上のことは何も言っていない。
無意味である。

Iterator

多態性が有用であることのサンプルの一つ。
iterator自体は知っておいて損はないだろう。
現在においては言語組み込みとなっていることが多く、改めてデザインパターンで学ぶ必要はないかもしれない。

Mediator

内向きのFacadeパターンである。ではなかった。コメントを受けて修正。

クラス間の疎結合を満たすために仲介クラスを用意するのは良い設計である。
Godクラスにならないように注意する必要がある。

Memento

状態を元に戻したいという要求はある。
しかしこれの実装はまさに実装依存であるので、デザインパターンとして列挙するにはおかしい。

Observer

OOPのデザインパターンとしてふさわしい内容だろう。
ただ「関数オブジェクト」で置き換え可能な場合が多く、学ばなくてもよいかもしれない。

State

OOPの良くないパターンである。
状態遷移図があるような遷移を各クラスで表現すると、凝集度が下がりコードが読みづらい。
普通にstateフィールドとifで書いたほうがわかりやすい。

一方、表を書くほどではなく、単に次へ次へと進んでいくような場合は悪くない設計である。
ゲームプログラムにおいてSceneクラスとして知られている。

Strategy

単なる関数オブジェクトである。

Template Method

非常によく使うが、単なるabstractメソッドの使い方である。
デザインパターンかといわれると判断しづらい。

Visitor

良くはないが、やむを得ないデザインパターンである。
State同様、凝集度が下がりコードの見通しが悪くなる。
ScalaやRubyなど、クラスでパターンマッチができる言語であればそちらを利用すべき。

そもそもこのパターンを使う機会はほとんどないのではないか。
木構造で有名なものはファイルシステムだが、実際にはisDirのような関数を用いる。
自分も構文木をたどるときしか使った覚えがない。
(構文木を扱うプログラマは数少ないだろう)

委譲による継承

RubyにRackというインタフェース(あるいは約束)があり、これはリクエストを受けてレスポンスを返す抽象的なWebサーバーである。

class Greet
  def call(env)
    req = Rack::Request.new(env)
    return [200,
      { 'Content-Type' => 'text/plain' },
      ["Hello, #{req.param['name']}"]
  end
end

さらにRackMiddlewareがあり、これはRackインスタンスをラップし自身がRackインスタンスとして振る舞う。

class CacheMiddleware
  def initialize(app)
    @app = app
  end
  def call(env)
    @cache ||= {}
    @cache[env['REQUEST_PATH']] ||= @app.call(env)
  end
end

さてこのパターンを何と呼ぶのだろう?
Decoratorか、Chain of Responsibilityか。
今回はキャッシュなのでChain of Responsibilityだというかもしれないが、JSONPに対応させればDecoratorだというだろう。
もしリクエストパスで分岐するのであればCompositeパターンになる。

これは単純に「委譲による継承」パターンとすべきだろう。

最後に

これは私の主観(経験則)なので、ほかの人は別のことを考えるだろう。
ここで挙げていないが有用なデザインパターンも数多くある(Loanパターンなど)
誰か有識者がまとめてくれないだろうか。

コメント募集(再)