未完成の原稿を投稿してよいものか悩みましたが、オブジェクト指向の説明部分は書き終えたので一旦投稿します。
会社でUMLの勉強会をすることになったので、資料として使えるように簡単にまとめました。
書いてみてほとんどオブジェクト指向の説明になってしまいましたが、オブジェクト指向的な考え方ができないとUMLを有効活用できないと思いますので、ご容赦ください。
- 対象者
- 中途採用として同時期に入社した同期
- 前職は非ITのメンバーも多い
- プログラミング経験は入社してからの研修のみのメンバーも含まれる
- 目的
- チーム内でコミュニケーションのレベルを合わせられるようになること
- 目標
- オブジェクト指向設計の利点を理解する
- オブジェクト指向設計の考え方を理解する
- 考えを図式化する際に適切な書き方を選べるようになる
UMLの目的
UML(Unified Modeling Language)はシステムの構造や設計を外部(発注者、チームメンバー、未来の自分)と共有するための記法を規定した言語です。
オブジェクト指向が台頭してきた頃、分析/設計の表現方法は人(宗派)によってバラバラでした。同じ概念のものを図式化しているのに、表現方法が違っていてはシステムを実装する現場が混乱してしまいます。
オブジェクト指向は元々、大規模なシステム開発に耐えられるよう、分業を前提として提唱されたため、コミュニケーションのミスは致命的です。分析/設計を支援するためのツールとして統一された記法が必要になりました。
そこで賢い人たち(Grady Booch, Jim Rumbaugh, Ivar Jacobsonなど)が集まって、標準化された記法がUMLです。
UMLはその後も改訂されており、現在は2015年に改訂されたUML2.5が最新バージョンとなっています。
UMLのバージョンはOMG(Object Management Group)という団体で管理されています。
オブジェクト指向とは
プログラミングの手法の一つで、アルゴリズムというよりもプログラミングする対象のもの(オブジェクト)に焦点を当てて設計/実装する手法です。
大規模なシステムを大人数で分担することが容易になります。また、オブジェクト同士の結合を疎にすることで、再利用性と変更容易性(保守性、拡張性)が高くなるという特徴があります。
オブジェクト
オブジェクト指向において、プログラミングする対象のものです。
物理的なものであったり、概念的なものやただのデータの集合体であったりします。
オブジェクト指向では、プログラミングする対象のものをできるだけ細かくオブジェクトに分け、オブジェクトごとに役割(責任)を与えることで、システム全体を設計します。
自動車を例にすると、『自動車』というオブジェクトを設計するとき、『自動車』を『アクセル』や『ハンドル』、『ブレーキ』というオブジェクトに分け、それぞれに『走る』、『曲がる』、『止まる』という役割を与えることで、『自動車』全体を設計します。
このようにオブジェクトを分割した後、それぞれの役割を実現するためのアルゴリズムを設計していきます。
ここで重要なのは、それぞれの役割を実現しようとするとき、一つのオブジェクトの変更が他のオブジェクトに影響しないように分割することです。
オブジェクトの分割が不適切だと作業分担がうまくいかず、無駄なロジックやデータの重複が発生してしまいます。
極端な例ですが、動力は一つなのにタイヤを『右タイヤ』、『左タイヤ』というオブジェクトに分割してしまうと、同じ役割を複数のオブジェクトに実装しなければならない上に、"右タイヤが右を向いているときは、左タイヤは左を向けない。"というように、お互いの状態によってお互いの動作が制限されてしまうような関係性ができてしまいます。
これではせっかくオブジェクトを分割しても、設計と実装は互いに整合性を保ちながら進めなければならず、オブジェクト指向の効果が発揮できません。
このような失敗をしないために、オブジェクト指向において守るべき『カプセル化』、『継承』、『多態性(ポリモーフィズム)』という3原則(人によっては4原則とかありますが割愛)について説明します。
カプセル化
個人的にはオブジェクト指向の中で一番大事な原則だと思います。
上流寄りの設計段階(概要設計 or 機能設計)では、この原則だけを頭に入れておけばそれなりの設計になると思います。
カプセル化とは、役割とデータをひとまとめにすることです。
ここで言うデータとは、そのオブジェクトの役割を実現するために必要なデータのみであり、過不足があってはいけません。
また、データは基本的に他のオブジェクトから操作してはいけません。
データの変更は、そのデータを持つオブジェクトを介して行います。
先ほどの自動車の例えでいうと、『ハンドル』オブジェクトは『ハンドルを切る』という役割を実現するために『操舵角』というデータを持っており、『操舵角』の変更はメソッド(or 関数 or 手続き)を介して行われます。
決して『アクセル』や『ブレーキ』が『操舵角』を直接変更するようなことがあってはいけません。
カプセル化することで以下の効果を期待できます。
- データの整合性がとりやすい
- 仕様変更の影響範囲を限定できる
一つのデータに対する変更のアルゴリズムを一つのオブジェクトにまとめるので、データを変更するタイミングや異常値の判定などを一つのオブジェクト内で完結できます。
そのため、データの整合性がとりやすく、役割の仕様変更の影響は一つのオブジェクトに閉じることができます。
継承
継承はオブジェクト間の関係を表すもので、継承関係にあるオブジェクト同士は階層構造で考えることができます。
上位層のオブジェクトは下位層のオブジェクトを抽象化したものになり、下位層のオブジェクトは上位層のオブジェクトを具体化したものになります。
『自動車』の抽象化を考えると、『自動車』は『乗り物』であると言えます。
この時、『乗り物』は『自動車』の上位層であり、『自動車』は『乗り物』を継承していることになります。
逆に『自動車』の具体化を考えると、『車種』になります。
プリウスやリーフ、フィットなどです。
この時、『自動車』はそれぞれの『車種』の上位層であり、それぞれの『車種』は『自動車』を継承していることになります。
継承は、原則として紹介していますが利用するかどうかは構築するシステムによります。
構築するシステムで『自動車』しか扱わないのであれば、わざわざ『乗り物』というオブジェクトを作成して継承させる意味はありません。
継承を利用することの意味は、上流寄りの設計では実感することはできないかもしれません。
個人的には、詳細設計をしている段階で複数のオブジェクトが発生して、それらの共通点を一つのオブジェクトとして定義するというボトムアップ的な使い方が多いような気がします。
『自動車』には『荷物/人を乗せる』という役割がありますが、この役割は他の『乗り物』、例えば『列車』や『飛行機』でも同じです。
『自動車』と『列車』と『飛行機』を同じシステム上で考える必要があるとき、『荷物/人を乗せる』という役割を実現するための『積載量』や『定員』というデータやその計算方法をそれぞれのオブジェクトに実装するのではなく、『乗り物』というオブジェクトにまとめて実装することで、『自動車』や『列車』や『飛行機』のオブジェクトの設計者はそれぞれ固有の役割の設計に専念することができます。
他には例えば、各オブジェクトが呼び出されたときにログを出力する必要がある場合などは、継承を利用したほうがよいでしょう。
ログの内容自体はオブジェクトによって異なるかもしれませんが、出力先のログファイルや日付などのフォーマットはオブジェクトによらず、システムで統一するべきです。
この部分の実装を各オブジェクトに任せてしまうと整合性をとることが困難ですし、オブジェクトの設計者はそのオブジェクトの役割の実現のために工数を割くべきです。
多態性(ポリモーフィズム)
多態性(ポリモーフィズム)は機能の具体的な実装と、外(他のオブジェクトやメソッド)からの見え方/呼び出し方を分ける考え方です。
ポリモーフィズムの考え方に基づいて設計された機能は、インターフェース(関数名、引数など)だけが他のオブジェクトに公開されていて、呼び出し側からは実際にどのオブジェクトに実装されている機能なのかを意識する必要もありません。
インターフェースを実装しているオブジェクトはしばしば、○○できる(-able)オブジェクトと表現されます。
自動車の例では、『自動車』オブジェクトは『走れる(runnable)』、『曲がれる(turnnable)』、『止まれる(stoppable)』というインターフェースを実装していることになります。
具体的にどのような走り方(実装方法)をするかは、それぞれの『車種』によって異なります。
加速性に優れた『車種』もあるでしょうし、燃料効率を重視する『車種』もあるでしょうが、どれも『走れる』ということは変わりません。
呼び出し側から考えた場合、ポリモーフィズムの利点が理解しやすいです。
『乗り物を走らせる』という役割を持った機能があった場合、この機能は『走れる(runnable)』オブジェクトであればなんでも走らせることができるようにします。
つまり、『走れる(runnable)』オブジェクトの『走る(run)』というメソッド(or 関数 or 手続き)を呼び出すようにしておきます。
こうしておけば、実際に『走る』オブジェクトが変更されても呼び出し側の機能を変更する必要がなくなります。この機能は、プリウスでもリーフでも、フィットでも列車であっても、走らせることができるようになっているわけです。
仮にポリモーフィズムの考え方で設計していなかった場合、『乗り物を走らせたい』と考えたときには、『プリウスを走らせる』という機能と『リーフを走らせる』機能と『フィットを走らせる』という機能をそれぞれ実装する必要があります。
これらの機能は、それぞれの『車種』を走らせることを前提に設計されており、オブジェクト間の結合が密であるため、あまり良い設計とは言えません。
クラス図
クラス図は、クラス同士の関係性を表す図です。
クラスとは、オブジェクトから特定の(システム化に必要な)役割だけを抜き出して作られる設計図のようなものです。
プログラミングをしていると、クラスから生成されたインスタンス(設計図を基にして作られた実際のモノ)のこともオブジェクトと呼ぶので混乱しますが、概念としてのオブジェクトは、クラスよりも上位の概念だと思います。
UMLでは、クラス図とオブジェクト図というものがあり、オブジェクト図はクラス図を具体化したものという位置づけになっています。
つまり上記の説明とは逆の位置づけです。
個人的意見ですが、UMLでのオブジェクト図はインスタンス図という名前の方がよいのではないかと思います。
クラス図では、下記の要素を図示することができます。
- クラス名
- 属性(or メンバー or データ)
- 属性の名称
- 属性の型
- 操作(or 役割 or メソッド or 関数 or 手続き)
- 名称
- 引数
- 引数の型
- クラス同士の関係性
- 関連
- 他のどの関係にも当てはまらない関係性の場合に使うような気がします。
- 集約
- 同質のクラスが複数集まって別のクラスになる場合に使います。
- コンポジション
- 異なるクラスが複数集まって別のクラスになる場合に使います。
- 依存
- そのクラスの状態が他のクラスからの操作や状態によって変化する場合に使います。
- 汎化
- クラス同士が継承の関係にある場合に使います。
- 実現
- インターフェースとそれを実装しているクラスの関係を示す場合に使います。
- 多重度
- 上記の関係性において、数的な関係がある場合に使います。
- 関連
クラス同士の関係性について、後ほどもう少し詳しく説明します。
どういう場合に必要か
最初にも書きましたが、クラス図はクラス同士の関係性を図示するための図です。
複数のクラスによって成り立っているシステムを説明する場合には、必ずと言っていいほど必要となるでしょう。
記載する内容のレベルの違いはありますが、オブジェクト指向でシステムを設計するときに、頭の中ですらクラス図を描かないというのは無理のような気がします。
問題は自分の頭の中の設計を人に伝えようとするときに、正確に伝えられるかどうかです。
クラス図の描き方
クラス名と属性、操作
関係性の描き方
関係性の表
アクティビティ図
データの流れや処理の順番を示す図です。
フローチャートと似ていますが、人間系を含めたシステム全体の分析など、複数の端末や関係者が登場する処理の場合に使うと効果的です。
シーケンス図
複数の関連部署やシステムの間でのデータのやり取りの順番を示す図です。
アクティビティ図を縦にしたような見た目ですが、それぞれの処理の内容よりも入力と出力に注目できるので、入出力を明確にしたい場合に使うと効果的です。