Software Engineering Lecture s9
Menu Menu
先週の復習
- ステートチャート
- 状態の実装
- Callback, EventLoop
ソフトウェア・パターン
オブジェクトは、このようなオブジェクトの組み合わせ(Object Composition) で作られていく。これを、一つの知識体系としてまとめたものの一つが、デザイン・パターンあるいは、ソフトウェア・パターンと呼ばれるものである。オブジェクト指向プログラミングでは、GoF パターンと呼ばれるものが有名である。(Gang of Four)状態遷移の作り方は一つのデザイン・パターンである。
デザイン・パターン (ソフトウェア・パターン)
作成するプログラムが複雑になって来ると、単一のデータ構造とアルゴリズムを使用する方法では対処できない。データ構造は、必然的に、それを処理するアルゴリズム(メソッド)と対応し、オブジェクトを構成する。メソッドは、オブジェクトのクラス記述に分散されてしまうが、予測される変更に対して、変更部分がプログラムソースのさまざまな部分に分散されてしまうのは好ましくない。特に、同じような処理をする部分がコピーにより複数の部分に記述されていると、一部を修正/変更したりすると、それが、バグの原因になってしまう。
オブジェクトの構成法には「こつ」があり、それを自分で習得すると供に、プログラムを作っているグループ、あるいは、弟子や学生に伝えていくこと(共有していくこと、知識として集約していくこと、発展させていくこと) が重要である。それは、従来、「ソースを読むこと」によってでしか学習することは出来なかった。もちろん、それは、今でも重要であるが、これらを記述する方法の一つとして「ソフトウェア・パターン」がある。
ソフトウェア・パターンは、建築技術をルールの集合として記述する手法を提案してきたアレキサンダーと、GoF (Gang of Four)として知られる「デザイン・パターン」を設計した E. Gamma, R. Helm, R. Johnson, J, Vlissides によって90年代に確立した手法である。UML記法と組み合わせることにより、ソフトウェア業界での共通言語となっているといえる。
パターンは、パターンそのものよりも、パターンの記述法に着目されることも多い。ただし、その有効性は、まだ、はっきりしていない。
パターンを作るための道具立て
ソフトウェア・パターンは、原理的にはプログラム言語に依存しない。パターン自体は、非オブジェクト指向言語にも適用できる。しかし、オブジェクト指向言語を使うことにより、より、効果的にパターンを記述することが出来る。オブジェクト指向言語の機能としては以下のようなものがある。
インスタンスの生成 (Class / Instance / Constructor) クラスという型紙から、異なる状態を持つオブジェクトを生成する 継承 (Inheritance) 一つのクラスに対して、追加のメソッドやインスタンス変数のみを記述する ことにより、共通部分を再利用する。効率的な実装が可能。 多重継承 (Multieple Inheritance, Mix-in) 複数のクラスに対する継承を実現する。オブジェクトの追加の機能の 表現として使う場合もある。(Mix-in, Flavor) 整合的な実装は難しい。 抽象クラス 継承されることを想定したメソッドの実装を持たないクラス定義 使用されるインスタンス変数などを前もって規定する。 移譲 複数のインスタンスへ処理を移行する手法。構文でサポートされず、 プログラム手法的に実現する場合が多い。多重継承をソフト的に実現 する手法として優れている。 インタフェース あるオブジェクトが持つべきメソッドの集合の指定。抽象クラスと 異なり、インスタンス変数を規定しない。抽象クラスで代用可能だが、 コンパイラのサポートを受けられない。Objective-C では、protocol と呼ばれる。Java では、implements を使うことにより、コンパイラ によるチェックが可能。 MessageDoesNotUnderstand オブジェクトの対応するメソッドがないことを表す、メタなメソッド。 Object クラスで実現されることもある。 メタ・オブジェクト オブジェクトの実行そのものを取り扱うメソッドを記述するオブジェクト。 Threadの優先順位の変更などはメタ・メソッドである。
単一継承と多重継承
オブジェクト指向プログラミングでは、プログラムの再利用ということが言われる。これは実際のプログラム技術としては、継承というメカニズムを使って行なわれる。このメカニズムには、大きく分けて二つの方法がある。一つは、継承できるclass は一つしか無いというものであり、もう一つは、複数のclassを継承することを許す方法である。
単一継承では、プログラムのコードの共有は単純であり再利用は上位のclass の変更と言う形で行なわれる。個々のメソッドの基本的なものは上位のclass で決めるのが普通である。これを逆に利用して、上位のclassにはわざと実装を記述しないと言う方法もある。このようなclassを抽象class (abstract class)という。これは、このclassを継承するオブジェクトに必要なプロトコルを規定していると考えることもできる。単一継承にはインスタンス変数のオフセットを簡単に決めることができるという実装上の利点もある。
多重継承では、個々のclassを部品として構成することが多い。必要な部品を集め、それぞれの機能を利用してプログラムを作りあげる。この方がコードの再利用率は高くなる。その一方で、それぞれの部品の整合性を取ることは難しくなる。また、単一継承の場合と異なり、インスタンス変数のオフセットを単純に決めることができない。また、多重継承は、インスタンス変数に部品となるオブジェクトを格納しておき、必要な時に、そのオブジェクトに処理を委託するという方法で実現することもできる。これは委譲(delegate)と呼ばれる。
多重継承でも抽象クラスを使うことができるが、抽象クラスの役割だけを、取りだす方法もある。これはインタフェースとかプロトコルと呼ばれることが多いようである。例えば、単一継承のシステムで、受け入れられるメソッドの種類だけを記述する記述法を用意して、自分が使う変数に対して、インタフェースを指定するというように使う。
継承の単調増加性
もし、継承によりオブジェクトの属性や機能が増えるだけならば、それは単調増加であると呼ばれる。クラスの構造は、できるだけ、単調増加であることが望ましい。しかし、それが可能でない場合もでてくる。例えば、鯨は哺乳類だが、足がない。哺乳類のクラスに足を属性として入れてしまうと、鯨にも足が必ずあることになってしまう。
この場合、鯨のオブジェクトを生成する際に、足の属性を削除することもできるが、例えば、walk などのメソッドも一緒に削除する必要がある。これらは、メソッドを明示的に overwrite することにより実現する場合が多い。
オブジェクトの属性にアクセスする場合に、オブジェクトのインスタンス変数を直接参照する方法を使うと、このような制限を付けることはできなくなってしまう。したがって、オブジェクトの汎用性を上げたいならば、インスタンス変数に対するアクセスは、すべてメソッド経由で行なうべきである。
オブジェクトの属性とクラス
クラスはオブジェクトの属性でもある。SimpleCard.pm を、Visible Card と Hidden Card の二つのクラスに分割して実装することを考える。Perl による実装を継承を用いて記述し、この方法の利点と欠点を考察せよ。
抽象クラスとインターフェース
もし、あるクラスの記述が実装を持たないならば、それは、そのクラスが持つべきメソッドの集合を示す枠組となる。このようなクラスを抽象クラス (Abstract Class)と言う。抽象クラスは、属性値の集合を定義した、データベース・レコードとして使う場合がある。ある特定の抽象クラスを継承していれば、そのデータベースとしての性質を持っていると期待できる。この場合、属性値が継承によって削除されてしまうことは、望ましくない。したがって、抽象クラスの継承は、単調増加性を保証すべきであると考えられる。この単調増加性を保証するため、抽象クラスは単一継承されるのが普通である。(本当か?)一方で、あるオブジェクトが持つ 機能(APIやメソッド) の集合 を確認したい場合がある。それが明確になれば、安心して、そのオブジェクトにアクセスすることができる。直接的には、コンパイラなどにより、実装されていないメソッドなどの呼出しを検出することができる。特に、分散オブジェクトなど、他の環境化での実装を、オブジェクト定義時に参照できない場合には、これが重要である。このメソッドの集合を規定したものをインターフェースと呼ぶ。
インターフェースは、抽象クラスを使って記述することも出来る。しかし、それらは、まったく別なものであることを理解する必要がある。Java やObjective-C では、インターフェースはクラスの継承とは別な構文として用意されている。C++ では、特別な構文がないので、抽象クラスを多重継承することにより、実現することが多い。
抽象クラスでは属性は必ず単調増加的に継承される必要がある。しかし、インタフェースは、あくまでもメソッドの集合だけを記述しているので、属性の内容には立ち入らない。インタフェースは属性値を増やさないので、多重継承しても実装上の問題は生じないと言う利点がある。
単一継承、多重継承の Perl での実装
Perl では、@ISA という特殊変数にパッケージを複数記述することにより、単一継承、多重継承を実現することができる。
委譲と継承
継承は、オブジェクト指向言語の機能として実現されていてるが、委譲は、それとは関係なく自分で実装することができる。Perl でも、
package class1; sub method1 { my $self = shift; $self->{-delegate1}->method1(@_); }みたいな形で委譲を実現することができる。これは、class1 のオブジェクトと、$self->{-deletage1} のオブジェクトの協調動作ということになる。
委譲を使うと、継承と異なり、委譲先を実行時に変更することができる。また、必要ならば、委譲先を、lazy に生成することもできる。また、メソッドの選択を継承のように言語の機能に頼ることなく、自分で行うことができるのも利点である。
ただし、メッセージの転送が必要になるので、実行のオーバヘッドは若干生じる。これは、適応性や再利用性をとるか、実行時の効率をとるかという問題になる。
オブジェクトは、このようなオブジェクトの組み合わせ(Object Composition) で作られていくようにすると、継承よりも、より柔軟な構造を作ることができる。
メタオブジェクト
もう一つの関係は、オブジェクトの性質に関連する。これは、クラスの参照関係でもある。クラスは、オブジェクトのメタな性質を表していると言うことがある。meta というのは、高階記述(Higher Order) 手続きそのものではなくて、手続きの変更などの記述 実装記述(Implementation) 手続きそのものではなくて、手続きを他の(多くの場合、より基本的な)機構により記述することを指すことが多い。
継承関係は、オブジェクトの持つ手続きの間の相互関係なので、メタな関係である。
Perl では、継承関係は、無名ハッシュの共有と、複数パッケージの手続き(サブルーチン)を結びつけることで実現されている。
問題9.1
ファイルに対する操作には、block I/O, sequential I/Oがある。Unix では、それらは、write/read と seek によって実現される。また、これに対してbuffering を使った、fwrite, fread, fgetc, fputc、そして、入出力をstdioに限定した、getchar, putchar がある。これらをclass として実現するとしよう。可能な多重継承による構成と、単一継承による構成を考えて、それぞれの利点と欠点について考察せよ。これらを、Perl のオブジェクトを使用して実装してみよ。多重継承や単一継承の利点と欠点は、Perl のオブジェクトでは、どのように現われたかを考察せよ。
以上の構成で、どのクラスが抽象クラスであり、インタフェースであるべきかを考察せよ。
オブジェクト作成のパターン
Factory PatternAbstract Factory
Builder
Factory Method
Prototype
Singleton
Factory Pattern
同じ機能を持つが、異なる表現を生成するオブジェクトがあった場合に、アプリケーションが直接、その「異なる表現の対応するオブジェクト」を生成するようにすると、その表現を変えることが難しくなってしまう。そこで、その機能を指定するインスタンスを一つ作り、そのインスタンスに、インスタンスの生成を依頼する。
Command Pattern
ユーザが指定するコマンドに対応するオブジェクトを作り、そのコマンドの列を生成することにより、undo/redo を実現する。
State Pattern
状態遷移を、状態に対応するオブジェクトを作成することにより実現する。
問題9.2 state pattern
前回の信号機または、電子レンジの例題を、State Pattern を用いて、実装せよ。
問題9.3
CardPlay のプログラムを調べて、そこで使われているパターンを指摘せよ。また、GoF パターンを使用できる部分を指摘し、実装してみよ。その場合の利点と欠点について、考察せよ。