月別: 2016年1月

MVC再考 (1)

どの文献を参照しても共通していること

  • ユーザーインターフェースを持つアプリケーションを Model/View/Controller の3つにの部分に分離する

解釈が難しい点

  • モデル・ビュー・コントローラの責務と関係
    • 特にコントローラの責務
    • コントローラとビューの関係性
  • 各プラットフォームにおいて、上記の責務をどう実装に落とし込むのか
    • ほんとうにビューとコントローラは分離できるのか?
    • そもそもビューとコントローラを分離する利点は?

分離とは?

Model-View-Controller に分離するパターンを見ていくが、クラスに分割するだけが分離に相当するわけではない

  • サブルーチン・メソッド
  • クラス
  • パッケージ
  • モジュール

MSDNのMVC記事まとめ

MSDNの記事がわかりやすかったのでまとめた

背景

  • コンピュータシステムの多くは データ を取ってきて、 ユーザ に表示する
  • ユーザがデータを変更したら、 データストア の内容を更新することもある

そのため、データストアとUI間のデータの流れに目がいきがちであり、パフォーマンスのため、またはコード量を減らすために、しばしばその処理を密結合にしてしまうことがある。

このアプローチには2つ問題点がある。

  • UIはデータストアと比較して頻繁に変更が入る傾向にある
  • 複雑なアプリケーションでは、単にデータストアからデータを送受信するだけでなく、複雑なビジネスロジックが存在する傾向にある

問題

ドメインロジックとUIの変更をそれぞれ手軽に独立して行えるようにするためには、Webアプリケーションをどのようにモジュール化すれば良いのだろうか

前提

問題を解決する際に、次のようなことを考慮する必要がある

  • UIロジックはビジネスロジックに比べて頻繁に変更が入る傾向に有る
    • 特にWebアプリケーションにおいては顕著
    • 例えば、新しい画面が追加されたり、すでに存在するページのレイアウトが変更されたりすることが多い
    • したがって、サーバー側に処理をまとめたシステム(シンクライアントシステム)は、アプリケーションを再配布しなくて済む
    • プレゼンテーションコードとビジネスロジックがひとつのオブジェクトとして結合している場合、UIだけを変更したくてもビジネスロジックを含んだオブジェクトを変更しなければならない
    • これはUIの些細な変更であっても、エラーを誘発したり、再テストが必要だったりすることを意味する。
  • 場合によっては、アプリケーションは同じデータを異なる形で表示することがある
    • 例えば、アナリストはスプレットシートを好み、マネージャは円グラフを望むことがある
    • またそれらを同時に表示することも十分に考えられる
    • もしユーザがデータに変更を加えた場合、すべてのビューに反映されるべき
  • デザインと複雑なビジネスロジックを作る作業は全く異なるスキルであり、両方のスキルを持ち合わせる人は滅多にいない
    • そこで、これらの作業はバラバラに実行できることが望ましい。
  • プレゼンテーションはデータソースからデータを取得し、データをフォーマットして表示する
    • 表示されたデータに応じてユーザーはなんらかのアクションを行い、そこからうまれた変更を反映させるためにビジネスロジックへ制御が渡る
  • WebアプリケーションはHTTPのステートレス性の上で、異なるアクションを実現しなければならない
  • UI層のコードは、ビジネスロジックと比較して、デバイスに依存する傾向にある
    • したがって、もしプラウザベースのアプリケーションからモバイル端末などに移植したい場合はUI層のコードを置き換える必要がでてくる
    • UIに関するコードとビジネスロジックを分離しておくことは、移植を素早く行えるようにしたり、移植後のビジネスロジック層のエラーを減らしたりすることに役立つ
  • UI層のテストは難しくて、実行に時間がかかる傾向にある。したがってUI層と直接結びついているコードを減らすことはテスタビリティ向上につながる。

解決策

Model-View-Controllerパターンは、ドメインモデル、プレゼンテーション、ユーザーからの入力に対するアクションの3つのパートに分離をする[Burbeck92]。

  • Model: モデルは状態に応じたアプリケーションドメインの振る舞いを扱い、主にControllerからの操作に応じて状態を変化させる。
  • View: ビューは表示される情報を扱う
  • Controller: マウスやキーボードなどのユーザからの操作を解釈して、モデルやビューに適切に伝える

mvc

  • ビューとコントローラーはモデルに依存してよい
  • モデルはどちらにも依存してはいけない
    • これにより、モデル層とプレゼンテーション層のビルドやテストをバラバラに行うことができるようになる
  • ビューとコントローラーの分離はリッチクライアントアプリケーションでは二の次となる
    • 実際のところ多くのUIフレームワークではひとつのオブジェクトにビューとコントローラーの責務をおいているものが多い
    • 一方、Webアプリケーションにおいてはビュー(ブラウザ)とコントローラー(HTTPリクエストをさばくサーバーサイドコンポーネント)は明確に分かれている。

Model-View-Controller はUIロジックとビジネスロジックを分離するための基本的なデザインパターンである。有名であるがゆえに、多くの間違った説明がなされている。

特に Controller は色々な文脈で異なる説明がなされている。ただし、Webアプリケーションにおいてはビューとコントローラーは明確に分かれているために、こういった曖昧さを解消するためには丁度良い。

passive型 MVC

Steve Burbeckは、In Application Programming in Smalltalk-80: How to use Model-View-Controllerの中で passive 型と active 型の2種類の MVC のかたちについて説明している。

passive型MVCはあるコントローラがモデルを排他的に制御するときに用いることができる。コントローラはモデルを変更し、ビューにモデルが変更されたので内容を更新する必要があることを伝える。このやり方だと、モデルはビューやコントローラと完全に独立した形になり、モデルはビューに変更を伝える手段がない状態になる。HTTPプロトコルなどがこのパターンに合致する。ブラウザがサーバーから更新を受け取るための、この上なくシンプルな方法である。ブラウザはユーザからの入力に応じてビューを表示するが、サーバー上のデータの変化を検出することはできない。ユーザが明示的に更新を問い合わせる必要が有る。

passive

active型 MVC

active 型はコントローラが関わらなくてもモデルの状態が変化する際に用いられる。このモデルの変化は他の入力ソースからデータの変更が発生してしまう場合に起こりうる。基本的にはその変更はビューに反映されるべきである。電光掲示板を作るときにはこうしなければならないだろう。表示内容に変更があった場合、データ取ってきてビューに反映させる必要がありそうである。

しかし、MVCパターンを用いる大きな目的のひとつに、モデルとビューを分離するというものがあったことを思い出そう。モデルがビューに変更を通知するとなると、ふつうにやれば依存関係が発生してしまう。そこでGoFのObserverパターンを思い出そう。これはオブジェクト同士の依存を避けつつも、変更を通知することを実現するデザインパターンである。それぞれのビューはObservarインターフェースを実装して、モデルを購読する。モデルが変更されたら、登録されているすべての observer に通知を行うという仕組みになる。このやり方はしばしば、publish-subscribe と呼ばれる。モデルはビューに関する情報は一切必要としない。たとえば、メニューのオプションを使えなくする必要があるなど、コントローラーがモデルの変更通知を必要とする場合、すべてのコントローラはobserverインターフェースを実装する必要がある。また、たくさんのビューがある場合、それぞれ異なる領域ごとにモデルを定義し、各ビューは関心のあるモデルのみを subscribe すると良いでしょう。

mvc

activemvc

もたらされる影響

MVCのもたらす恩恵

  • 複数のビューをサポートできる: モデルとビューが分離されているため、モデルからビューへ直接の参照は存在しない。したがって、同時に同じデータを様々なビューで表示することができる。これはWebアプリケーションにおける複数のページは同じモデルオブジェクトを使っていることがあることからもわかるだろう。またユーザがページの見た目を変更できるようなWebアプリケーションの存在からも理解することができるだろ。そういったページは同じモデルから得られた同じデータを、異なる方法で表示することができている。
  • 変更に強くなる: UIはビジネスロジックと比較して変更が生じやすい。ユーザは異なる配色やフォントやレイアウトを好むかもしれないし、デバイスに特化したインターフェースを望むかもしれない。モデルがビューに依存していないので、新しいビューのセットをシステムに追加しても、モデルは何の影響も受けない。結果として、変更の影響はビューだけに限られる。この部分をさらに細分化したパターンとして Page Controller と Front Controller というパターンがある。

MVCのもたらす弊害

  • 複雑になる: MVCパターンは遠回りなやり方であり、システムを複雑にする。またデバッグの難しいイベント駆動なコードベースになる傾向にある。
  • 頻繁な更新に対するコストが大きい: モデルとビューを切り離すことは重要だが、決してモデルがビューの存在を無視してよいということではない。モデルが頻繁に更新を行うような場合、ビューにそれを反映させるコストは大きなものとなるだろう。特にグラフィカルなユーザーインターフェースにおいては顕著になる。このような10教科ではビューは描画に失敗することもあるだろう。したがってモデルを作成するときにはビューを念頭に置くことは重要だ。場合によってはモデルの複数の変更をまとめてビューに通知するなどということも必要なはずである。

Document-View パターン

Model-View-Controllerの責務分離は行ったまま、ViewとControllerを結合 したものを Document-View パターンとよんでいる。今日の、多くのGUIプラットフォームではこのパターンが用いられている。VC++のMFCなどはこのパターンの実例のひとつになる。

単一責任の原則(SRP)

Single Responsibility Principle(単一責任の原則)は、Tom DeMarco の “Structured Analysis and Systems Specification” と、Meilir Page-Jones の “The Practical Guide to Structured Systems Design” で説明されている。この原則は次のようなものだ。

クラスを変更する理由は複数存在してはいけない

なぜこうする必要があるのか?

 役割が単一なクラスの仕様要求が変化した場合、その変更部分は浮き彫りになり、どのように変化したのかわかりやすい。これは、ほとんどの人が納得するはなしではないだろうか。

 しかし、役割が複数あると、その1つ1つが変更理由になってしまう。複数の変更理由によってクラスが変更されると、変更部分がぼやけてしまう。また、ある理由により変更した部分が、他の役割に影響してしまい、連鎖的に変更が必要になるなどといったケースが生じうる。これは もろい設計 にあてはまる。もろい設計がソフトウェア開発にどんな影響を及ぼすかは、前に書いた記事を参照されたい。

役割とはなにか?

 「役割=変更理由」と定義している。この判断が非常に難しいケースも多々有る。

変更の理由が変更の理由たるのは、実際に変更の理由が生じた場合だけである

 変化の兆候がないのにSRPを含めた原則を適用するのは賢明ではない。原則を適用することを目的にしちゃいけないというのは、いつも気に止めておかなければならないなぁと思います。

結合している役割を見つけそれらを分離する作業は、ソフトウェア設計の本質である

 非常に身にしみる言葉。

どのように分離するのか

たとえば、次のようなモデムインターフェースを考える。

 最初の2つのメソッドは接続の開始と終了、すなわち接続管理の機能を持つ。後半の2つのメソッドはメッセージのやりとりに用いる。この2つの役割を分離すべきかどうかは今後このアプリケーションがどのように変更されるか次第。たとえば接続管理の機能が影響を受けるような変更がはいる場合は、sendrecv のみを呼び出しているクラスをリコンパイル、リロードしなおさなければならない。もっとも単純にこの状態から打破するには、以下のように分離すればよい。ModelImplはその場しのぎではあるが、すべての依存関係が解消しているはずだ。

oymhIIrAIqnELN19B4bqpaZCoqlDKQZcqbOeJirBqR1ApaWiqWJoYbAJopGqkMgvk3BXEfppWOgJIpBpyy0QKZ8JSpGAylAj589BYZBpqa7QWEQbbnP0D29RO5EZgwlWcv-IckkPMv2Zm8La59eS0000

 テスト主導の開発方法に従えば、設計に怪しい兆しが現れる前に、複雑な役割を持ったクラスは分離されるはずだ。なぜならば、複雑な役割をもったクラスのテストを書くのは非常に難しいからだ。しかしそれでもなお、「硬さ」や「もろさ」を感じる場合には FacadeProxy などを使ってリファクタリングするとよいそうだ[要確認]。

余談

 アジャイル開発の奥義を読むまでは、この原則の名前からして、なぜ「クラスの役割は複数存在してはいけない」という内容ではないのかという疑問を持っていた。読んだ結果自分なりには、「変更」という観点に立ったときにはじめて「役割」ごとに分離する必要がでてくるから、「クラスを変更する理由は複数存在してはいけない」と主張しているのではないかと思った。

 たしかに役割は1つのほうが、見通しがよいかもしれないが、複雑にはなる。変更がとうてい生じないような場所に関して、無駄に分離してコードを複雑にする必要がないという意味を込めたい場合、たしかに「クラスを変更する理由は複数存在してはいけない」のほうがよい言葉だと感じた。この解釈自体は、完全に自分が読んだ感想なので、なにか間違っているなどの指摘がある方はコメントいただけるとありがたい。

アジャイル設計の本を読んでいる

 SOLID原則良いなぁ(←適当)…と思いつつ、それが書かれているアジャイルソフトウェア開発の奥義を読んでいなかったので、最近ちらちらと読み始めた。開発プロセスのあたりにはあんまり興味がわかないので、さらさらと軽い感じに読み飛ばしている。開発プロセス関連のはなしはなるほどなぁとは思うし、全員が内容を理解した上でやっていくと意味はありそうなんだけど、現実的にそんなことは起こり得ないと思っていて、学習コストが必要になる開発プロセスに意味はあるのか…とか偉そうなことを思っている。まあそんなことはどうでもよくて、第2部からのアジャイル設計の部分は本当に参考になるなぁ…と思いながら読んでる。

注意:この文章はポエムです

アジャイル設計とは?

 本によると、アジャイルはソフトウェアを少しずつ開発する手法だそうだ。基本的にラビットハウスでは完成させたソフトウェア(完成とは…という話になるが)を納品するようなお仕事ではなく、ソフトウェアを少しずつ拡張していくスタイルなので、まさに合致している。

腐敗するソフトウェアの兆候

 さて、そんな「少しずつ開発していく」スタイルの場合に、どうやってソフトウェアを設計できるのだろうか?という問いが冒頭にくる。少しずつ開発するとしても、最終的な目的は大きなソフトウェアを保守可能な状態で動かすことにあるので、その段階まで持っていけるような状態を各工程(アジャイル言葉ではイテレーションとかよんでるっぽい)で踏んでいかなければならない。それを阻む要素として次のような項目が挙げられている。

  1. 硬さ: 設計変更が困難
  2. もろさ: 設計が壊れやすい
  3. 移植性のなさ: 設計の再利用が困難
  4. 扱いにくさ: 正しい設計をするのが困難
  5. 不必要な複雑さ: 行き過ぎた設計
  6. 不必要な繰り返し: マウスを使いすぎ
  7. 不透明さ: 整然としない表現

 なるほど、たしかにという気持ちになる。こういった構造を作らないための原則として、有名なSOLID原則が紹介されている

  1. 単一責任の原則(SRP: Single Responsibility Principle)
  2. オープンクローズド原則(OCP: Open-Closed Principle)
  3. リスコフの置換原則(LSP: Liskov Substitution Principle)
  4. 依存関係逆転の原則(DIP: Dependency Inversion Principle)
  5. インターフェース分離の原則(ISP: Interface Segregation Principle)

 設計に関するいろいろな本がでているが、基本的にまずこれを守れてない上でそういうものを取り入れようとしても、かえってカオスになるケースのほうが多いのではないかなぁと個人的に感じている。まあ採用しているアーキテクチャなど状況によりけりでしょうけど。とはいえ、ソフトウェアのうまくいっている部分に無理やり、この5原則を適用しにかかるのはやめなさいという忠告も書かれている。そういう部分にはいろいろ変更がくわわってカオスになってきてから、原則に則ってリファクタしても遅くないと思うので、なるほどなぁ…と思った。こいつらについては、別記事で個別にみていく。

硬さ

 ソフトウェアのちょっとした変更が難しい、1行の変更をしようとすると芋づる式に変更箇所が膨らむということを、硬い設計とよんでいる。これって要はその一行に他のモジュールが依存しているのと変わらないので、結局のところモジュールに分割している意味がないってことな気がする。そうならないようにちゃんと考えて書けよ〜ってことなんでしょうね。

もろさ

 1つの変更により他の多くの部分が壊れてしまう傾向のことを、もろい設計というらしい。そういった箇所は、ソフトウェアの部品としての抽象化に失敗しているということを指すのではないかなと思った。

移植性のなさ

 他のシステムでも役に立つ部分を含んでいるのに、その場所から切り離すことが困難・リスクをともなうような状態。これも機能の抽出に失敗してしまっているときに起こるようなことかなと解釈した。移植するかしないかはともかく、移植できる状態にあることは良いことだとも思う。ただ他のシステムでも役に立つが、移植可能な状態にするのがあまりにも厳しいものはあると思うので、そういった部分はまた別の話かなぁとも思う。難しかったり前例がなかったりする実装はそうなってしまいがちなのはしょうがないと思う。バランスが大事。

(ソフトウェアの)扱いにくさ

 設計構造を保持するやり方が難しく、これまでの設計とは違うが圧倒的に簡単に機能を追加したり変更したりできてしまう場合、そのソフトウェアは扱いにくい。これは非常に重要だと思っていて、難しい設計技法とかをあたまのいい人たちが頑張って取り入れても、そこに追っついていける人が開発メンバでなければ、思想のまったくことなる実装が入り組み、余計複雑なソフトウェアを生み出してしまうと思う。同じ設計手法を用いるとしても、その考え方を理解して、やり方を揃えていけるのであればきっとうまくいくのではないかなと思う。そうでない場合は失敗するだろう。当然のことのはずだけど、新しいことを覚えて感動しているときには、こういうバランス感覚を忘れてしまいがちになるはずで、気をつけなければならない。

(環境の)扱いにくさ

 開発環境がクソゴミで非効率なことを指す。コンパイルに時間が掛かる場合に、設計を維持できないとわかっていてもモジュール分割してしまうみたいなこと。差分管理システムへのコミットに時間が掛かるようなことも例として書かれていた。まあgit使ってる分にはよほどなことがない限りこんな感じにはならないかなと思う。モジュール分割については、もうちょっと考えないと本のいっていることの意味が理解しきれない。そもそもコンパイル時間を減らすことを直接の目的としてモジュール分割するのはどうなんだろうか?あたまが悪いので自分には判断がつかない。

不必要な複雑さ

 設計している時点で不必要な要素が含まれている設計のことを指す。これ気をつけないとやってしまいがちですよね。結局あとから手直しするハメにあったり、つかわれることがないまましれっと削除コミットをしたり。このようなことをすると不必要にソフトウェアは複雑になるそうだ。たしかに分かるような、分からないような。不必要なコードをコミットするのは読み手にとっては非常につらみを増やすだけだが、それがソフトウェアの複雑さに結びつくかどうかの部分が自分の中であんまりはっきりとイメージできないので、これからも継続的に考えていきたい。

不必要な繰り返し

 同じようなコードが繰り返しでてくる。読み手がつらい。こういうコードのレビューをしているとき、推敲されていない文章(この記事のように…)を読んでいるかのようでつらい気持ちになる。このリポジトリはお前のブログじゃねえんだよという気持ちにさせないように、ちゃんと自分の書いたコードは推敲しないといけない。でもこれを応用すれば、嫌いなレビュワーを苦しめることができるということか…(嫌いなレビュワーとは…)。そしてこれを放置していると複雑さにつながると思う。

不透明さ

 わかりにくいモジュールのこと。他人が見て、わかりずれえなってところはこれに該当するんじゃないだろうか。モジュールという言葉はJavaのパッケージ的なものだけでなく、普通にサブルーチン、クラスレベルを含んだ話だと解釈した。書いている本人はわかったつもりだけど、誰かが読むと意味不明なのは、日本人の書く英語のようなものなのかなぁと思う。

アジャイルな設計とはなんなのか?

 アジャイルな設計とは、プロセスやイベントではなく、ソフトウェアの構造や可読性を向上させるために、原則・パターン・プラクティスを継続的に適用する行為だそうだ!(意識たかそう!!!)そのための原則とかパターンが8章以降に書かれているので随時まとめていきたい。

乱数生成を純粋化する

乱数の生成についての簡単な例を見てみよう。scala.util.Random が用意されているのでそれを使えば簡単だ。

nextInt メソッドは呼び出しのたびに返ってくる値が異なる。呼び出しのたびに rng の状態が遷移していることが想像出来る。たとえば、ランダム性を利用したメソッドのテストを書こうとした場合、テストを再現可能にする必要があるが scala.util.Random では難しそうである。仮に乱数ジェネレータを直接扱うとしても、ジェネレータの状態を揃えてあげる必要がある。こういった状態に対処するために、副作用を使用しないという原点に立ち返ろう。

純粋関数型の乱数生成

状態を遷移させるのではなく、新しい状態を返すという気持ちでやっていく。乱数ジェネレータの場合は、ジェネレータの nextInt を呼び出すと、値と新しいジェネレータを返すという感じになる。もちろん、もとのジェネレータは状態遷移しない。したがって、もとのジェネレータの nextInt を何回呼び出しても、同じ値と新しいジェネレータが返ってくる。コードで表現すると次のようになる。

こいつは以下のような挙動を示す。

見ての通り、nextInt は純粋な関数になった。

ステートフルなAPIと向き合う

乱数生成の例で見たようなことと同じような方法で、すべてのステートフルAPIは純粋化できる。基本的には状態を遷移させるのではなく、値と新しい状態を返してやるという方針になる。ただし普通にこれをやってみるとボイラープレートが多くて辛い気持ちになるだろう。たとえば RandomGenerator に対していろんな便利関数を定義してみる。同じことばっかりやっているように見えないだろうか?

すべて RandomGenerator => (A, RandomGenerator) な関数になっている。「状態を遷移させるのではなく、値と新しい状態を返してやる」という方針が関数に明確に現れてきている。この部分は繰り返し出現するのですこし抽象化しよう。type を使って RandomGenerator => (A, RandomGenerator)Rand[A] というエイリアスを張り付ける。すると見なれた mapmap2 などの高階関数が定義できて、かつそれを用いて上で定義した関数などを実装できるようになる。