[技術講座]
今日のソフトウェアで扱う問題は非常に複雑であり、近年では、さらに複雑化かつ多様化する方向にあります。しかし困ったことに、われわれ生身の人間は、一度に考える範囲が限られている為、全ての問題を一度に取り掛かることは、現実問題として不可能です。そのため設計では、実現手段を考える上で、複雑な問題を分割する作業も必要になります。オブジェクト指向設計では、問題を構成する責務を意識して、クラスとして分割します。そして、そのクラスのインスタンスであるオブジェクトが協調作業することによって、提示された問題を解決するのです。
ここで、クラスをどうやって導出する、あるいは既存のクラスに何を実行させるか、ということを考える際に「責務(Responsibility)」という視点が非常に重要になってきます。もちろん、責務だけを考えればで優れた設計ができるわけではありません。しかしながら、責務を意識することは、オブジェクト指向設計において重要な要素の一つです。そして、オブジェクト指向設計において、クラスに対して責務を適切に割り当てることは、非常に重要な設計スキルだと言えます。なぜなら、この責務の割り当てがソフトウェアの品質に大きく影響するからです。
では、責務とは具体的に何なのでしょうか?私自身は、非常によく使われる言葉でありながら、曖昧にされてきた印象を持っていました。特に入門書においては、当たり前のように(かつ、さりげなく)使用されたり、役割と混同されて使用されるケースもあり、オブジェクト指向を勉強したての頃は混乱した記憶があります。そのおかげでいろいろな文献やサイトを調べる機会に恵まれ、現在に至っています。
今回は、この「責務」について考えてみましょう。
まず、責務 (Responsibility) とは何なのでしょうか?
Booch は、責務を「オブジェクトが責任を持って行わなければならない振る舞い。責任はオブジェクトがある特定の振る舞いを提供しなければならない義務のことである。」として定義しています[BOOCH]
ここでのポイントは、そのクラスの持つ責務は、あるオブジェクトとの協調作用のなかで、決定される点です。
実際の設計においても、クラス図や相互作用図を作成するなかで、どのクラスにどういった責務を持たせるかということを決定していきます。そして、責務を持ちするぎるクラス (たとえば相互作用に参加する機会が多いクラス) を発見した場合は、そのクラスが持つ責務を分割し、他のクラスに割り当てます。
では、実際の設計において、どのようなものが責務として認識されるのでしょうか?クラスの持つ責務は、基本的に次の2種類に分類されます[CRC]。
- 振る舞いに対する責務 (Responsibility for Behavior)
- 知識に対する責務 (Responsibility for Knowledge)
以下、それぞれについて見てみましょう。
これは、実践UML[LARMAN]では、「実行責務」という言葉で分類されているものです。
振る舞いに対する責務の種類には以下のようなものがあります。
- 何かを自分自身で行う。
- 他のオブジェクトのアクションを始動する。
- 他のオブジェクトのアクティビティを制御し調整する。
ほとんどのクラスは何かしらの振る舞いを行います。
王道に従って、銀行のアプリケーションを例にとって考えてみましょう。
例を単純にするために、ここで登場するクラスを以下の2つに絞ります。
- 口座 (Account)
- 預金者 (Depositor)
そして、「預金を引き出す (Withdraw) 」、「預金を預ける (Deposit) 」という相互作用を考えてみましょう。
それぞれの一番単純なコラボレーション図を以下に示します。 (Figure 2-1)
Figure 2-1 : 単純なコラボレーション図 身も蓋も無いコラボレーション図ですが、この図から読み取れる情報は以下のとおりです。
- 口座 は、「金銭を預かる」という責務を持つ。
- 口座 は、「預金を引き出す」という責務を持つ。
この責務が、口座クラスの「振る舞いに対する責務」になります。
これは、実践UML[LARMAN]では、「情報把握責務」という言葉で分類されているものです。
知識に対する責務の種類には以下のようなものがあります。
- カプセル化されたプライベートなデータを把握する。
- 関係するオブジェクトを把握する。
- 導出または計算可能なものを把握する。
先程の例に戻ると、口座 は残高を有しているはずですし、預金者 は、名前や生年月日などの基本情報を有し、またそこから現在の年齢を導出することができます。 このとき、それぞれのクラスは以下のような責務を持つことになります。
- 口座 は「残高を知っている」責務を持つ。
- 預金者 は「名前を知っている」責務を持つ。
- 預金者 は「生年月日を知っている」責務を持つ。
この責務が、クラスの「知識に対する責務」になります。
UML では責務を記述する個所が厳密に規定されているわけではありません。記述する場合は、操作の定義の下の区画に記述するか、ノートとして記述するかの2つの方法があります。しかし、私が日常的に使用している CASE ツールでは操作の下の区画は使用できないので前者の記法は使用できないため、以下の図 (Figure 2-3-1) のようにメモとステレオタイプを使用して記述するようにしています。
Figure 2-3-1 : 責務を追加した例
UML では、コメントとして責務を記述することになりましたが、ソースコードを記述する際には、責務の表現はどうなるのでしょうか?先程のクラス図をソースコードまでブレークダウンした場合は以下のようになります。
以下のソースコードは、私のお気に入りの言語のひとつである C# で 口座 クラスと 預金者 クラスを記述したものです。今回は、C# を知らない読者の為に、Java と VB.NET のソースコードを Appendix に掲載しておきます。C# は開発言語として非常に良くできていると思いますので、これを機に一度 C# を触ってみる事をおすすめします。
List 2-4-1 : 口座クラス(Account.cs) 1:using System; 2: 3:namespace BankApplication 4:{ 5: /// <summary>口座クラス</summary> 6: public class Account 7: { 8: #region Constructors & Destructors 9: public Account(Depositor depositor) 10: { 11: this.Depositor = depositor; 12: } 13: #endregion 14: 15: #region Public Instance Properties 16: public decimal Balance 17: { 18: get { return balance; } 19: set { balance = value; } 20: } 21: 22: public Depositor Depositor 23: { 24: get { return depositor; } 25: set { depositor = value; } 26: } 27: #endregion 28: 29: #region Public Instance Methods 30: public decimal Withdraw(decimal amount) 31: { 32: balance -= amount; 33: return amount; 34: } 35: 36: public void Deposit(decimal amount) 37: { 38: balance += amount; 39: } 40: #endregion 41: 42: #region Private Instance Fields 43: private decimal balance; 44: private Depositor depositor; 45: #endregion 46: } 47:}
List 2-4-2 : 預金者クラス(Depositor.cs) 1:using System; 2: 3:namespace BankApplication 4:{ 5: /// <summary>預金者</summary> 6: public class Depositor 7: { 8: #region Constructors & Destructors 9: public Depositor(string name, DateTime birthday) 10: { 11: this.Name = name; 12: this.BirthDay = birthday; 13: } 14: #endregion 15: 16: #region Public Instance Properties 17: public string Name 18: { 19: get { return name; } 20: set { name = value; } 21: } 22: 23: public DateTime BirthDay 24: { 25: get { return birthday; } 26: set { birthday = value; } 27: } 28: 29: public int Age 30: { 31: get 32: { 33: DateTime today = DateTime.Today; 34: int age = today.Year - birthday.Year 35: - ( today.DayOfYear > birthday.DayOfYear ) 36: ? 1 37: : 0; 38: } 39: } 40: 41: public Account Account 42: { 43: get { return account; } 44: set { account = value; } 45: } 46: #endregion 47: 48: #region Private Instance Fields 49: private string name; 50: private DateTime birthday; 51: private Account account; 52: #endregion 53: } 54:}ソースコードには明示的な責務の記述が、どこにも存在していないことに注目してください。
C# においては「振る舞いに対する責務」は Account.Withdraw() や Account.Deposit() などのメソッドに、「知識に対する責務」は Account.Balance や Depositor.Name などのプロパティにそれぞれ暗黙的に対応付けられています。Java の場合では、「振る舞いに対する責務」、「知識に対する責務」ともにAccount.withdraw() や Account.getBalance() などのメソッドとして表現されるでしょう。ここでの重要なポイントは、これらメソッドやプロパティは責務とまったく同一のものではありませんが、責務を満たすためにメソッドやプロパティを使って実装を行う点です。
責務がどういうものかというイメージが掴めたところで、今度はなぜクラスに責務を適切に割り当てなければならないかを示したいと思いますが、その際には、責務が適切に割り当てられてない場合にどのようなデメリットがあるかを例に取るとイメージしやすいので、ひとつの例を題材に考えてみたいと思います。
一応断っておきますが、この例は、私がオージス総研に入社する前の話です。
よくある話ですが、あるアプリケーションに機能を追加するという話が持ち上がりましたが、そのアプリケーションの担当者はプロジェクトを、ずいぶん昔に離れてしまっているので、誰かがメンテナンスをしなければなりません。そこで、私にそのお鉢が回ってきたのですが、ご多分に漏れずドキュメントも何も無い状態で、プログラムの解析から始めなければなりませんでした。
仮に "BogusApp" と命名し、そのアプリケーションの特徴を以下に示します。
- Visual C++ の MFC アプリケーション
- ダイアログベース (メインのダイアログ・ボックスが一つ)
- ある帳票を文字解析した結果を、データベースに対して更新する。
- その際に解析結果のスコアを判定し、スコアが低い場合は更新を拒否して差し戻しをおこなう。
アプリケーションの規模も手頃で、仕様も明確だったので、当時オブジェクト指向を独学で勉強していた私にとっては、学んだ知識を適用するには格好の餌食でした。さしあたり、教科書どおりに UML で既存のクラス図を書くことから始めましたが、ごくごくシンプルなものになりました。その結果を以下に示します。(Figure 3-1)
Figure 3-1 : 解析したクラス図 もちろん、オブジェクト指向設計に基づいて設計したものではないので、責務によるクラス分割は行われていません。また、MFC で用意されている些末なクラスは省いていますが、このクラス図からドメインのために定義したクラスは唯のひとつも存在していないことが判ります。
機能追加するに当たって、私がこのアプリケーションを修正するのに苦労した点は以下のとおりです。
- 一つのモジュール (CBogusDlg) に全ての処理が無秩序に記述されているため、ソースコードの見通しが悪く、どこで何を行っているのかが把握しづらい。
- 同じような処理が至る所に散在しているため、ある変更に対してその処理を記述したコード全てを変更しなければならない。
- ある種類の帳票にだけ必要なデータの初期化が共通部分に存在したりと冗長な処理が多い為、期待するパフォーマンスが出ない。
- 呼び出されてない(と思われる)関数が存在しており、削除して良いものかどうか判断がつかない。
- 一つの関数の中でいろいろな処理が行われているため、何をしているのか解析しづらい。
- ・・・その他、いろいろな苦難。
当時のクラス図やソースコードを持っていないため、残念ながら修正結果をお見せすることはできませんが、最終的には、このドメインを20程のクラスに分割し処理を局所化した上で、機能追加のための修正を行うことにしました。要するに全面的な作り直しをしたわけです。
この例をまとめると、修正前のアプリケーションでは、一つのクラス (CBogusDlg) がドメインで必要な全ての責務を持っていた、つまり、責務による適切なクラス分割を行っていなかったため、以下のような弊害あったと考えています。
- 理解容易性が著しく低い
- コードが散在するため、不具合が混入する可能性を高める
- 機能追加/変更に対する脆弱性
言い換えると、クラスに適切な責務を割り当てるのは「理解容易性」、「変更容易性」、「頑健性」、「再利用性」などの品質要因をソフトウェアに付与するためです。ここで注意しなければならないのは、責務を割り当てれば自然にそうなる訳ではなく、そうなるようにクラスに適切な責務を割り当てなければならないという点です。そうしなければ、逆にクラス数が増大してしまい、本来の目的を果たすことができません。
このように責務はソースコードに直接的には表現されることはありませんが、責務の追加や変更は確実にソースコードに大きな影響を与えます。
これは構造化設計手法でいうところの「凝集度 (Cohesion)」と同様に捉えることができるかもしれません。
つまり、責務を別の視点で見ると、「機能追加や変更を理由にコードを修正する影響範囲」と捉えることができるということです。これに関して、Robert . C Martin は責務を "a reason for change"と定義しています。要するに、あるクラスに対する変更の動機 (a motive for changing a class) がひとつあるとすれば、クラスはひとつの責務を持っていることになる、というのです。そして、クラスに高い凝集性を付与するために、原則を適用することを推奨しています。それが、以下で紹介する "SPR (The single Responsibility Principle)"です[MARTIN]。
SPR の定義を以下に示します。
The Single Responsibility Principle THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE.
(クラスを変更する理由は一つ以上存在してはならない。)この原則について、先程の銀行アプリケーションの設計を例にとって考えてみましょう。
さっきの例はシンプルすぎるので「年利を計算する」という責務を追加したクラス図とソースコードを以下に示します。(Figure 5-1)(List 5-1-1)
Figure 5-1 : 「年利を計算する」責務を追加した例
List 5-1-1 : 変更した 口座クラス(Account.cs) 1:using System; 2: 3:namespace BankApplication 4:{ 5: /// <summary>口座クラス</summary> 6: public class Account 7: { 8: #region Constructors & Destructors ~: ・・・ 省略 ・・・ 13: #endregion 14: 15: #region Public Instance Properties ~: ・・・ 省略 ・・・ 27: #endregion 28: 29: #region Public Instance Methods ~: ・・・ 省略 ・・・ 40: 41: public decimal ComputeAnnualInterest() 42: { 43: return Decimal.Truncate( 44: balance * annualRate 45: / 365 46: * DateTime.Today.DayOfYear 47: ); 48: } 49: #endregion 50: 51: #region Private Instance Fields ~: ・・・ 省略 ・・・ 54: #endregion 55: 56: #region Private Static Fields 57: private const decimal annualRate = 0.0003M; 58: #endregion 59: } 60:}どうでしょう?先程のクラスにメソッドと属性をそれぞれ一つ追加しただけです。自画自賛するつもりは毛頭ありませんが、メソッド数もそれほど多くないし、そんなに悪くない設計のように思えます。
しかし、要求定義で以下の要求が挙がっていたとします。
- そのシステムで、利息の計算方法が頻繁に変更される。
- 違う銀行システムでも、口座 クラスを再利用したい。
このような視点で、先程の 口座 クラスを見るとどうでしょうか?
そうすれば、口座 クラスには以下のような弊害が存在することがわかります。
- 利率計算の方法が各システムで異なるため、各システムに適用する場合には年利計算のコードを変更しなければならず、再利用性が損なわれる。
- 年利計算方法の変更のためだけに、口座 クラスそのものが変更にさらされるため、保守性が損なわれる。
また、C++ などの静的リンクを行う言語では、口座 クラスを参照しているクラスは、口座 クラスのヘッダファイルが変更する都度、全てコンパイルおよびリンクをし直さなければならず、あまりに非効率です。
つまり、現状の 口座 クラスには、SPR が上手く適用できていないと言えます。では、口座 クラスに SPR を適用してみましょう。適用結果を以下に示します。(List 5-2-1)(List 5-2-2)
List 5-2-1 : 口座クラス(Account.cs) 1:using System; 2: 3:namespace BankApplication 4:{ 5: /// <summary>口座クラス</summary> 6: public class Account 7: { 8: ・・・List 2-4-1 と同様・・・ 46: } 47:}修正した 口座 クラスに関しては、List 2-4-1 のソースコードとまったく同一になるため詳細は割愛します。
年利計算に関する責務は、新たに年利計算のためのクラスを作成し責務を割り当てました。(List 5-2-2)
List 5-2-2 : 年利計算クラス(AnnualRate.cs) 1:using System; 2: 3:namespace BankApplication 4:{ 5: /// <summary>年利計算クラス</summary> 6: public class AnnualRate 7: { 8: #region Constructors & Destructors 9: public AnnualRate() { } 10: #endregion 11: 12: #region Public Instance Method 13: public decimal ComputeAnnualInterest(Account account) 14: { 15: return Decimal.Truncate( 16: account.Balance * annualRate 17: / 365 18: * DateTime.Today.DayOfYear 19: ); 20: } 21: #endregion 22: 23: #region Private Static Fields 24: private const decimal annualRate = 0.0003M; 25: #endregion 26: } 27:}このように SPR を適用し変更理由による変更箇所を局所化したことによって、口座 クラスに対して変更を加えることなく、年利計算を自由に修正することができるようになりました。また、口座 クラスはシステム固有で変更する箇所が減った為、安定した状態になり再利用性が高まったと思います。
このように責務を意識することは、オブジェクト指向設計において重要な視点であり、責務を注意深く割り当てることにより、ソフトウェアに「理解容易性」、「変更容易性」、「頑健性」、「再利用性」などの品質要因を付与することができます。
しかし、実際の設計においては複数の責務を実装クラスに割り当ててしまいがちです。
そこで、責務をある要求に対する変更要因と捉え、SPR を適用することによって、責務の分割を促すことができます。ただし、必要以上のクラス分割は逆に設計に複雑性をもたらすため、本当に必要なところにだけ適用することを心がけることが重要です。今回は、「責務」というオブジェクト指向設計における重要な概念について考えてみました。次回は、設計をさらに洗練するために「役割」について考えてみたいと思います。
では、また次回にお会いしましょう。
- [BOOCH] 『Booch法:オブジェクト指向分析と設計 第2版』 Grady Booch/著, 山城 明宏・井上 勝博・田中 博明・入江 豊・清水 洋子・小尾 俊之/訳, アジソン・ウェスレイ
- [CRC] 『The CRC Card Book』 David Bellin・Susan Suchman Simone/著 Addison-Wesley
- [LARMAN] 『実践UML』 Craig Larman/著, 依田 光江/訳, 今野 睦・依田 智夫/監訳 プレンティスホール
- [MARTIN] 『The Single Respolsibility Principle』 Robert C. Martin
https://www.objectmentor.com/resources/articleIndex
この記事は、2002年出版予定の書籍"The Principles, Patters, and Practices of Agile Softweare Development"の草稿だそうです。
List 8-1-1 : 口座クラス(Account.java) 1:package BankApplication; 2: 3:import java.math.BigDecimal; 4: 5:public class Account 6:{ 7: public Account(Depositor depositor) { 8: setDepositor(depositor); 9: } 10: 11: public BigDecimal getBalance() { 12: return balance; 13: } 14: 15: public void setBalance(BigDecimal value) { 16: balance = value; 17: } 18: 19: public Depositor getDepositor() { 20: return depositor; 21: } 22: 23: public void setDepositor(Depositor value) { 24: depositor = value; 25: } 26: 27: public BigDecimal withdraw(BigDecimal amount) { 28: balance = balance.subtract(amount); 29: return balance; 30: } 31: 32: public void deposit(BigDecimal amount) { 33: balance.add(amount); 34: } 35: 36: private BigDecimal balance = new BigDecimal(0.0); 37: private Depositor depositor; 38:}
List 8-1-2 : 預金者クラス(Depositor.java) 1:package BankApplication; 2: 3:import java.util.Date; 4:import java.util.Calendar; 5: 6:public class Depositor 7:{ 8: public Depositor(String name, Date birthday) { 9: setName(name); 10: setBirthDay(birthday); 11: } 12: 13: public String getName() { 14: return name; 15: } 16: 17: public void setName(String value) { 18: name = value; 19: } 20: 21: public Date getBirthDay() { 22: return birthday; 23: } 24: 25: public void setBirthDay(Date value) { 26: birthday = value; 27: } 28: 29: public int getAge() { 30: Calendar birth = Calendar.getInstance(); 31: birth.setTime(birthday); 32: Calendar today = Calendar.getInstance(); 33: today.setTime(new Date()); 34: 35: int age = today.get(Calendar.YEAR) 36: - birth.get(Calendar.YEAR) 37: - ( today.get(Calendar.DAY_OF_YEAR) > birth.get(Calendar.DAY_OF_YEAR) ) 38: ? 1 39: : 0; 40: } 41: 42: public Account getAccount() { 43: return account; 44: } 45: 46: public void setAccount(Account value) { 47: account = value; 48: } 49: 50: private String name; 51: private Date birthday; 52: private Account account; 53:}
List 8-1-3 : 年利計算クラス(AnnualRate.java) 1:package BankApplication; 2: 3:import java.math.BigDecimal; 4:import java.util.Calendar; 5:import java.util.Date; 6: 7:public class AnnualRate 8:{ 9: public AnnualRate() { } 10: 11: public BigDecimal computeAnnualInterest(Account account) { 12: Calendar today = Calendar.getInstance(); 13: today.setTime(new Date()); 14: 15: return account.getBalance().multiply(annualRate).divide( 16: new BigDecimal(365.0), BigDecimal.ROUND_HALF_UP).multiply( 17: new BigDecimal(today.get(Calendar.DAY_OF_YEAR)) 18: ).setScale(0, BigDecimal.ROUND_DOWN); 19: } 20: 21: private static final BigDecimal annualRate = new BigDecimal(0.0003); 22:}
List 8-2-1 : 口座クラス(Account.vb) 1:Public Class Account 2:#Region "Constructors & Destructors" 3: Public Sub New(ByVal Depositor As Depositor) 4: Me.Depositor = Depositor 5: End Sub 6:#End Region 7: 8:#Region "Public Instance Properties" 9: Public Property Balance() As Decimal 10: Get 11: Return m_Balance 12: End Get 13: Set(ByVal Value As Decimal) 14: m_Balance = Value 15: End Set 16: End Property 17: 18: Public Property Depositor() As Depositor 19: Get 20: Return m_Depositor 21: End Get 22: Set(ByVal Value As Depositor) 23: m_Depositor = Value 24: End Set 25: End Property 26:#End Region 27: 28:#Region "Public Instance Methods" 29: Public Function Withdraw(ByVal Amount As Decimal) 30: m_Balance -= Amount 31: Return m_Balance 32: End Function 33: 34: Public Function Deposit(ByVal Amount As Decimal) 35: m_Balance += Amount 36: End Function 37:#End Region 38: 39:#Region "Private Instance Fields" 40: Private m_Balance As Decimal 41: Private m_Depositor As Depositor 42:#End Region 43:End Class
List 8-2-2 : 預金者クラス(Depositor.vb) 1:Public Class Depositor 2:#Region "Constructors & Destructors" 3: Public Sub New(ByVal Name As String, ByVal BirthDay As Date) 4: Me.Name = Name 5: Me.BirthDay = BirthDay 6: End Sub 7:#End Region 8: 9:#Region "Public Instance Properties" 10: Public Property Name() As String 11: Get 12: Return m_Name 13: End Get 14: Set(ByVal Value As String) 15: m_Name = Value 16: End Set 17: End Property 18: 19: Public Property BirthDay() As Date 20: Get 21: Return m_BirthDay 22: End Get 23: Set(ByVal Value As Date) 24: m_BirthDay = Value 25: End Set 26: End Property 27: 28: Public ReadOnly Property Age() As Integer 29: Get 30: Dim today As Date = DateTime.Today 31: Dim intAge As Integer = today.Year - m_BirthDay.Year 32: 33: intAge += CInt(today.DayOfYear > m_BirthDay.DayOfYear) 34: Return intAge 35: End Get 36: End Property 37: 38: Public Property Account() As Account 39: Get 40: Return m_Account 41: End Get 42: Set(ByVal Value As Account) 43: m_Account = Value 44: End Set 45: End Property 46:#End Region 47: 48:#Region "Private Instance Fields" 49: Private m_Name As String 50: Private m_BirthDay As Date 51: Private m_Account As Account 52:#End Region 53:End Class
List 8-2-3 : 年利計算クラス(AnnualRate.vb) 1:Public Class AnnualRate 2:#Region "Constructors & Destructors" 3: Public Sub New() 4: End Sub 5:#End Region 6: 7:#Region "Public Instance Method" 8: Public Function ComputeAnnualInterest(ByVal Account As Account) As Decimal 9: Return Decimal.Truncate(Account.Balance * c_AnnualRate _ 10: / 365 _ 11: * Date.Today.DayOfYear _ 12: ) 13: End Function 14:#End Region 15: 16:#Region "Private Static Fields" 17: Private Const c_AnnualRate As Decimal = 0.0003 18:#End Region 19:End Class
© 2002 OGIS-RI Co., Ltd.
Prev. Index