PHP Mentors

1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

DCI Tokyo 2 - Commonality / Variability Analysis: Practical MPD - が開催されました

2018年1月10日に開催された DCI Tokyo 1 に続き、2018年3月27日に DCI Tokyo 2 が開催されました。今回も James Coplien @jcoplien さんをお招きしてのトークセッションとなりました。会場は 株式会社ヴァル研究所 様に提供していただきました。

セッションは、前回同様 @remore さんと @ganchiku さんによる同時通訳とともに進められました。

今回のテーマはマルチパラダイムデザイン(Multi-Paradigm Design: MPD)の中核を成し、DCI / リーンアーキテクチャ(Lean Architecture)とも深く関係する 共通性/可変性分析 でした。

レポートは @smori1983 が担当させていただきます。

当日の様子は Coplien さんの許可を得て YouTube の DCI Tokyo 公式アカウントにて公開されています。Coplien さんのおもしろTシャツにも注目しながら、ぜひご覧ください。

全体の流れ

全体の流れとしては、1980年代以降のソフトウェア開発の歴史を、主にオブジェクト指向に対する考え方の変遷を通して振り返りつつ、MPD と DCI の関係性に迫るものとなりました。

オブジェクト指向(1988)

当時は C++ の他に、テレコム業界向け言語である CHILL や、軍需産業向け言語である Ada などの言語があり、それらの言語がオブジェクト指向なのかという議論がされていました。

最終的に C++ が残ることになりますが、C++ の作者である Stroustrup 氏は、そもそも C++ をオブジェクト指向言語ではなく、マルチパラダイム言語と定義していたことに注意する必要があります。

当時考えられていたオブジェクト指向の構成要素は次の3項目でした。

  1. inheritance / 継承
  2. polymorphism / ポリモーフィズム
  3. instantiation / インスタンス化

1994年に Coplien さんは David Weiss 氏と Robert Chi Tao Lai 氏とともに仕事をしますが、その時、共通性/可変性分析を通じて、ソフトウェアファミリの振る舞いなどを特徴づけ、当時取り組んでいたソフトウェアのための DSL(Domain Specific Language) を開発しました。開発言語は C でした。

この、共通性に注目してグルーピングしたものをファミリと定義する方法は、Weiss 氏らが考案したFAST(Family Abstraction Specification Technique) という手法に基づいています。

DSL は小さなドメインを対象にしていて、ドメインが変化しない限りはうまくいったそうですが、ドメインは変化していくものです。DSL は、デバッガなど周辺技術との密な関係性の中で成立するため、その変化に対しては脆いという問題がありました。

ただし、共通性/可変性分析自体は非常に有用で、ドメイン分析に対しては非常に多くの収穫があったそうです。

分析の結果得られる共通性/可変性テーブルの内容の埋め込み(注: 解決ドメインの抽象による実装)に関して、

  • DSL で埋め込むと、固くなりすぎ、脆くなる。
  • C で埋め込むと、言語特有の制約が多く、開発者の intentionality(意図)がわからなくなる。

という課題がありましたが、C++ を用いることで、よりうまくいくようになりました。

オブジェクト指向(1995)

Coplien さんは、C++ を

共通性/可変性分析のための、汎用言語キットである

と捉えます。

その視点で見たオブジェクト指向は、次の4項目の構成要素で理解されるようになりました。

  1. Common Signature
  2. Common Data
  3. Variation in Data
  4. Variation in Algorithm

また、polymorphism / instantiation は、共通性/可変性のバインディングタイム(注: 共通性/可変性が変数に束縛されるタイミング)の選択の問題として捉え直されることになりました。

オブジェクト指向(DCI 時代)

Coplien さんによれば、更にこの後、3段階の気付きを経て、現在のオブジェクト指向理解へとつながっているそうです。

1段階目: There  is no OO in C++

  • 2010年くらいまでは、オブジェクト指向という言葉を、抽象データ型( Abstract Data Type: ADT )という意味で使っていた。
  • C++ は実際には抽象データ型ではなく、具象データ型( Concrete Data Type: CDT )であるということ。

2段階目: There are no objects in source code

  • C++ プログラムはソースコードである。
  • そこにはオブジェクトは存在しない。ソースコードに記述しているのはクラスである。
  • 多くの人々が Java などの言語でやっているのはクラス指向プログラミングである。

3段階目: OO is mental model

  • Trygve Reenskaug 氏とオブジェクトとは何か、オブジェクトパラダイムとは何かについて話し合い、DCI が生まれた。
  • DCI は、抽象データ型の実装を意味したこれまでのオブジェクト指向とは別の視点を与える。
  • 実行時におけるオブジェクトのネットワークがどのように問題を解決するかに注目する( Alan Kay の定義)。
  • 我々はクラスとロールをごちゃまぜにして考えていた。
    • 抽象データ型について考えるのがクラス指向。
    • ロールについて考えるのがオブジェクト指向。
    • 抽象データ型+インスタンス化がロールを実行(play)。

MPD と DCI

MPD は、クラス指向のレイヤーにおいてドメイン分析を行う時、ソフトウェアファミリの共通性/可変性分析を通じてドメインモデルを構築する手法を提供します。

DCI は、我々のメンタルモデルに基づいて、オブジェクトどうしの相互作用を定義します。そこでは、抽象データ型としてのクラスに具体的なロールを注入されたオブジェクトが、特定のコンテキストのもとに配置され、相互作用するという、動的な側面を捉える方法を提供します。

以上を簡単にまとめると、次の表のようになるかと思います。

パラダイム設計対象領域手段視点
DCIオブジェクトとそれらの相互作用ロール / コンテキストユースケースTime
MPD抽象データ型、モデルの構造クラス / Abstract Base Class (ABC)共通性/可変性分析Space

歴史的には MPD が先行しました。それは後に、モデルの静的な構造の分析を行うための領域を担う設計方法論という捉え直しがされました。そして、オブジェクト指向に対する新しい視点を提供する DCI が、クラス指向では扱うことが難しかった、モデルの動的な関係性を捉えるための方法論として提起されています。

Time and Space と DCI における共通性/可変性

DCI は、オブジェクトどうしの相互作用に注目します。つまり基本的には時間の経過(シーケンス)が意識されます。しかし、あるロールを付与された複数のオブジェクトが配置されてこそ、相互作用が成立するものです。

コンテキストとは、このように、ある状況に参加する主体の空間的配置と時間的な相互作用の総体として捉えられるものと言えます。

ここで、日本人にとってはありふれた「間」という概念が取り上げられました。具体例としては次のようなものが話題にあがりました。

  • 歌舞伎( Coplien さん)
  • 龍安寺の石庭( @koriym さん)

また、アレグザンダーの考案したパターンランゲージに話が及びました。パターンランゲージは、建築の構造(空間的なパターン)だけではなく、その構造が生み出す人々の相互作用(時間的なパターン)も内包したものとして理解されるべきです。

pattern of behavior = pattern in space + pattern in time

セッションの終盤では、このように、DCI が担う領域における共通性/可変性概念の適用可能性が示唆されました。

さいごに

会場を提供していただきましたヴァル研究所様、DCI Tokyo スタッフの皆様、今回もすばらしいイベントとなりました。ありがとうございました。

次回は、ワークショップ形式での開催も視野に入れながら、DCI Tokyo 3 が開催されることと思います。MPD / DCI / Lean Architecture に興味のある方は、ぜひご参加ください。

design mpd multiparadigm.design dci leanarchitecture ddd practical.ddd

DCI Tokyo 1 - Lean Architecture by James Coplien - が開催されました(後編)

後半のセッションでは、「What the system is(共通性や可変性を分析する)」を説明しつつも、「What the system does」とは別のものとして切り分けることはできないという主張から、 DDD とリーンアーキテクチャとの比較、そしてパターンに関する議論が展開されました。 @remore が前半部分をまとめてくださっていまして、そこで出て来るいくつかの用語(Form, Structure, What the system is, What the system does)を前提として私のできる範囲で行いました。

なお当日のCoplien氏によるセッション内容は、許可を得た上でYouTubeにアップロードされていますので、より深くご覧になりたい方はこちらも併せてご参照下さい。

What system is に関して

Jim Coplien さんの言うアーキテクチャ

  • 共通性、可変性分析に関する What system is に関連している。
  • ほとんどのドメイン知識は、アルゴリズムで表される What system does よりも What system is の方にある。

ソフトウェアを開発する際に重要なもの

  • プログラム言語自体の知識ではなく、ドメインでありビジネスである。
  • ほとんどのドメイン知識は、構造(Structural)であって、アルゴリズムではない。

このミートアップでは、構造(Structure)の方を主に扱う。What system is の方である。

  • 実際のシステムを構築する際には、構造は安定されていることが多いので先に行うことが多く、それらが出来上がってから、まだ実装されていない機能を作り始める。
  • その機能が、人々が求めているユースケースになり、これがビジネスとなる。

What system is と What system does の関係性

しかし、What system is と What system does は別々に考えられるものではない。それは、構造と時間やアルゴリズムを全く別のものとして、分けることは不可能だからである。

リーンアーキテクチャ本では、 What system is も What system does も説明しており、夫婦で共著で書いている。 リーンアーキテクチャ本では分けることができると書いたが、実際は簡単に構造とユースケースを分けることができるわけではない。

それは、歴史が物語っている。

  • 1960年から1970年では、ノイマンコンピュータでは、データ構造は全く注目せずに、 What system does に注力され、 CPU が大事でこの性能を良くするということが重要だった。
  • 1970年後半にデータベースが登場し、ボトルネックはデータへのアクセス速度が重要となった。CPU のことは忘れ去れられ、What system is が注意の関心の的になり、データモデルが重要になった。
  • 1980年にコンピューターネットワーキングが登場し、オブジェクト同士のコミュニケーションが重要になった。こちらは What system does がまた注目された。

このように、歴史を見ると、 What system is と What system does のサイクルができている。

アーキテクチャ全般に言えることだが、例えば部屋の構造では、どこにライトがあって、どこドアがあって、それが何か(What system is)を説明することができるが、どうしてそこにある(What system does)のか、とか、部屋の中をどうやって歩くかなどのプロセスにも影響している。つまり、これらは簡単に分けることはできずに互いに関係しあっている。

リーンアーキテクチャと DDD の関係性

DDD

  • 分析に関しては扱っていない。
  • コードに関することを主に扱っている。
  • 形態(Form)の社会性は扱わない。
  • 構造(Structure)のみに注目しており、具体的な事柄から始める。

リーンアーキテクチャ

  • 人々について、そして人々のメンタルモデルを扱う。モデルに関する議論をすることを扱っているので、社会的な活動である。
  • 形態(Form)を扱う。
  • 形態(Form)からスタートして、抽象的な Commonality から Variation を適用させて正しい実装をしていくことである。

アレグザンダーは、アーキテクチャは、社会的な活動であると明記している。

対称性とパターンに関して

アレグザンダーは、空間からイベントのパターンを作成している。ソフトウェアはまだ歴史が浅いため、パターンはほぼ存在しない。建築は世紀を超えてパターンを作ってきた。ソフトウェアの世界で言われているパターンは、コンテキストの上でのロジックであって、それはパターンではない。唯一、例として上げてもらったパターンは、リーキーバケットに関するものであり、イベントのパターンを説明している。

リーキーバケット

共通性と可変性は対称性を持っているが、あるとき対称性が壊れることがある。プログラム言語では対称性を表そうとするが、実際の要件では、対称性のない複雑なものを作り上げる必要がある。リーンアーキテクチャは対称性を扱っており、負の可変性(Negative Variation)は扱っていない。ここが複雑な場所である。パターンは、What system is と what system does の両方を考える必要があり、また、空間と時間を考える必要がある。

パターンに関する議論に関しては、私(ganchiku)の中で消化しきれていない場所があり、まとめることができませんでしたので、今後の課題としてアレグザンダーや Jim Coplien さんの議論を追いかけて、自分の言葉で説明できるようになっていこうと思います。

会場を提供していただいた UUUM 株式会社、また、オーガナイズしていただきました DCI Tokyo メンバーの皆様ありがとうございました。

design ddd leanarchitecture dci mpd multiparadigm.design practical.ddd

DCI Tokyo 1 - Lean Architecture by James Coplien - が開催されました(前編)

1月10日に六本木ヒルズにて、James Coplien氏をお招きしてLean Architectureに関する勉強会を開催しました。UUUMさんに大変素敵な会場を提供頂き、スタッフ含めて40名前後の参加者が集まりました。

DCI Tokyo 1

このブログでは、当日の翻訳担当を務めた@remore@ganchikuが、当日の翻訳内容から抜け落ちた部分の捕捉を含めて、内容のアウトラインを簡単に振り返れればと思います。セッションの前半部分は@remoreが、後半は@ganchikuが解説を担当します。

なお当日のCoplien氏によるセッション内容は、許可を得た上でYouTubeにアップロードされていますので、より深くご覧になりたい方はこちらも併せてご参照下さい。


Form(形態)とLean Architecture

形態は機能に従う(Form Follows Function)という言葉があります。Googleで検索してみると、元々はアメリカの建築家ルイス・サリヴァンの言葉であると出てきます1。この言葉は建築やプロダクトデザインなど様々な領域で引用されている言葉ですが、ソフトウェア・エンジニアリングの世界でもしばしば引用されているようです。今回のセッション前半では、前回の内容を簡単におさらいしつつ、この「形態」に焦点を当てた内容が主に展開されました。

前半部分のセッションの内容について、独断と偏見で要点を書き出してみました。なるべく短く抜き出してみたつもりなのですが、各トピックの内容が濃いせいか、結果的にそれなりの長さになってしまいました:

1. Form(形態)とStructure(構造)の違い

  • Formは抽象的であり、Structureは具体的である
  • 複数のFormをよく見ていくと、パターン認識によって共通性と可変性という特徴を抽出することができる
  • ArchitectureはFormに関すること
    • Formは対称性を強調する
    • 人間の脳は共通性を見つけることに秀でている

2. 現代のプログラミング言語とOOPについて

  • 共通性と可変性のペアをパラダイムと呼ぶ
  • プログラミング言語はこれらのパラダイムのうちいくつか限られた数だけを言語の設計思想に取り入れているに過ぎない
    • 例えばOverloadingといったOOPのプログラミングテクニックはパラダイムの一つと言えるし、その他の言語が持つ機構や機能(例えばC++の場合#ifdef, Template、class, 継承といった言語が提供する機能)は共通性と可変性のペアをForm(形態)において表現しているだけとも言える
  • これを実際に実装していくとStructure(構造)の話となるが、設計において我々がどうそれを捉えているかというと、あくまでFormの形で捉えている
  • C++を作ったStroustrup氏は、C++をオブジェクト指向言語とは呼ばずマルチパラダイム言語だと呼んでいる
    • オブジェクト指向は一つのパラダイムに過ぎないのに、「オブジェクト指向分析」で全ての問題を解決しようと考えだすからおかしくなる
    • C++はオブジェクト指向以上のことが実現できる言語

3. マルチパラダイムデザインがどこで始まり、Lean Architectureがどこからきたのか

  • Lean Architectureでは2種類の視点でFormを捉えている
    • What the system is(共通性や可変性を分析する)
    • What the system does(振る舞いや"間"で見る)
  • 建築家は「形態は機能に従うか?(Does Form Follows Function?)」という点に関して各々の主張を持っている
    • 建築家アドルフ・ルースは「形態は機能に従う」と唱えた
    • ソフトウェアアーキテクトもこれを唱えがちだし、ユースケースを分析すれば最適なアーキテクチャも見つけられると考えがちであるが、これは完全に間違いである
  • 銀行口座送金の例を見ると、振る舞いを表現するために適切なものはオブジェクトIDでもクラス名でもなく、Roleであることが分かる
    • 機能の形態(Form of Function)はRoleの中にある
  • Roleという概念が一級市民として存在しており、オブジェクトがRoleを動的に演じることができるようなプログラミング言語を想像してみよう。機能の形態をコードで表現することができ、コードを読むだけでユースケースを理解できる。これがDCI。
    • ここでいう"機能の形態"は、オブジェクト間のやり取りや振る舞いといった"What the system does"の部分にあたるもののこと

前半サマリは以上となります。なお、動画の54:43頃から始まる対称性や幾何学の話を起点に展開される、現代のシステム開発におけるオブジェクト指向言語のあり方と利用のされ方に対する強い批判は、要約ではとても表現しきれる内容ではなくかつオススメの内容のため敢えて上記には含めていません。動画全てを見る時間が取れない方でも、英語と内容の両方が一部難解な部分もありますが2それでも、ぜひ一度ご覧頂くことをオススメします。

To Be Continued

更に興味がある方がいらっしゃいましたら、今回のセッションの参加ブログを書いて頂いた方もいらっしゃいますので、こちらも併せてご参照頂けると理解がより深まるかと思います。@ganchikuによるセッション後半の解説記事も追ってこちらのブログで配信予定となっております。また、Twitterでは #dcitokyo というタグで本勉強会についての過去のツイートを検索できますのでこちらもオススメです。

Jimは年内にまた来日予定があるとお話されていましたので、次回日程等決まりましたらまた告知などを行っていければと考えています。その際に、最近開設されたDCITokyoの公式Twitterアカウントから各種告知等されていく予定となっていますので、今後こちらもぜひご注目ください。


  1. 当日Jimはルイスと同年代の建築家アドルフ・ロースの言葉と紹介していたため、要出典確認 ↩︎

  2. 55:46頃にJimが解説しているオペレーショナルモデルについて当日私の方で通訳として十分に説明できなかった部分については、追ってTwitterで後追いする形で一部捕捉の説明を試みていますので、こちらもご参照ください ↩︎

design ddd multiparadigm.design leanarchitecture dci mpd practical.ddd

「Lean Architecture / DCI Evening」参加レポート

2017年10月18日、James Coplienさんとその奥様であるGertrud Bjørnvigさんをお招きして、「Lean Architecture / DCI Evening 」というイベントを開催しました。日本ではソフトウェアパターンやアジャイルのリーダーとして知られるJames Coplienさんは、『 マルチパラダイムデザイン 』(1998年)でドメインとドメイン間の関係を中心に据えた設計パラダイムを提唱していました。Coplienさんは2009年、MVCアーキテクチャの考案者である Trygve Reenskaug さんと共に「DCIアーキテクチャ」を発表しました。2010年、CoplienさんはGertrudさんとともに書籍『 Lean Architecture 』を上梓、トヨタ生産方式をソフトウェアアーキテクチャに適用するリーンアーキテクチャについて、DCIアーキテクチャをそのビルディングブロックと位置づけた具体的な方法を記述しました。今回、マルチパラダイムデザインの読書会を大阪で開催していた縁から、表題のイベントを開催することができました。この記事ではセッションの内容についてトークを再現する形でお伝えしたいと思います。

NOTE: (※)内は筆者によるコメントです。また、基本的に英語から修正せずに翻訳しているので、事実関係に関しては確認がとれていないものもあります。

CoplienさんによるDCIアーキテクチャの概説

Coplienさん(以下、C): 最初にクイズをします。みんな立って。DCIは、DCIと呼ぶ前はなんと呼んでいたか知っていますか?・・・DCA (Data-Collabolation-Algorithm)(※関連リンクを参照)です。知ってた人はそのまま立って。これを差し上げます。(※参加者の一人がサイン付きのLean Architectureをプレゼントされる)

C: (※DCIで開発するための言語基盤とIDEを提供する)Trygveのプログラムの拡張子は「.k」になっているのはなぜでしょう?これは哲学者のカントからとっている。

C: オブジェクト指向プログラミングができない、モダンな言語はなんでしょう?Rust?Java。OK、それだ。Javaはオブジェクト指向ではなく、クラス指向の言語だ。

C: DCIのWebサイトfulloo.infoにいろんなリソースがあります。サンプルコードとかビデオとかも入っている。

C: 今日はインフォーマルな会です。小さいDCIのチュートリアルをすることもできるし、私が最近どういう研究をやっているかを話すこともできる。DCIが実際にどうやってプログラマーの集中力を高めてエラーを取り除くことができるかということを話してもいいし、Trygve言語の実際の例もお見せすることができる。見た目はJavaみたいだけどね。そこには、コンテキストとロールも登場する。設計の話もしてもいいし、ドメイン分析の話をしてもいい。

参加者A: ドメイン分析とTrygve IDEも見たい。私たちは大阪で「マルチパラダイムデザイン」の読書会をしていました。MPDとDCIをどのように繋ぐかの話も聞きたい。

C: 今日は何人、大阪から来たの?Oh! DCIを殆ど知らないという人は?最初に小さいインタラクションをしましょうか。15分くらい。

C: マルチパラダイムデザインは、クラスベースの言語に基礎を置いて議論を展開している。DCIはリアルなビルディングブロック(構成要素)とオブジェクト群のネットワークです。そこが大きな違いです。ドメイン分析とかユースケース、ロールのアクターに関しては後ほど妻が説明してくれる。

C: まずはDCIの触り。簡単なデモを。銀行口座間の送金のケースを考えてみる。ソフトウェア上ではそれはどういう状態でしょうか。クラスを書いて、オブジェクト同士をインタラクションさせますね。クラスを書くときというのは、クラスの全てのメソッドを考慮して書く必要がある。

C: ATMの例を考えてみましょう。ATMが稼働していて、それを確認するために、コンソールにオブジェクトIDをプリントするプログラムを考えてみましょう。

(※メソッドが呼ばれるたびにオブジェクトIDがプリントされていくデモ)

C: これを見て何かパターンは見つかるでしょうか? 単に同じオブジェクトが延々と呼ばれていることがわかるだけですね!(※会場笑)呼ばれるオブジェクトの順番とかは全然重要ではなかったわけです。

C: 次にオブジェクトIDではなく、クラス名が表示されるようにしてみましょう。クラス名は重要ですよね。コンソールを見てパターンは見つかりますか?ないですね。これもまだ完璧ではないようです。クラスのうち、2つは口座に関するもの、最後の3つ目は通貨に関するものだということはわかります。しかし、アーキテクチャの振る舞いはクラスだけでは表現されていないようです。

C: さて、今度はメソッドが呼ばれる毎に、役割(※ロール)がプリントされるようにしてみますよ。・・・Ah ha!(※会場笑) 分かりましたね。お金を送金するときの構造はこういうふうになりますよね。構造は役割(※ロール)の中に入っています。

C: 1つの銀行に2つ以上の口座を持っている人はいますか?(※会場から手が)それでは、私に1つの自分の口座から他の口座に送金するユーザーストーリーについて説明して下さい。

参加者B: ある口座からある口座に送金します。妻にダイアモンドを買うために。貯蓄口座から普通口座へ。

C: もっと一般的に言うと?

参加者B: 振込元から振込先へ送金する。

C: 振込元口座を持っている人はいますか?それは、オブジェクトでしょうか?クラスでしょうか?なんでしょうかね、振込元口座みたいな役割とは?・・・人はこれを役割(ロール)として考えているわけですよね。クラスやオブジェクトとして役割(ロール)を考えてはいないですよね。

C: アラン・ケイの最初に考えていたオブジェクト指向というのはヒューマンメンタルモデルをコンピュータに適用させたものでした。そのメンタルモデルを今発見したのです!

参加者B: どうやって、口座の振込元とか振込先とかを判別するんでしょうか?

C: ピアジェという発達心理学者がいます。ピアジェに「オペレーショナルモデル理論」というのがあります。ビルディングブロック(※構成要素)を使って論理的に考える必要があります。オブジェクトやクラスではないものがあるのです。おそらく私たちは最初にオブジェクトを学び、その後にクラス・クラス化を学ぶようです。子どもが一番最初にオブジェクトを学ぶ時は、全てを自分自身として認識するようです、母親さえも自分だと思う。4-7ヶ月の間に区別をつけることができるようになっていく。小さな子どもは馬を見た時に、馬を指差して「犬!」と叫ぶ。(※あ〜!と会場共感の声)こういった認識は物事に共通性を見出していることによって成り立っています。私たちはプログラムを問題解決のために書いています。子どもは8ヶ月くらいの時に原因と結果という因果関係について学ぶ。その頃から机を押したり、上にあるものを叩いたりして何かが落ちたりするのを楽しむ。そうして最終的には、問題解決する能力を学習していく。それがオペレーショナルモデルと呼ばれるものです。

C: プログラムを通して問題解決するためにこの考えが必要ですね。プログラムを書くときにはどうやっているのでしょう?ここにこのようにオブジェクトが散らばっている。そしてオブジェクトをつなげるユースケースがある。これはまた別のユースケースですね。これはまた別のユースケース、また別のユースケース・・・(※会場笑)ユースケースはオブジェクトのシーケンスではないんですよね、こう見ると。順番があるわけではない。

C: では、ユースケースのコードをオブジェクトよりも上のレイヤーに取り出す、ということをやってみましょう。今、ユースケースのコードは、オブジェクトのクラスの中にあります。このユースケースの部分を、クラスの外側に出すのです。そうすると、オブジェクトの方は基本的なクラスのインスタンスのままなので、とても単純です。しかし、これらは実際のところユースケースの一部にはなりません。では、ユースケースの部品は何でしょうか? ロールですね。ユースケースの部品は、ロールの中にあります。開発者は、このようなロールの中からいくつかを選んでまとめます。このまとまりを、コンテキストと呼びます。 つまり、コンテキストがユースケースに相当します。そして、ユースケースを実行するには、サブジェクト(※ユースケースの入り口となるエンティティ、後述のサンプルコードのTransferMoneyContext.SOURCE_ACCOUNTに代入されるオブジェクトにあたる)にこれらのメソッド(※ロールのメソッド)をインジェクトします。このオブジェクト(※他のエンティティ、後述のサンプルコードのTransferMoneyContext.DESTINATION_ACCOUNTに代入されるオブジェクトにあたる)にもこれらのメソッドをインジェクトします。

C: オブジェクトは既にあるからユースケースを書きたい。例えばヘリコプターを飛ばすことについて考えてみましょう。オペレーショナルモデルで思考して、ロールを考えます。映画の「マトリックス」を見たことがあるますか?トリニティはヘリコプターを飛ばさなければいけなかったですよね。ヘリコプターを操縦するプログラムをダウンロードしていました。ロールのプログラムをダウンロードすることによってヘリコプターを飛ばすことができたわけです。ランタイムでオブジェクトに対してどのようにユースケース上で振る舞うのかを教えてあげる。ユースケースが終わると、オブジェクトは何をやるか、何をやったかを(※どのように振る舞うかを)忘れてしまうわけです。

参加者B: トリニティはオブジェクト(※エンティティ)、パイロットはロールということですね。

C: トリニティは、ヘリコプターを操縦するためのメソッドを持っている。手を動かしたり、足を動かしたりといった基本的なトリニティが持っているメソッドを使うことができる。コンテキストがヘリコプターを運転させる。トリニティは基本的な動作はできる。コンテキストとトリニティの間のメソッドの契約というものが必要で、トリニティは人なので人が持っているメソッドだけが使えるという契約がある。手を動かすとか。ヘリコプターを操縦するというのははコンテキストです。ロールはデータを持たない。純粋にメソッドを持っている。以上がDCIの基本的な話でした。

参加者C: ロールとクラスの違いがまだわからない。

C: クラスはデータを持ちます。あなた達はいわば人間クラスのインスタンスですよね?そこから初期化されている。(※会場笑)ヘリコプターパイロットはロールなので、ヘリコプターパイロットのインスタンスを作ることはできない。人間が必要なんです。あるいはロボット?もしくはゴリラ。

参加者C: Javaのインタフェースはロールに近い?

C: 似てるとは言えるけど、実装がないからロールを完全に表すかというと違うと思います。(※Java 8以降はインタフェースは実装を持てるようになりましたが)あと、Javaはインタフェースは静的に決まっていて、動的に決めることができないというところが異なります。クラスがインタフェースをextendするというのを全てコンパイル時に決めなければなりません。

参加者D: Swiftのプロトコルエクステンションを使うと近いことができますか?

C: Objective-Cでお腹いっぱいなので、Swiftの学習は避けているものでね(※会場笑)プロトコルエクステンションを使うと近いことができるようです。

参加者C: クラスとロールの違いについて。Java クラスとインタフェース、Scala クラスとトレイト、C++ 動的クラス、Virtualクラス これらはDCIの実装として使うことができるか。

C: Scalaのトレイトはかなり近いです。Scalaではトレイトのミックスインを使う。トレイトを使ってクラスに合成できる。その結果クラスは両方の機能のメソッドを持つことができる。C++ではどうするか?C++の場合はクラスの合成に継承を使ってしまうので難しい。どうするか?ロールをテンプレートを使って書くことができる。テンプレートを使えばDCIを実現することは可能です。ただそれはコンパイルのタイミングになってしまう。ダイナミックではない。fulloo.infoも見てくださいね。なんとなく、DCIのイメージが掴めましたか?

C: DCIのための言語として気に入ってるものの一つはRubyです。オブジェクトの拡張が簡単にできる。モジュールを使ってミックスインすることもできます。ただ、メソッドは自由に追加することはできるが、それを取り外すのが難しい。名前の衝突の問題もある。Matzはこの問題について理解してくれたが、一度インジェクトしたものを引き剥がす機能を入れることまでは、説得するには至らなかった。Rubyの仮想マシンを変更すればできるのですがね。

(※実は筆者はRubyistなのですが、refinementを使うとある程度は目的に適うのではないのかなーと思ってしまいました)

C: Trygveを使ってる人。(※一人だけ手が挙がる)オープンソースですよ!(※会場笑) お金の送金のサンプルを見せていきます。ここにクラスがあります。これはドメイン分析から抽出されたものです。 amount()(金額) などのメソッドを持っている。クラス指向のプログラミングならこんなものでしょうか?Trygveの紹介をしていきますね。

(※以下のコードはTrygve で書かれた送金のサンプル https://github.com/jcoplien/trygve/blob/master/examples/july_money_transfer.k より引用)

/*
* july_money_transfer.k
*/

class Currency
{
    public Currency(double amount) {
        amount_ = amount.clone
    }
    public Currency +(Currency amount) {
        assert(false)
        return this;
    }
    public Currency -(Currency amount) {
        assert(false)
        return this;
    }
    public Currency() {
    }
    public String name() const
    {
        assert(false);
        return ""
    }
    public String sign() const
    {
        assert(false);
        return ""
    }
    public double amountInEuro() const {
        assert(false);
        return 0.0
    }
    public double amount() const {
        return amount_
    }
    public String toString() const {
       return amountInEuro().toString()
    }
    public int compareTo(Currency other) {
       if (amount() > other.amount()) return 1
       else if (amount() < other.amount()) return -1;
       return 0
    }

    private double amount_
}

class Euro extends Currency {
    public Euro(double amount) {
        Currency(amount)
    }
    public Euro -(Currency amount) {
        return new Euro(amount() - amount.amountInEuro())
    }
    public Euro +(Currency amount) {
        return new Euro(amount() + amount.amountInEuro())
    }
    public String name() const
    {
        return "Euro";
    }
    public String sign() const
    {
        return "€";
    }
    public double amountInEuro() const
    {
        return amount()
    }
    public String toString() const {
        return amount().toString()
    }
}

class Account
{
    public Account(int acctno) {
       acct_ = acctno
    }
    public String accountID() const {
       return acct_.toString()
    }
    public Currency availableBalance() const { assert(false); return null }
    public void increaseBalance(Currency amount) { assert(false) }
    public void decreaseBalance(Currency amount) { assert(false) }
    public void updateLog(String message, Date dt, Currency amount) {
        assert(false)
    }

    private int acct_
}

class CheckingAccount extends Account {
    public CheckingAccount() {
            Account(1234);
            availableBalance_ = new Euro(100.00)
    }
    public Currency availableBalance() const
    {
        return availableBalance_
    }
    public void decreaseBalance(Currency c) {
        availableBalance_ = availableBalance_ -  c
    }
    public void updateLog(String message, Date t, Currency c) const {
        System.out.print("account: ").print(accountID())
                  .print(" CheckingAccount::updateLog(\"").print(message)
                  .print("\", ").print(t.toString()).print(", ")
                  .print(c.toString()).print(")")
                  .println()
    }
    public void increaseBalance(Currency c) {
        availableBalance_ = availableBalance_ + c
    }

    private Currency availableBalance_
}

class SavingsAccount extends Account {
    public SavingsAccount() {
            Account(1234);
            availableBalance_ = new Euro(0.00)
    }
    public Currency availableBalance() const {
        return availableBalance_
    }
    public void decreaseBalance(Currency c) {
        assert(c > availableBalance_);
        availableBalance_ = availableBalance_ - c
    }

    public void updateLog(String logMessage, Date timeOfTransaction,
                               Currency amountForTransaction) const {
        assert(logMessage.length() > 0);
        assert(logMessage.length() < MAX_BUFFER_SIZE);
        // assert(new Date() < timeOfTransaction);
        System.out.print("account: ").print(accountID())
                  .print(" SavingsAccount::updateLog(\"").print(logMessage)
                  .print("\", ").print(timeOfTransaction.toString())
                  .print(", ").print(amountForTransaction.toString())
                  .print(")").println()
    }
    public void increaseBalance(Currency c) {
        availableBalance_ = availableBalance_ + c
    }

    private Currency availableBalance_;
    private int  MAX_BUFFER_SIZE = 256
}

class InvestmentAccount extends Account
{
    public InvestmentAccount() {
            Account(1234);
            availableBalance_ = new Euro(0.00)
    }
    public Currency availableBalance() const {
        return availableBalance_
    }
    public void increaseBalance(Currency c) {
        availableBalance_ =  availableBalance_ + c
    }
    public void decreaseBalance(Currency c) {
        availableBalance_ = availableBalance_ - c;
    }
    public void updateLog(String s, Date t, Currency c) const {
       System.out.print("account: ").print(accountID())
                 .print(" InvestmentAccount::updateLog(\"")
                 .print(s).print("\", ").print(t.toString())
                 .print(", ").print(c.toString()).print(")")
                 .println()
    }

    private Currency availableBalance_;
}

class Creditor
{
    public Creditor(Account account) {
            account_ = account
    }
    public Account account() { return account_ }
    public Currency amountOwed() const { return new Currency(0.0) }

    private Account account_
}

class ElectricCompany extends Creditor
{
    public ElectricCompany() {
        Creditor(new CheckingAccount())
    }
    public Currency amountOwed() const {
        return new Euro(15.0)
    }
}

class GasCompany extends Creditor
{
    public GasCompany() {
        Creditor( new SavingsAccount());
        account().increaseBalance(new Euro(500.00))    // start off with a balance of 500
    }
    public Currency amountOwed() const {
        return new Euro(18.76)
    }
}

context TransferMoneyContext
{
    // Roles

    role AMOUNT {
        public Currency(double amount);
        public Currency +(Currency amount);
        public Currency -(Currency amount);
        public String name() const;
        public String sign() const;
        public double amountInEuro() const;
        public double amount() const;
        public String toString() const;
        public int compareTo(Currency other)
    } requires {
        Currency(double amount);
        Currency +(Currency amount);
        Currency -(Currency amount);
        String name() const;
        String sign() const;
        double amountInEuro() const;
        double amount() const;
        String toString() const;
        int compareTo(Currency other)
    }

    role GUI
    {
        public void displayScreen(int displayCode)
    } requires {
        void displayScreen(int displayCode)
    }

    role SOURCE_ACCOUNT {
        public void transferTo() {
            // This code is reviewable and meaningfully testable with stubs!
            int SUCCESS_DEPOSIT_SCREEN = 10;

            // beginTransaction();

            if (this.availableBalance() < AMOUNT) {
                // endTransaction();
                assert(false, "Unavailable balance")
            } else {
                this.decreaseBalance(AMOUNT);
                DESTINATION_ACCOUNT.increaseBalance(AMOUNT);
                this.updateLog("Transfer Out", new Date(), AMOUNT);
                DESTINATION_ACCOUNT.updateLog("Transfer In", new Date(), AMOUNT);
            }
            // GUI.displayScreen(SUCCESS_DEPOSIT_SCREEN);
            // endTransaction()
        }
    } requires {
        void decreaseBalance(Currency amount);
        Currency availableBalance() const;
        void updateLog(String msg, Date time, Currency amount)
    }

    role DESTINATION_ACCOUNT {
        public void transferFrom() {
            this.increaseBalance(AMOUNT);
            this.updateLog("Transfer in", new Date(), AMOUNT);
        }
        public void increaseBalance(Currency amount);
        public void updateLog(String msg, Date time, Currency amount)
    } requires {
        void increaseBalance(Currency amount);
        void updateLog(String msg, Date time, Currency amount)
    }

    public TransferMoneyContext(Currency amount, Account source, Account destination)
    {
        SOURCE_ACCOUNT = source;
        DESTINATION_ACCOUNT = destination;
        AMOUNT = amount
    }
    public TransferMoneyContext() {
        lookupBindings()
    }
    public void doit()
    {
        SOURCE_ACCOUNT.transferTo()
    }
    private void lookupBindings() {
        // These are somewhat arbitrary and for illustrative
        // purposes. The simulate a database lookup
        InvestmentAccount investmentAccount = new InvestmentAccount();
        investmentAccount.increaseBalance(new Euro(100.00)); // prime it with some money

        SOURCE_ACCOUNT = investmentAccount;
        DESTINATION_ACCOUNT = new SavingsAccount();
        DESTINATION_ACCOUNT.increaseBalance(new Euro(500.00)); // start it off with money
        AMOUNT = new Euro(30.00)
    }
}

context PayBillsContext
{
    public PayBillsContext() {
       lookupBindings
    }
    role [] CREDITORS {
    } requires {
       Currency amountOwed()
    }
    stageprop SOURCE_ACCOUNT {
        public String accountID() const;
        public Currency availableBalance() const;
        public void increaseBalance(Currency amount) unused;
        public void decreaseBalance(Currency amount) unused;
        public void updateLog(String message, Date dt, Currency amount) unused
    } requires {
        String accountID() const;
        Currency availableBalance() const;
        void increaseBalance(Currency amount);
        void decreaseBalance(Currency amount);
        void updateLog(String message, Date dt, Currency amount)
    }

    // Use case behaviours

    public void doit()  {
        for (Creditor credit : CREDITORS) {
            // Note that here we invoke another Use Case
            TransferMoneyContext xfer = new TransferMoneyContext(
                                                      credit.amountOwed(),
                                                      SOURCE_ACCOUNT,
                                                      credit.account());
            xfer.doit()
        }
    }

    private void lookupBindings() {
       // These are somewhat arbitrary and for illustrative
       // purposes. The simulate a database lookup
       InvestmentAccount investmentAccount = new InvestmentAccount();
       investmentAccount.increaseBalance(new Euro(100.00)); // prime it with some money
       SOURCE_ACCOUNT = investmentAccount;

       Creditor [] creditors = new Creditor [2];

       creditors[0] = new ElectricCompany();
       creditors[1] = new GasCompany();

       CREDITORS = creditors
    }
}


{
    // Main

    TransferMoneyContext  aNewUseCase = new TransferMoneyContext();
    aNewUseCase.doit();

    PayBillsContext anotherNewUseCase = new PayBillsContext();
    anotherNewUseCase.doit()
}

C: Account(口座) という別のクラスがあります。ドメイン分析から抽出されたものです。口座番号というデータを持っている。メソッドは increaseBalance()(残高増) と decreaseBalance()(残高減) 。口座番号を与えれば、このメソッドを使っていろんなことができる。いわば、ちょっと高級な Integer みたいなものです・・・。全くもって良くないですね!貯金用の口座とか、投資用の口座とかどんどん増えていってしまいます。会社の口座、お金の債権者… ここまでができの良くないクラス指向のプログラミングのやり方でした。

C: さて、それでは送金のユースケースについて見ていきましょう。Context がキーワードです。多くの点でクラスのように見えるのだけれど、クラスとは異なっていて、ほとんどの場合データを持たずにロールだけを持っている。 Amount(金額) と呼ばれるロールはとてもシンプルなロールです。これで資金移動をすることができる。

C: (※デモを見せて)これがロールのインタフェースのシグネチャです。オブジェクトがこれらのメソッド全てをサポートするように定義する必要があります。これを「必須の契約」(※the requires contract)と言います。オブジェクトがロールを表現する場合、これらのメソッドを全てサポートしなければならない。

参加者C: ここで言う契約とは、「契約による設計」の契約のことですか?

C: その通りです。バートランド・メイヤーという人が提唱した契約による設計。この人の言ってることは、だいたい合っているけど突き詰められていない。

参加者B: メイヤーの考えのどこが機能しないんでしょう?

C: クラスAがあり、そこにメソッド1があります。そこには事前条件、メソッド実行、事後条件があります。メソッド1は事前条件が真であることを要求し、事後条件が保証されていることを約束します。次にメソッド1の中からメソッド2を呼ぶケースを考えてみましょう。メソッド2にとっての事前条件を見た時、メソッド2にとってはメソッド1が状態を維持してくれているという想定をしているわけですが、メソッド1の実行中に自己隠蔽されている状態の中で条件がおかしくなった状態でメソッド2を呼ぶとメソッド2が結果保証している事後条件が壊れてしまうことがある。

C: クラス側で本体となる実装を持たないのは、ロールのプレイヤーの持っている実装を使うだけだからです。ロールを演じるオブジェクトの実装を使います。別のオブジェクトに由来するメソッドを使うわけです。これらのメソッドはパブリックインタフェースで、ロールが何をするかを知ることができます。

C: よりわかり易い例として SourceAccount(振込元口座) と呼ばれる別のロールも見ていきましょう。SourceAccounttransferTo()(〜に送金する) として呼ばれるメソッド、責務を持っています。今からこの箇所のコメントを外して動くようにしましょう。トランザクションを開始しますよ。もし振込元口座が利用可能な残高を持っていれば、コメントを外した箇所は何になるでしょうか?

参加者B: ロールを注入されたオブジェクト

C: そうです! SourceAccount ロールを適切に演じるオブジェクトです。利用可能な残高はどこでしょうか?気をつけてください。ここ(※ロール)にはありませんよ。残高は、ロールを演じる口座オブジェクトの側、ロールプレイヤーにあります。

C: クラスみたいだけれど、ランタイムで考えられるようにしてくれます。SourceAccount ロールを演じるオブジェクトについて考えてほしいです。ロールプレイヤーはマトリックス(※コンテキストが持つロールとオブジェクトのマッピング表)によってもたらされるものです。ロールがオブジェクトに注入されると、オブジェクトが機能を持つようになって、送金ユースケースを実現できるようになります。送金メソッドは振込元の Account オブジェクトに注入されます。

C: こちらは別のロール、DestinationAccount(振込先口座) です。DestinationAccountはメソッド transferFrom()(〜から送金される) を持ちます。transferFrom() メソッドは DestinationAccount の残高を更新します。

C: コンテキストのコンストラクタを見てみましょう。TransferMoneyContext(送金コンテキスト) です。コンテキストの中で何がオブジェクトかを伝える必要あります。送金の金額、振込元、振込先、口座の種類。

C: マトリックス(※コンテキストが持つロールとオブジェクトのマッピング表)を表すオブジェクトを持っていて、ロールのメソッドが振込元の Account オブジェクトに注入されます。要はこれ(※コンテキスト)はオブジェクトだと言うことです。Account オブジェクトに対して振込元口座にあるべきメソッド群を与えていきます。送金ユースケースのためにロールプレイヤーが演じるメソッド群です。そうすれば、ランタイムでオブジェクトを組み立てることができます。

C: 他のユースケース、お金を支払うについて見ていきましょう。特別なロールがあります。そして、AmountSavingsAccount(貯蓄口座) のようなオブジェクトが新たに一つここにあります。これらは、プラグアンドプレイをするために用意したアクターです。私が台本を書いたロールがあり、アクターは芝居においてロールを演じます。

C: これは舞台のようなもので、大道具(stage props)はアクターの持っている道具です。大道具だけでは、一切何もできません。ただデータを引き出すことはできます。

参加者B補足: 何かをすることはできないのだけど、データを引き出すことはできる。

C: このロールの全てのメソッドは const で、アクターの状態を変更することはできません。

参加者B: const というのはロールの型かなにかですか?

C: そうです。ロールの束縛、制約の種類です。

C: さて、stage props (大道具に由来する機構名) の使いどころはわかりますか?オブジェクトは2つのロールを一度に演じるかもしれません。同じオブジェクト、同じインスタンスで二役を一度にこなすことがあります。とても複雑な例として再帰実行になるケースもありえます。これが先程上がっていた問題の一つで、複数回呼び出したら事前条件が崩れることがあるという例です。このstage propsはデータを更新しないから、問題が起こらないようにすることができます。

C: ちょっと難し目の例を見てみましょう。Javaのグラフ描画のデータ構造です。ブロック崩しと言われるピンポンゲームです。変数の中をチェックするデバッガも入っています。700行くらいのプログラムでこれができます。

C: MPDからDCIへどう変遷していくかについて。よく知られているように、対象のドメインを理解することから始めます。殆どの場合、インタラクティブなプログラムはクラス指向のプログラムでできています。オブジェクト指向プログラミングとは、アラン・ケイが定義したように、オブジェクト同士の協調によるネットワークです。これをどうやって意識するかについて。

GertrudさんとCoplienさんによるユーザーストーリー、アジャイルとリーンアーキテクチャ

Gertrud さん(以下、G): 皆さんはユーザーストーリーは知っていますか?ユースケースは?アジャイルになるためにはユーザーストーリーをたくさん作りましょう。リーンになるためには、ユースケースが必要。

G: ユーザーストーリーの形式(フォーム)として、以下のようなフォーマットで考えるといいです。

As a <USER or ROLE> 例: 口座を持っている人として
I want <FEATURE>    例: ある口座から別の口座に移したいという行為
So I can <MOTIVATION>   そうすればなにができるか。例えば、ダイアモンドを買ってあげることができる。

ユーザーストーリーの構成要素はこのようになっています。これをブレインストーミングなどでたくさん作っていく。 上から、Who、What、Why に対応している。

G: ユーザーロールには、口座を持っている人、銀行の従業員、(※例えば電気代を支払おうとする)市民などがいます。

G: お金の送金は、今回の場合、全てのロールで行うケースがある。しかし、それぞれのロールでやりたい事は違う。ロールやモチベーションによってコンテキストが変わってくる。実装する前に、モチベーションがわかるので、実装することができる。これは実装する前に知る必要があるんです。

G: ユーザーストーリーの洗い出しは、ブレインストーミングであげていく。するとかなり数が増えていく。そこから一般的な(※抽象度の高い)状態に近づけていく。そうすれば、ユーザーストーリーからユースケースに落とし込むことができる。

G: ユーザーストーリーは具体的で、抽象的なのがユースケース。どこまで抽象的にするべきか?コードで理解できるレベルまでです。ユースケースをリーダブルコードに落とし込めるところまで持っていくのが目標になる。

G: ユーザーストーリーはインプットなので、なるべくたくさんの実例を出したほうがいい。銀行の業務であれば、昔に使っていた小切手の話をユーザーストーリーとして抽出するくらい。

G: ユースケースの段階で要点を厳密に抽出する。これがそのままプログラムに使われるから。

(ここで休憩に入りました)

G: ユースケースで大事なことはターミノロジー(※用語)。システム分析とシステム設計で同じボキャブラリー、ターミノロジーを使ってお互いの同意を取っていくことが大事。関係者の間で合意をとるのも大事になってくる。

G: 分析と設計で、同じボキャブラリー、同じターミノロジーを使って話をするので、相互に一貫性をもたせることができる。もちろん違うチームに行くと言葉も変わっていく。

G: 銀行だったら銀行員にも話を聞かないといけない。ドメインエキスパートだけではなく、銀行員もチームに入って、彼らの使っている用語をコードに落とし込んでいく。ユースケースレベルになると、コードのステップレベルまで落とし込めるようになる。

G: コアシナリオとサテライトシナリオというのがある。コアシナリオはシナリオの中でも本道で大事な物。サテライトシナリオは、例外的だったり拡張されていたり複雑だったりするもの。それでも、システムはこういうものもカバーしなければならない。

参加者: なぜ、「サテライト」という用語にしたか?

G: オリジナルの用語は、インクルードとエクスクルードだった。その使い分けはわかりにくいので、コアシナリオとサテライトシナリオにした。また、デビエイション(※deviation: 逸脱、脱線、偏向)と一時期は呼んでいた。コアシナリオじゃないなにか、そんな感じ。

G: ユースケースという用語は昔ながらの用語で古臭い感じがするので使わない方向でというのはある

G: 感覚的にはコアシナリオがスコープ全体の80%、残りはサテライトシナリオという割合になる。

G: サテライトシナリオのほうがアジャイルっぽいやり方で進めていける。そもそも複雑だったり、変わりやすかったりするので。

G: 具体的なユーザーストーリーをユースケースに落としていく時に、どこまで一般化するのか。例えば、銀行で資金移動する時に使う一般的な用語は送金するという用語とか、そういうことを考えてちょうどいい落とし所を見つけていく。

C: 銀行口座の残高がマイナスになった時に、自動的に所有者の他の口座から振り替えてマイナスを補填するような、システム起動のユーザーが関与しない形でのシステムオペレーションを考える。MVCはエンドユーザーから見た、それに基づいたプログラミングモデル。DCIはプログラマーサイドのユーザーモデルを表現している。先程の、ユーザーがでてこないシステムオペレーションのようなシステムオペレーションも自然に扱うことができる。

G: ターミノロジーがとても大事。ターミノロジーのデータベースを会社が持っていて、緻密に定義した辞書を持っている。そう、会社に辞書がある。プログラムの中の変数やファイル名なんかもそのデータベースを見て決める。 辞書に用語がなければ、辞書に追加したり変更したりする。追加するときにはちゃんと申請しなければいけない。

G: ソースコードの中にはコメントを書いてはいけない。さきほどの辞書を使って、ソースコードが、例えば英語からドイツ語などの自然言語にそのまま翻訳されるので。ソースコード中にコメントがあるとその内容をいちいち翻訳しなければならなくなる。

G: 最初に生まれた子供と同じくらいの注意深さで名前をつけよう。

C: ポリモーフィズムがないからコードが読みやすいのかもしれない。ポリモーフィズムがあると、メソッドがどこから呼び出されるかわからないからプログラムの見通しが悪くなる。ボキャブラリーも大事だが構造も大事。ある研究では、ユースケースのいろんな箇所にコードを分散させるとエラー率が70%増えるという調査結果がある。

C: リーンアーキテクチャの良いところとして、エラーを早く見つけることができることがある。テストをしているときではなく、コードを読んでいる時にエラーに気づくことができる。

C: フォードは組み立てを実際にして、テストは最後に確認している。トヨタの場合は、組立時にその場でテストをしている。リーンのアプローチの1つとして、問題をより早く見つけるという利点がある。

C: アジャイルとリーンについて。リーンはクラスの構造が重要。リーンにコストをかけて、アジャイルで実際にどう稼いで行くかを考える。アジャイルはオブジェクトのインタラクションが重要。

C: ドメイン分析とユースケース分析も同時並行的にやる。同時並行的にやると重なる場所がある。みんなそれぞれ同時に実行していく。

C: 野中先生がトヨタにいた時に気づいた。それぞれの分析がかさなりあって進んでいく。これを、刺し身モデルという。(※ラグビーの)スクラムも刺し身。挟まり合っているでしょ。ウォーターフォールとは違って皆が全てのことをやるからスクラム。

C: リーンは長い期間のプランニングが必要で大事になってくる。アジャイルはフィードバックに応じた再調整が肝要。リーンは、深いレベルの専門家が作る。アジャイルはなんでもありみたいなところがある。スクラムとDCIはそのふたつのことをやる。

C: パターンも同じように両方のものを含む。これは日本の禅から来ていると思います。パターン系のガイダンスに従って、門を通ってゴールに近づくことはできるのではないか?パターンもスクラムも、ある程度のレベルではDCIも、ほとんどは日本由来のもの。道教だよね、これ。

C: リーン側のほうは、日本のトヨタの思想。山田 ひろしと言う有名なエンジニアでホンダの人がトヨタに教えた。(※原典見つけられず)

C: アジャイルのほうは、日本でよく知られた比喩で伝えるのは難しいですが… 社会的構造が違うので。デンマークは、社会がとてもフラット。日本は階層的なところがある。よりシステム化されているので、色々なルールが存在する。日本は、もうちょっとアジャイルのほうをエクササイズするといいかもしれません。

C: 実際の開発の流れ。ドメイン分析 → 共通性可変性分析 → クラス → インタフェース、API設計。最初はインタフェースしか書かない。もしかしたらスタブくらいは書くかもしれない。実装はいつ書くか?ユースケースのタイミングで。Just In Time。ユースケースが必要とするインタフェースだけを実装する。(※インターフェイスをクラスで実装するのではなく、インターフェイス自体を実装する)インタフェースは抽象的な概念で、ロールは具体的なインタラクションやアルゴリズムなので、そのタイミングで。

C: この中でプラットフォームを開発している人。プラットフォーム開発はリーンではない。あらかじめ用意しておくもの。だからリーンではない。使わないものを作る時は会社が危なくなる。これは、無駄と言います。

C: DCIアーキテクチャを利用した開発の順番としては、まずアーキテクチャを作ってそれからそれぞれデータベースとかGUIのインタフェースを作る。会社としてなにを売るかというのはいろいろあるけど、売るのはユースケース。それぞれのクラスを売っているわけではない。ユースケースは、それぞれのデータベースとかサーバーとかクライアントのユーザーインタフェースなどを少しずつ重ねたもの。

C: 生産性が一番の無駄を生むと、トヨタの大野耐一さんがおっしゃっている。

C: こういう話を、「object-composition」というメーリングリストで話をしているので、入ってくださいね。

C: 1月にまた東京に来るので、またこういう会を開催しましょう!

最後に、GertrudさんとCoplienさんと参加メンバーで集合写真を撮りました。

image

NOTE: 当日は同時翻訳付きの英語トークでした。通訳の任を快諾してくださった、 @ganchiku さん、@remore さん、ありがとうございました。また、記事を書くにあたっては英語の下訳で @kuma_nana さんに貢献していただきました。こちらもありがとうございます。もちろん、当ポストの文責は執筆者である私にあります。

関連リンク

関連記事

design ddd multiparadigm.design practical.ddd mpd dci leanarchitecture

「現場で役立つシステム設計の原則」批判 (2) ~ポリモーフィズムは何のために?~

増田亨氏の「現場で役立つシステム設計の原則]」批判の第2編です。

(2)ポリモーフィズムは何のために?

オブジェクト指向の要件

本書には「変更を楽で安全にするオブジェクト指向の実践技法」というサブタイトルが付与されています。オブジェクト指向が何かという点については、論者によって違いはあるものの、以下の3つが要件とされることには、多くの人が合意するでしょう。

  • カプセル化
  • インヘリタンス(継承)
  • ポリモーフィズム(多態)

前回で明らかになったように、カプセル化は、オブジェクト指向とは独立にモジュール分割の指導原理としてデイビッド・パーナスにより提唱された「情報隠蔽」を敷衍したものです。オブジェクト指向の要件ではありますが、オブジェクト指向固有のアイデアではありません。

インヘリタンスは便利な機能ですが、コンポジションや移譲により代替できるので、オブジェクト指向の本質的な要件とは見做されなくなってきたかと思います。

となれば、プログラミングの歴史にオブジェクト指向が付け加えたものは、ポリモーフィズムであるということになります。オブジェクト指向のもっとも偉大な貢献は、イヌがワンと鳴き、ネコがニャーと鳴くことです!

さて、こうした視点から本書を通読すると、まず、カプセル化については、その手段に過ぎない「データと処理を一体にする」という点に囚われ過ぎて、目的である「情報隠蔽」を取り逃がしているということが言えるかと思います。これが前回の指摘でした。

本書でのポリモーフィズム

では、本書ではポリモーフィズムはどのように扱われているでしょうか。

本書でポリモーフィズムを明示的に扱っているのは、「Chapter2 場合分けのロジックを整理する」だけです。この章でのポリモーフィズムの用途は、区分値に関する条件分岐(if文/switch文)を排除することです。

ポリモーフィズムのこうした利用方法は、マーティン・ファウラーのリファクタリング本で紹介され知られるようになったかと思いますが、便利でもあり、広く使われているものでもあると思います。

ただ、こうした「区分値ポリモーフィズム」は、ポリモーフィズムを適用可能なユースケース全体の中で、かなり周辺的なものであるように私には感じられます。その理由について以下ご説明します。

ポリモーフィズム ― 典型的な用法

その前にまず、ポリモーフィズムの典型的な用法とはどのようなものでしょうか。前回ご提示した、受注(SalesOrder)における値引き計算を例にご説明しましょう:

class SalesOrder {
  Money unitPrice;
  Quantity quantity;
  // ...

  Money amount() {
    if (isDiscountable())
      return discount(unitPrice, quantity);

    return unitPrice.multiply(quantity);
  }

  // ...

  boolean isDiscountable() {
    return quantity.compareTo(discountCriteria) >= 0;
  }
}

このコードでは、開示してよい知識と隠蔽したい知識を以下のように切り分け、前者をisDiscountable()に閉じ込めました:

開示してよい知識
受注ごとにその内容に応じて値引き可否が決まるという知識。
隠蔽したい知識
注文数量・金額等にもとづく具体的な値引き決定ルール。

ここで、「隠蔽したい知識」をなぜ隠蔽したいかというと、それが変わりやすいと考えるからです。ならば、一歩進んで、変わりやすい部分は差し替え可能にしておこうという発想が生まれます:

class SalesOrder {
    Money unitPrice;
    Quantity quantity;
    DiscountPolicy discountPolicy;
    // ...

    boolean isDiscountable() {
        return discountPolicy.isDiscountable(this);
    }
}

ここで、DiscountPolicyは、以下のようなインターフェースです:

interface DiscountPolicy {
    boolean isDiscountable(SalesOrder salesOrder);
}

DiscountPolicyの実装のひとつとして、QuantityBasedDiscountPolicyがあります:

class QuantityBasedDiscountPolicy implements DiscountPolicy {
    Quantity discountCriteria = Quantity.valueof(100);
      
    boolean isDiscountable(SalesOrder salesOrder) {
        return salesOrder.getQuantity().compareTo(discountCriteria) >= 0;
    }
}

QuantityBasedDiscountPolicyは、たぶん、DIコンテナなどにより、SalesOrderdiscountPolicyに注入されるでしょう。

この例で、ポリモーフィズムは、SalesOrderに関する様々な関心事から値引き計算に関する関心事を分離するのに用いられています。 例えば、テストという局面をとっても、QuantityBasedDiscountPolicySalesOrderに関する他のテストから切り離しでテストできますし、SalesOrderの方は(スタブを用いて)値引き計算の詳細を考慮せずにテストすることができます。 さらに、このソフトウェアが成功し、他社でも使われるようになれば、値引き計算ルールをこのように簡単にカスタマイズできることは、さらなるメリットをもたらすでしょう。

DiscountPolicyのように、特定の計算/判定条件をカプセル化し差替可能にするというポリモーフィズムの利用法は、GoFのデザインパターン本で「ストラテジー(別名:ポリシー)」として知られており、エリック・エバンスのドメイン駆動設計本でも、ごく最初の方で、オーバーブッキングポリシーという例が紹介されています(p.19/位置No.1068)。高度な利用法ではなく、極めて普通な用いられ方かと思います。

前回お話しした「情報隠蔽(=カプセル化)」では、隠蔽したい知識と開示してよい知識を区分して両者を別のモジュール(クラスもモジュールです)に割り振りました。 ポリモーフィズムは、そこから一歩進んで、それら2つのモジュールを切り離し、「疎結合化」するのに役立ちます。「疎結合」というと大げさに響きますが、要するに処理の依頼側と引受側2つのモジュールがあって、依頼側が引受側を知らずとも処理を依頼できるということです。この例で、値引き可否判定を依頼する側であるSalesOrderクラスはQuantityBasedDiscountPolicyクラスを知りません。

こういった意味における疎結合化は、オブジェクト指向以前も行われていましたが(*1)、気軽に、広範に適用できるようになったのは、やはりポリモーフィズムが生み出されてからのことです。

区分値ポリモーフィズムと本書に対する評価

さて、ポリモーフィズムの典型的なユースケースをおさらいした頭で、「区分値ポリモーフィズム」を見直してみましょう。

本書が説明する通り、区分値ポリモーフィズムには、if文やswitch文を排除することで、ソースコードの読みやすさを改善する機会を提供する、という役立ちがあります。

しかし、区分値オブジェクトのメソッド(本書の例では、yen()など)を呼び出す側のプログラムは、区分値オブジェクトのクラスを「知って」いるのが通常ですから、処理の依頼側と引受側は結合したままです(*2)。 ですから、前述したように「疎結合化」をポリモーフィズムの大きな意義と捉える立場からすれば、区分値ポリモーフィズムはやや傍流的な用法に見えることがお分かり頂けるでしょう。

公平に見て、本書は、ポリモーフィズムの他のユースケースについて触れていないだけであり、それらを否定しているわけではありません。ですから、本件は、本書の問題というより、読む側が注意すべき点である、ということなのかもしれません。

ただ、「多態は、区分ごとのロジックをクラス単位に分離してコードを読みやすくするオブジェクト指向らしいしくみです(p.58/位置No.1040)」といった説明を読む読者、とりわけオブジェクト指向の経験が浅い読者が、ポリモーフィズムの主な用途は区分ごとの場合分けロジックを整理することなのだと受け止めるのは自然なことでしょう。しかし、その道の先は行き止まりなのです。

むしろ、初級者には、疎結合化というビジョンのもとで、できるだけ簡単な入り口を示してあげるのが親切というものでしょう。上述したポリシーあるいはストラテジーパターンはそのような入り口として好適な例のひとつと思います。

「現場で役立つシステム設計の原則」という格調高いタイトルと「変更を楽で安全にするオブジェクト指向の実践技法」という具体的な副題を持つ300ページ超の本の中で、オブジェクト指向の業績の中心にあるポリモーフィズムについてこのように周辺的なユースケースのみ解説されている事態は、少なくとも私には驚異的なことです。

ポリモーフィズム ― さらなる展開

今回の批判はこれで終わりですが、例として取り上げた値引き計算のケースは、ポリモーフィズムの可能性を検討する上で興味深いので、もう少し深堀りしてみましょう。 前回以来の例では値引き可否判定ロジックの扱いが焦点でしたが、実際には可否を判定するだけでなく値引き額の計算が重要でしょう。業種にもよりますが、値引きの計算は複雑で、特定品目限定の値引き、過去の取引履歴に基づく値引き、一回の総発注額に基づく値引き、キャンペーンでの値引きなど多岐にわたるかもしれません。また、そうした値引きルールは、時の経過に応じて追加され、また廃止されていきます。システムは、そうした諸々の値引きについて、理由と金額をお客様に提示する必要があるでしょう。

こうした状況にどう対応すればよいでしょうか。SalesOrderクラスは、isDiscountable()メソッドで値引き可否だけを返せばよいのではなく、値引きの詳細を返さなければなりません。例えば、以下のようなメソッドを備えることになるでしょう:

List<Discount> getDiscounts();

Discountは、値引き理由の説明と値引き額を保持するオブジェクトです:

class Discount {
    String description;
    Money  discount;
    // ...
}

ここでの焦点は、値引きルールの詳細を隠蔽することに加えて、その時々に応じて値引きルールを追加し、あるいは廃止出来るようにすることです。それを踏まえれば、getDiscounts()の詳細は以下のようになるでしょう:

class SalesOrder {
    // DIコンテナなどから値引きポリシーのリストを設定
    List<DiscountPolicy> discountPolicies;
      
    // ...

    List<Discount> getDiscounts() {
        List<Discount> discounts = new ArrayList<Discount>();

        // 値引きポリシーを適用
        for (DiscountPolicy discountPolicy : discountPolicies) {
            Discount discount = discountPolicy.getDiscountFor(this);
            if (discount != null) {
                discounts.add(discount);
            }
        }

        return discounts;
    }
}

インターフェースDiscountPolicyは以下のようになります。

interface DiscountPolicy {
    /**
     * 与えられた salesOrder に対する値引きを計算し、その結果を Discount として返す。
     * 当ポリシーでの値引きが salesOrder に適用されない場合は null を返す。
     */
    Discount getDiscountFor(SalesOrder salesOrder);
}

このようにしておけば、新しい値引き制度が出来たときには、それに対応するDiscountPolicyを作成してシステムに登録するだけで対応が完了します(*3)。

値引き計算以外に、請求/回収条件、在庫引当、配送方法などにも同じような仕組みが適用可能かもしれません。こうした手法を常に適用すべきということではありませんが、適用しようと思えば出来る、ということを理解していると、設計における選択肢の幅が顕著に広がります。

こうした例をお示ししたのは、ポリモーフィズムというものが過度に技術的なものと受け止められているのではないかと私が危惧しているからです。

イヌがワンワン、ネコがニャーニャーという説明ばかりでポリモーフィズムはわからん、という方が多いですが、一方でその方もDIコンテナは使っていたりします。DIコンテナは、技術的環境に依存するオブジェクトに関して、アプリケーションにはインターフェースだけ提示し、その実装はポリモーフィズムによって実行時に結合することで、アプリケーションと技術的環境を疎結合に保つための仕掛けです。ポリモーフィズムのメカニズムが本当にわからないのであればDIコンテナを使うことさえ出来ないはずです。

ですから、多くの人にとってわからないのは、ポリモーフィズムのメカニズムではなく、「使いどころ」なのだろうと思うのです。フレームワークなどがポリモーフィズムを使っているのはわかっている。ただ、自分たちが書いている「アプリケーション」のコードにおいてどういう局面でポリモーフィズムを使えばよいのか、わからない。結果として、ポリモーフィズムは自分たちに関係ない「技術的」な概念だと感じてしまう。そういうことではないでしょうか。

実際には、それは誤解で、アプリケーション開発においてもポリモーフィズムはカジュアルに利用できます。値引き計算の例ではそれをご理解頂きたかったわけです。ポリシー(ストラテジー)パターンは、ポリモーフィズムのユースケースのごく一部に過ぎませんが、疎結合化というポリモーフィズムの本旨に沿っており、かつ、利用価値も高いものだと思います。

杉本 啓 @sugimoto_kei
経営管理基盤ソフトウェア fusion_place の、プログラマ兼設計者
http://www.fusions.co.jp


[脚注]

[1] 昔からある「ユーザ出口ルーチン」などはこの例でしょう。
[2] 料金計算など区分値に依存するメソッドを区分値クラスに移すことと、その計算にポリモーフィズムを適用することは別問題です。例えば本書p.60/位置No.1062のFeeTypeクラスは以下のように記述することもできます(label()に関する実装は省略)。

enum FeeType {
    ADULT,
    CHILD,
    SENIOR;
      
    Yen yen() {
        switch (this){
            case ADULT:
                return adultFee();
            case CHILD:
                return childFee();
            case SENIOR:
                return seniorFee();
            default:
                throw new UnsupportedOperationException("Fee for fee type [" + this + "] not defined.");
        }
    }

    private Yen adultFee() {
        return new Yen(100);
    }

    private Yen childFee() {
        return new Yen(50);
    }

    private Yen seniorFee() {
        return new Yen(80);
    }
}

p.60/位置No.1062のコードとこのコードを比べてみると、料金計算方法という知識がFeeTypeクラスに隠蔽されている点は同じで、大人・子供・シニアの料金計算がそれぞれ別のメソッドに分離されている点も同じです。違いは、条件分岐に switch文を使うかポリモーフィズムを使うかという点だけです。 こうしたメソッドが複数あるならば、それぞれの実装でswitchを書くより、ポリモーフィズムを用いるべきでしょう。上記の例のようにswitchが1箇所だけであれば、いずれにするかは、設計の良否というより多分に好みの問題と、私は思います。

なお、このケースではそもそもポリモーフィズムを使うほどのこともなく、以下のコードで十分です。

enum FeeType {
    ADULT(100),
    CHILD(50),
    SENIOR(80);
      
    private final Yen yen;
      
    private FeeType(int yenAsInt) {
        this.yen = new Yen(yenAsInt);
    }
      
    Yen yen() {
        return yen;
    }
}

ただ、これは、単にサンプルコードの選択に関する趣味の問題であって、区分値ポリモーフィズムの妥当性という論点には影響しません。

[3] ここで示したコード例は、値引き制度の間に相互依存関係がないという想定にもとづいています。依存関係があり得る場合(例えば上得意先向け値引きを適用したときにはキャンペーン値引きは適用しないといった条件に対応する場合)、DiscountPolicyDiscountのリストを渡し、各ポリシーがそのリストに対してDiscountを除去したり追加したりできるようにする方がよいかもしれません。

interface DiscountPolicy {
    /**
     * 与えられた salesOrder に対する値引きを計算し、その結果を DiscountのListに追加する。
     * リストから既存のDiscountを除去しても構わない。
     */
     void updateDiscounts(SalesOrder salesOrder, List<Discount> discounts);
}

この場合、Discountに、値引き制度を識別するためのDiscountTypeなどを保持させることも必要でしょう。

値引き計算といった機能をこのように疎結合化するにあたり、疎結合化されたモジュール間の役割分担をどのように設計するかは極めて重要です。あるいはそれこそがオブジェクト指向にもとづくアプリケーション設計の核心部分かもしれません。 「オブジェクト指向」という言葉を作ったアラン・ケイは「オブジェクトよりもメッセージング」と強調したメールの中で、以下のように言っています:

The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.

(拙訳)偉大で成長可能なシステムを作る上でのカギは、個々のモジュールの内部特性や振る舞いがどうあるべきかということにではなく、むしろ、モジュールたちがどのようにコミュニケ―トするかをデザインすることにこそある。

また、「マルチパラダイムデザイン」で、ジム・コプリエンが提示した「共通性/可変性分析」も、こういった相互作用に重点を置いた設計手法です。

design ddd practical.ddd