モデル描きからコード書きへ
UML 登場時は自分でモデリングツールを書いていたこともあります。
オブジェクト指向分析・設計を得意とする会社に入って10年以上経った。UML でモデルを描く機会が減って、コードばかり書いてる今日この頃。
— MH (@kondoumh) 2016年11月29日
昔はモデルを描いてからコードを書いてたというか、モデルだけ描いてお仕事が終わってたプロジェクトもありました。
UML の登場とオブジェクト指向プログラミング
UML は C++ 普及期に誕生し Java の成熟とともに発展してきました。元々 OMT や Booch など複数の流派が存在して独自に CASE ツール*1 が開発されていました。しかし、世紀末が近づいたからか、統一の機運が高まり Unified Model Language として標準化されました。
UML 2.0 以降、表現力が増し、色々なモデル表現*2がサポートされていますし、シーケンス図の opt / alt / loop やガード条件を駆使して、手続き型の処理も表現できるようになっています。とはいえ基本的にはオブジェクト指向設計、実装を主要なターゲットパラダイムとしており、クラス図で静的な構造を、コミュニケーション図やシーケンス図で動的な振る舞い (インスタンス同士のコラボレーション) を表現します。クラス分割と責務の割り当てという設計作業を支援するグラフ言語と言ってよいでしょう。GoF デザインパターンの解説本も、クラス図とシーケンス図が使われてました。
90年代後半から2000年代前半ぐらいまでは UML と OOP が開発者の必須スキルと言ってもよい状況*3でした。
UML の不得意分野
総称プログラミング
C++ の Template や Java の ジェネリクスは、オブジェクト指向ではなく、型をパラメータとした生成的プログラミング技法に端を発する技術です。そして UML はこれが苦手というか表現が面倒な部分があります。
ジェネリクスは Java におけるタイプセーフなコーディングには欠かせません。業務システムでエンティティや DAO (Data Access Object) を作る場合などでも多用します。
/** 識別子を表すインターフェース */ public interface Identifiable<T> { // .. } /** エンティティの抽象型 */ public abstract class DefaultEntity implements Identifiable<Long>, Serializable { // .. } /** DAO のインターフェース */ public interface Dao<E extends DefaultEntity> { // .. } /** デフォルト DAO 実装 */ public abstract class DefaultDao<E extends DefaultEntity> implements Dao<E> { // .. } /** 顧客エンティティ */ public class Customer extends DefaultEntity { // .. } /** 顧客 Dao */ public class CustomerDao extends DefaultDao<Customer> { // .. }
といった具合。型パラメータに具象型を適用しつつ拡張・・といったことが簡潔に表現できます。これをクラス図でまじめに表現するとこんな感じ*4。
モデリングするにあたり型パラメータにバインディングされた型を個別に定義しないといけません。つまりコンパイラがやってくれる型バインディングによる型生成を手動でやるということ。コード書くよりモデリングの方が複雑になってしまいます。
実際問題、ジェネリクスの構造を UML エディタで作るのはかなり面倒です。設計時にはやってられないので、実装した後に可視化のためリバースした方が楽ということになります。
関数型プログラミング
C++ の STL*5 でコンテナとファンクタ (と内部のアルゴリズム) を組み合わせてコードを書くパラダイムが登場します。ファンクタの実態は関数ポインタで、オブジェクト指向パラダイムのポリモフィズムよりも強力にデータ構造とアルゴリズムを分離可能にしました。その後、C# に LINQ が登場し、近年では Java 8 で Stream が実装され、メジャーなプログラミング言語は、関数型プログラミングパラダイムを取り込んでいます*6。
関数型プログラミングが有効なのは、状態を持たない計算やデータ加工 ( map / filter ) で、Java や C# ではメソッド内の局所的な処理です。関数型はオブジェクト指向をリプレースする技術ではなく手続き型を駆逐する技術として普及しています。
一方で、メソッドや関数をファーストクラスオブジェクトとして扱っていなかった UML では関数型プログラミングを表現するのは困難です。LINQ や Stream を表現する手段はないと言ってよいでしょう。関数型プログラミングは宣言的な表現になり、可読性が高いのが特徴です。関数型プログラミングには独自のモデル表現が考案されていますが、現在のところ UML には取り込まれていません*7。
それでも UML は有用
UML は OOP にマッチしているので、OOP がワークする部分、すなわちクラスを跨るメソッド呼び出し、クラス自体の状態遷移にフォーカスすべきです。そして、クラスのメソッド内に実装されるアルゴリズムは、極力ステートレス (状態を持たない) に実装すべきで、その領域には関数型プログラミングが有効なのです。ということで、プログラムの構造は UML で可視化、メソッドの実装は関数型プログラミングで宣言的に実装することで可視化するというのが現状ではないかと思います。
以下 UML ツールのユースケースについて。
スケッチ
設計者と実装者の間でコミュニケーションするための用途がスケッチとしてのモデリングです。合意ができればモデルの役割は終了。未来永劫保守することは最初から諦めています。
ソースコード生成
Model-Driven Architecture が注目されていた時代がありました。モデルからコードを生成できれば、実装工数は圧縮できると期待されたのです。大量に属性を持つ Entity クラスの生成などには有効ですが、Excel マクロなどでも事足りることが多いです。コードエディタや IDE が高度に進化している現在 CASEツールが生成するコードの価値は下がる一方です。コードを書くスピードは、もはやボトルネックとは言えません*8。
可視化
実装が終わったコードの可視化。主に保守用に使用します。モデリングと同様、プログラムの構造を伝えるためセンスが必要とされます。
*1:Computer Aided Software Engineering のツール
*2:アクティビティ図、ステートマシン図、コンポーネント図、配置図など
*3:少なくともエンタープライズのオープン系では
*4:実際にはこんな詳細までは図にしませんが
*5:Standard Template Library
*6:オリジナルの C++ / Java は純粋なオブジェクト指向言語であり、関数はファーストクラスオブジェクトではありませんでした。最近はラムダや無名関数がサポートされ、モダンな言語とのギャップが小さくなっています
*7:ツールベンダーは UML にこだわらず使えるようにする可能性はあります。
*8:組み込み業界では MDA のパラダイムが残っています