PHP でオブジェクト指向の設計をするための 7 つの良い習慣を身につける

オブジェクト指向によって PHP アプリケーションをより良いものにする

PHP にはオブジェクト指向 (OO: Object-Oriented) 言語としての特徴があるため、皆さんが既に OO の原則を念頭に置いてアプリケーションを作成しているのでない場合には、この記事で紹介する 7 つの習慣を実践することによって手続き型プログラミングから OO プログラミングへの移行をスムーズに開始することができます。

Nathan A. Good (mail@nathanagood.com), Senior Information Engineer, Freelance Developer

Nathan A. Good lives in the Twin Cities area of Minnesota. Professionally, he does software development, software architecture, and systems administration. When he's not writing software, he enjoys building PCs and servers, reading about and working with new technologies, and trying to get his friends to make the move to open source software. He's written and co-written many books and articles, including Professional Red Hat Enterprise Linux 3, Regular Expression Recipes: A Problem-Solution Approach and Foundations of PEAR: Rapid PHP Development.



2008年 10月 28日

PHP プログラミングの初期の頃には、PHP コードは事実上、手続き型のコードとしての使われ方に限定されていました。手続き型のコードの特徴は、アプリケーションの構成ブロックにプロシージャーが使われることです。プロシージャーは、あるレベルでの再利用が可能であり、プロシージャーから他のプロシージャーを呼び出すことができます。

一方で、オブジェクト指向言語の構成体を使わなくても、PHP コードに OO の性質を持たせることはできます。ただし異なるパラダイム (手続き型言語と擬似的な OO 設計) を混在させることになるため、これを実際に行うのは少しばかり難しく、またコードが読みにくくなります。PHP コードで OO 構成体 (例えばクラスを定義したり使用したりできる機能や、継承を利用した関係をクラス間に作成できる機能、インターフェースを定義できる機能など) を利用すると、適切な OO のプラクティスに従ったコードを作成しやすくなります。

あまりモジュール化されていない、純粋に手続き型の設計でも実行には問題ありませんが、OO による設計は保守の際に力が発揮されます。典型的なアプリケーションではライフサイクルの大部分が保守に費やされるため、アプリケーションのライフサイクル全体の中でコードの保守にかかる費用は大きな部分を占めます。しかし開発中には保守のことは忘れられがちです。アプリケーションを開発してデプロイするのに必死になっている状況では、アプリケーションを動作させることが最優先になり、長期的なスパンで影響が出る保守性に関しては後回しにされてしまう可能性があります。

モジュール性は、適切な OO 設計の重要な特性の 1 つですが、モジュール性を持った設計をすると保守の際に役立ちます。モジュール性があれば変更をカプセル化することができ、開発完了から時間が経ってからでもアプリケーションの拡張や変更を容易に行えるようになります。

OO ソフトウェアの作成全般にわたって必要な習慣は 7 つだけではありませんが、ここで紹介する 7 つの習慣は、基本的な OO 設計の基準にあったコードを作成するために必要なものです。この 7 つの習慣は OO ソフトウェアを作成する上で確固たる基礎となるものであり、これらの習慣にさらに OO における習慣を追加したり、これらの習慣を土台に保守や拡張が容易なソフトウェアを作成したりすることができます。これらの習慣によって実現しようとしているのは、モジュール性が持つ重要な特性のうちのいくつかです。言語に依存しない OO 設計のメリットについての詳細は「参考文献」を参照してください。

PHP での適切な OO の習慣を 7 つ挙げると、次のとおりです。

  1. 控え目である
  2. 良き隣人である
  3. メドゥーサを見ないようにする
  4. 結びつきを極力弱くする
  5. 結束性を高める
  6. 家族の一員として扱う
  7. パターンで考える

控え目である

控え目であるということは、クラスや関数の実装の中で中身が見えないようにすることです。情報を隠すことは基本的な習慣です。実装の詳細を隠す習慣を身につけない限り、他のどのような習慣を身につけるのも難しいものです。情報を隠すことはカプセル化としても知られています。

public フィールドを直接公開することがなぜ不適切なのか、その理由は数多くありますが、なかでも最も重要な理由は、実装の中で何かが変更された場合にいかなる方法でも対処しきれなくなることです。OO の概念を使うと変更を分離することができ、またいかなる変更の影響も実質的に他には伝染しないことを確実にする上で、カプセル化が重要な役割を果たします。伝染性のある変更というのは、最初は些細な変更から始まるものです (例えば 3 つの要素を含む配列を変更して 2 つの要素しか含まない配列にする、など)。ところが突然、些細な変更であったはずのものが、その 1 つの変更に対応するために次々にコードを変更する羽目になっていることに気付くのです。

情報を隠すための手始めとして簡単な方法は、フィールドを private にし、public アクセサーを使ってフィールドを公開する方法です (public アクセサーは家の窓のようなものです)。つまり壁全体を外に向けて開け広げるのではなく、1 つか 2 つの窓のみを持つようにするのです。(アクセサーについては「適切な習慣: public アクセサーを使う」で詳しく説明します。)

フィールドを直接公開せずに public アクセサーを使うことで、実装が変更の影響を受けずにすむ上、アクセサーの実装をオーバーライドして親の振る舞いとは少し異なる振る舞いを実現できるため、基本実装をベースにした実装を作成することができます。また、基本実装をオーバーライドするクラスに実際の実装を委ねる抽象実装を作成することもできます。

不適切な習慣: public フィールドを公開する

リスト 1 に示す不適切なコード・サンプルでは、Person オブジェクトのフィールドは直接 public フィールドとして公開されており、アクセサーを使って公開されているのではありません。この動作は (特に軽量のデータ・オブジェクトの場合には) 魅力的ですが、この方法には制約があります。

リスト 1. public フィールドを公開する不適切な習慣
<?php
class Person
{
    public $prefix;
    public $givenName;
    public $familyName;
    public $suffix;
}

$person = new Person();
$person->prefix = "Mr.";
$person->givenName = "John";

echo($person->prefix);
echo($person->givenName);

?>

このオブジェクトで何かが変更されると、このオブジェクトを使うすべてのコードも変更する必要があります。例えば、ある人の名前、苗字、その他の名前が PersonName オブジェクトの中にカプセル化されていたとすると、その変更に対応するためにすべてのコードを変更しなければなりません。

適切な習慣: public アクセサーを使う

適切な OO の習慣を使うと (リスト 2)、同じオブジェクトには今や public フィールドの代わりに private フィールドがあり、そして private フィールドは public の get メソッドと set メソッドを使って注意深く外の世界に公開されています (これらのメソッドはアクセサーと呼ばれます)。このようにすると、これらのアクセサーによって PHP クラスからの情報を取得するための公開された方法が提供されるため、実装の中で何かが変更された場合にも、そのクラスを使用するすべてのコードを変更するという事態が起きる可能性は低くなります。

リスト 2. public アクセサーを使う適切な習慣
<?php
class Person
{
    private $prefix;
    private $givenName;
    private $familyName;
    private $suffix;
    
    public function setPrefix($prefix)
    {
        $this->prefix = $prefix;
    }
    
    public function getPrefix()
    {
        return $this->prefix;
    }
    
    public function setGivenName($gn)
    {
        $this->givenName = $gn;
    }
    
    public function getGivenName()
    {
        return $this->givenName;
    }
    
    public function setFamilyName($fn)
    {
        $this->familyName = $fn;
    }
    
    public function getFamilyName() 
    {
        return $this->familyName;
    }
    
    public function setSuffix($suffix)
    {
        $this->suffix = $suffix;
    }
    
    public function getSuffix()
    {
        return $suffix;
    }
    
}

$person = new Person();
$person->setPrefix("Mr.");
$person->setGivenName("John");

echo($person->getPrefix());
echo($person->getGivenName());

?>

一見、この方法の方がずっと面倒に思え、実際のところフロントエンドには先ほどの方法よりも多くの作業が必要です。しかし通常は、適切な OO の習慣を使うことによって、将来変更があったとしてもその範囲がかなり限られたものになるため、長い目で見れば十分な見返りがあります。

リスト 3 に示すコードでは、内部実装を変更して名前の部分に連想配列を使うようにしています。理想的には、もっとエラー処理を増やし、要素が存在するかどうかのチェックをもっと注意深く行いたいのですが、この例を紹介した目的は私のクラスを使用するコードに変更が必要ないことを示すことにあるため (このコードは私のクラスの変更にまったく影響を受けません)、そういったエラー処理は含めていません。OO の習慣を身につける理由は変更を注意深くカプセル化することにあり、それによってコードの拡張や保守をしやすくすることであることを忘れないでください。

リスト 3. この適切な習慣に少し手を加え、別の内部実装を利用する
<?php
class Person
{
    private $personName = array();
    
    public function setPrefix($prefix)
    {
        $this->personName['prefix'] = $prefix;
    }
    
    public function getPrefix()
    {
        return $this->personName['prefix'];
    }
    
    public function setGivenName($gn)
    {
        $this->personName['givenName'] = $gn;
    }
    
    public function getGivenName()
    {
        return $this->personName['givenName'];
    }

    /* etc... */
}

/*
 * Even though the internal implementation changed, the code here stays exactly
 * the same. The change has been encapsulated only to the Person class.
 */
$person = new Person();
$person->setPrefix("Mr.");
$person->setGivenName("John");

echo($person->getPrefix());
echo($person->getGivenName());

?>

良き隣人である

クラスを作成する際には、クラス内で発生したエラーはそのクラスの中で適切に処理するように作成する必要があります。クラスの中でエラーを処理できない場合には、それらのエラーを呼び出し側が理解できる形式でパッケージングする必要があります。さらに、ヌルまたは無効な状態にあるオブジェクトを返さないようにする必要があります。多くの場合、このためには単に引数を検証し、提供された引数がなぜ無効なのかを伝える特定の例外をスローすればよいだけです。この習慣を身につけると、皆さんの時間や、皆さんのコードを保守したり皆さんのオブジェクトを使用したりする人達の時間を大幅に節約することができます。

不適切な習慣: エラーを処理しない

リスト 4 に示す例を考えてみてください。この例では引数をいくつか受け取り、そして一部の値が入った Person オブジェクトを返しています。しかし parsePersonName() メソッドでは、提供された $val 変数がヌルなのか長さゼロのストリングなのか、あるいは解析不能なフォーマットのストリングなのかの検証を行っていません。このため parsePersonName() メソッドは Person オブジェクトを返さず、ヌルを返す場合が出てきます。こうなると、このメソッドを使う管理者やプログラマーは、何が悪いのかわからずに頭をかく羽目になるかもしれず、少なくとも PHP スクリプトにブレークポイントを設定してデバッグを始めなければならない状況になるかもしれません。

リスト 4. エラーをスローしない、またはエラーを処理しない不適切な習慣
class PersonUtils
{
    public static function parsePersonName($format, $val)
    {
        if (strpos(",", $val) > 0) {
            $person = new Person();
            $parts = split(",", $val); // Assume the value is last, first
            $person->setGivenName($parts[1]);
            $person->setFamilyName($parts[0]);
        }
        return $person;
    }
}

リスト 4 の parsePersonName() メソッドを変更して if 条件の外で Person オブジェクトを初期化するようにし、必ず有効な Person オブジェクトが得られるようにすることもできます。しかしその場合は何もプロパティーが設定されていない Person オブジェクトを取得することになり、あまり状況は改善されません。

適切な習慣: モジュール内で発生したエラーはモジュール内で処理する

呼び出し側にエラーの原因を推測させるのではなく、積極的に引数を検証する必要があります。ある変数が設定されていないために、有効な結果を生成できない場合には、その変数をチェックして InvalidArgumentException をスローします。ストリングが空であってはならない場合、あるいは特定のフォーマットでなければならない場合には、そのフォーマットをチェックして例外をスローします。リスト 5 は、独自の例外を作成する方法と、いくつかの基本的な検証を行うための新しい条件を parsePerson() メソッドの中に作成する方法を示しています。

リスト 5. エラーをスローする適切な習慣
<?php
class InvalidPersonNameFormatException extends LogicException {}


class PersonUtils
{
    public static function parsePersonName($format, $val)
    {
        if (! $format) {
            throw new InvalidPersonNameFormatException("Invalid PersonName format.");
        }

        if ((! isset($val)) || strlen($val) == 0) {
            throw new InvalidArgumentException("Must supply a non-null value to parse.");
        }


    }
}
?>

要するに、クラス内部の動作を知らなくても他の人がそのクラスを使えるようにする必要があるということです。他の人がそのクラスの使い方を誤ったり、あるいは作成者が意図しない方法で使ったりした場合にも、その人達はそのクラスが動作しなかった原因を推測する必要はないのです。良き隣人として、そのクラスを再利用する他の人達が超能力者ではないことを理解し、その人達が推測をしなくてもすむようにする必要があります。


メドゥーサを見ないようにする

私は OO の概念を初めて学んだとき、インターフェースが本当に役立つものか疑問に思っていました。すると私の同僚が比喩として、インターフェースを使わないのはメドゥーサの頭を見るのと同じだという話を紹介してくれました。メドゥーサは、ギリシャ神話に登場する、髪の毛が蛇にされた女性です。メドゥーサを直接見た人は誰でも石にされてしまいます。ペルセウスはメドゥーサを殺しますが、彼は自分の盾に映る彼女の姿を見ることで彼女に立ち向かうことができ、石にされずにすんだのです。

インターフェースはメドゥーサに立ち向かうための鏡すなわち盾に当たります。専用の具象実装を使ってしまうと、その実装コードが変更された場合に、それに合わせて皆さんの作成したコードも変更しなければなりません。実装を直接使うということは、実質的にクラスを石に変えてしまうことになるため、多くの選択肢が制限されてしまいます。

不適切な習慣: インターフェースを使用しない

リスト 6 はデータベースから Person オブジェクトをロードする例を示しています。この例では、指定された人の名前を取得し、データベースの中にある、その名前に一致する Person オブジェクトを返します。

リスト 6. インターフェースを使用しない不適切な習慣
<?php
class DBPersonProvider
{
    public function getPerson($givenName, $familyName)
    {
        /* go to the database, get the person... */
        $person = new Person();
        $person->setPrefix("Mr.");
        $person->setGivenName("John");
        return $person;
    }
}

/* I need to get person data... */
$provider = new DBPersonProvider();
$person = $provider->getPerson("John", "Doe");

echo($person->getPrefix());
echo($person->getGivenName());

?>

データベースから Person オブジェクトをロードするためのコードは、環境の中で何らかの変更が発生するまでは問題ありません。例えば、データベースから Person オブジェクトをロードする動作はアプリケーションの初期のバージョンでは問題ないかもしれませんが、2 番目のバージョンでは、ある人を Web サービスからロードできる機能が必要になるかもしれません。つまりこのクラスは実装クラスを直接使用しており、変更に対する柔軟性がないため、石になってしまうのです。

適切な習慣: インターフェースを使う

リスト 7 に示すコード・サンプルでは、ユーザーの情報をロードするために新たな方法が導入されて実装されてもコードを変更する必要はありません。この例は 1 つのメソッドを宣言する PersonProvider というインターフェースを示しています。PersonProvider を使用するどのコードも、そのコードが実装クラスを直接使うことがないようにし、PersonProvider をあたかも本物のオブジェクトであるかのように使用するようにします。

リスト 7. インターフェースを使う適切な習慣
<?php
interface PersonProvider
{
    public function getPerson($givenName, $familyName);
}

class DBPersonProvider implements PersonProvider 
{
    public function getPerson($givenName, $familyName)
    {
        /* pretend to go to the database, get the person... */
        $person = new Person();
        $person->setPrefix("Mr.");
        $person->setGivenName("John");
        return $person;
    }
}

class PersonProviderFactory
{
    public static function createProvider($type)
    {
        if ($type == 'database')
        {
            return new DBPersonProvider();
        } else {
            return new NullProvider();
        }
    }
}

$config = 'database';
/* I need to get person data... */
$provider = PersonProviderFactory::createProvider($config);
$person = $provider->getPerson("John", "Doe");

echo($person->getPrefix());
echo($person->getGivenName());
?>

インターフェースを使う際には、絶対に実装クラスを直接参照しないようにし、代わりにオブジェクト外部のものを使用して適切な実装を行うようにします。もし何らかのロジックに基づいてクラスが実装をロードするようにしても、そのクラスはやはりすべての実装クラスの定義を必要とするため、結局何も変わりません。

インターフェースを実装する実装クラスのインスタンスを作成するには、ファクトリー・パターンを使用することができます。factory メソッドは習慣的に create で始まり、そのインターフェースを返します。factory メソッドは、どの実装クラスを返せばよいかを factory が判断するために必要な任意の引数を取ることができます。

リスト 7 では、createProvider() メソッドは $type を引数として取っています。$typedatabase に設定されている場合には、ファクトリーは DBPersonProvider のインスタンスを返します。データ・ストアからユーザーの情報をロードするためにどのような新しい実装を使用する場合にも、このファクトリーとインターフェースを使用するクラスを変更する必要はありません。DBPersonProviderPersonProvider インターフェースを実装しており、内部に getPerson() メソッドの実際の実装を持っています。


モジュール同士を疎結合にすることは適切なことであり、そうすることによって変更をカプセル化することができます。他の習慣のうちの 2 つ (「控え目である」と「メドゥーサを見ないようにする」) は、互いに疎結合のモジュールを作成する上で役に立ちます。クラス同士を疎結合にするという最終的な目標を実現するためには、クラス同士の依存関係を減らすという習慣を身につけることです。

不適切な習慣: 密結合

リスト 8 では、依存関係を減らすことが、必ずしもオブジェクトを使用するクライアントにとって依存関係が減ることにつながっていません。この習慣で示したいのは、むしろ、適切なクラスへの依存関係を減らすことで、他の部分での依存関係も最小限にするというものです。

リスト 8. Address との密結合という不適切な習慣Address
<?php

require_once "./AddressFormatters.php";

class Address
{
    private $addressLine1;
    private $addressLine2;
    private $city;
    private $state; // or province...
    private $postalCode;
    private $country;

    public function setAddressLine1($line1)
    {
        $this->addressLine1 = $line1;
    }

		/* accessors, etc... */

    public function getCountry()
    {
        return $this->country;
    }

    public function format($type)
    {
        if ($type == "inline") {
            $formatter = new InlineAddressFormatter();
        } else if ($type == "multiline") {
            $formatter = new MultilineAddressFormatter();
        } else {
            $formatter = new NullAddressFormatter();
        }
        return $formatter->format($this->getAddressLine1(), 
            $this->getAddressLine2(), 
            $this->getCity(), $this->getState(), $this->getPostalCode(), 
            $this->getCountry());
    }
}

$addr = new Address();
$addr->setAddressLine1("123 Any St.");
$addr->setAddressLine2("Ste 200");
$addr->setCity("Anytown");
$addr->setState("AY");
$addr->setPostalCode("55555-0000");
$addr->setCountry("US");

echo($addr->format("multiline"));
echo("\n");

echo($addr->format("inline"));
echo("\n");

?>

リスト 8 を見ると Address オブジェクトの format() メソッドを呼び出すコードにはまったく問題がないように見えるかもしれません (このコードで行っていることは、Address クラスを使用して、format() を呼び出すことだけです)。その一方で、Address クラスには残念ながら問題があります。Address クラスは出力フォーマットを適切に設定するために使われるさまざまなフォーマッターについて認識している必要がありますが、そうすると Address オブジェクトは他の人にとって再利用しやすいものではなくなってしまいます (特にその人が format() メソッドの中にあるフォーマッター・クラスを使おうとしない場合には、それが一層顕著になります)。Address を使用するコードにはあまり多くの依存関係はありませんが、Address クラスには、たとえ Address クラスが単なるデータ・オブジェクトにすぎないような場合にも、非常に多くの依存関係があります。

つまり Address クラスは、Address オブジェクトの出力フォーマットの設定方法を認識している実装クラスと密結合されているのです。

適切な習慣: オブジェクト同士を疎結合にする

適切な OO 設計を行おうとする場合には、SoC (Separation of Concerns: 関心の分離) と呼ばれる概念を考慮する必要があります。SoC とは、オブジェクトが実際に関係を持たなければならない対象ごとにオブジェクトを分離することで、結合の度合いを低下させようとすることです。元の Address クラスは、Address クラス自体の出力フォーマットを設定する方法と関係を持たなければなりませんでした。これはおそらく適切な設計ではありません。本来 Address クラスでは、Address のさまざまな部分について扱わなければならないのであり、アドレスの出力フォーマットを適切に設定するための方法は何らかのタイプのフォーマッターが扱うべき内容なのです。

リスト 9 では、アドレスの出力フォーマットを設定するコードは、インターフェースや実装クラス、ファクトリーなどへ移され、「インターフェースを使う」という習慣が実践されています。ここでは、AddressFormatUtils クラスがフォーマッターの作成と Address の出力フォーマットを設定する作業を行います。これで、他のどのオブジェクトから Address を使用する際にも、フォーマッターの定義を必要とする心配がなくなります。

リスト 9. オブジェクト同士を疎結合にする適切な習慣
<?php

interface AddressFormatter
{
    public function format($addressLine1, $addressLine2, $city, $state, 
        $postalCode, $country);
}

class MultiLineAddressFormatter implements AddressFormatter 
{
    public function format($addressLine1, $addressLine2, $city, $state, 
        $postalCode, $country)
    {
        return sprintf("%s\n%s\n%s, %s %s\n%s", 
            $addressLine1, $addressLine2, $city, $state, $postalCode, $country);
    }
}

class InlineAddressFormatter implements AddressFormatter 
{
    public function format($addressLine1, $addressLine2, $city, $state, 
        $postalCode, $country)
    {
        return sprintf("%s %s, %s, %s %s %s", 
            $addressLine1, $addressLine2, $city, $state, $postalCode, $country);
    }
}

class AddressFormatUtils 
{
    public static function formatAddress($type, $address)
    {
        $formatter = AddressFormatUtils::createAddressFormatter($type);
        
        return $formatter->format($address->getAddressLine1(), 
            $address->getAddressLine2(), 
            $address->getCity(), $address->getState(), 
            $address->getPostalCode(), 
            $address->getCountry());
    }
    
    private static function createAddressFormatter($type)
    {
        if ($type == "inline") {
            $formatter = new InlineAddressFormatter();
        } else if ($type == "multiline") {
            $formatter = new MultilineAddressFormatter();
        } else {
            $formatter = new NullAddressFormatter();
        }
        return $formatter;
    }
}

$addr = new Address();
$addr->setAddressLine1("123 Any St.");
$addr->setAddressLine2("Ste 200");
$addr->setCity("Anytown");
$addr->setState("AY");
$addr->setPostalCode("55555-0000");
$addr->setCountry("US");

echo(AddressFormatUtils::formatAddress("multiline", $addr));
echo("\n");

echo(AddressFormatUtils::formatAddress("inline", $addr));
echo("\n");
?>

欠点は、当然のことながら、パターンを使用すると大抵の場合は成果物 (クラスやファイル) の量も増えるという点です。その一方で、成果物が増加したとしても、それぞれのクラスを保守する作業は減ることになり、再利用が適切に行われれば、さらに保守の作業が減ることになり、成果物が増えた影響を打ち消すことができます。


結束性を高める

関連のあるモジュールのあいだでは、結束性の高い OO 設計をすることに焦点が当てられ、そうした設計が体系的に行われます。関数とクラスを結束性が高くなるような構成にする方法を判断する上では、「関心」事項について学ぶことが重要です。

不適切な習慣: 結束性が低い

結束性の低い設計では、クラスとメソッドがうまくグループ化されていません。クラスとメソッドが乱雑に集められた結束性の低いコードには、スパゲティー・コードという言葉がよく使われます。リスト 10 はスパゲティー・コードの例です。このコードでは、比較的汎用性のある Utils クラスが多くの異なるオブジェクトを使用し、依存関係を数多く持っています。このクラスは少しずつさまざまなことをするため、再利用しにくくなっています。

リスト 10. 結束性が低い不適切な習慣
<?php

class Utils
{
    public static function formatAddress($formatType, $address1, 
        $address2, $city, $state)
    {
        return "some address string";
    }
    
    public static function formatPersonName($formatType, $givenName, 
        $familyName)
    {
        return "some person name";
    }
    
    public static function parseAddress($formatType, $val)
    {
        // real implementation would set values, etc...
        return new Address();
    }
    
    public static function parseTelephoneNumber($formatType, $val)
    {
        // real implementation would set values, etc...
        return new TelephoneNumber();
    }
}

?>

適切な習慣: 結束性が高い

結束性が高いということは、互いに関連するクラスとメソッド同士がグループ化されていることを意味します。メソッドとクラスの結束性が高ければ、設計に影響を与えずにグループ全体を容易に切り離すことができます。結束性の高い設計によって、疎結合を実現しやすくなります。リスト 11 では、メソッドが 2 つずつ適切なクラスに分類されています。AddressUtils クラスは Address クラスを処理するためのメソッドを含んでおり、またアドレスに関連するメソッド同士の結束性が高くなっています。同様に、PersonUtils クラスには Person オブジェクトを処理するための専用のメソッドが含まれています。これらの、高い結束性を持った複数のメソッドからなる 2 つの新しいクラスは、互いに完全に独立して使われることが可能なので、結合性は低いのです。

リスト 11. 結束性が高い適切な習慣
<?php

class AddressUtils
{
    public static function formatAddress($formatType, $address1, 
        $address2, $city, $state)
    {
        return "some address string";
    }
    
    public static function parseAddress($formatType, $val)
    {
        // real implementation would set values, etc...
        return new Address();
    }
    
}

class PersonUtils
{
    public static function formatPersonName($formatType, $givenName, 
        $familyName)
    {
        return "some person name";
    }
    
    public static function parsePersonName($formatType, $val)
    {
        // real implementation would set values, etc...
        return new PersonName();
    }
}

?>

家族の一員として扱う

私が技術リーダーや技術アーキテクトをしているソフトウェア・チームの人達に対して、私はよく、OO 言語の最大の敵はコピー・アンド・ペースト操作だと言い聞かせています。最初から OO 設計がされていない場合に、あるファイルから別のファイルへコードをコピーすることほど大きな混乱を招くものはありません。あるクラスから別のクラスへコードをコピーしようと思った時には必ず、いったん立ち止まり、クラスの階層構造を使うことによって類似の機能や同じ機能を活用できないかを考えてみることです。設計が適切であれば、ほとんどの場合はコードをコピーする必要がまったくないことに気付くはずです。

不適切な習慣: クラスの階層構造を使用しない

リスト 12 は部分クラスの簡単な例です。このコードでは、まずフィールドとメソッドをコピーすることから始めています。これは長期的に考えるとアプリケーションに変更が必要になる可能性があるため、適切なことではありません。Person クラスに欠陥があった場合、まず間違いなく Employee クラスにも欠陥が生じることでしょう。なぜなら 2 つのクラスの間では、あたかも実装がコピーされているのように見えるからです。

リスト 12. クラスの階層構造を使用しない不適切な習慣
<?php
class Person
{
    private $givenName;
    private $familyName;
}

class Employee
{
    private $givenName;
    private $familyName;
}

?>

継承を利用する習慣を身につけるのは難しいものです。それは多くの場合、適切な継承モデルを作成するための分析に大きな時間がかかるためです。逆に、Ctrl+CCtrl+V を使って新しい実装を作成するのは、ほんの数秒しかかかりません。しかしその時間の差は、実際にアプリケーションのライフタイムの大部分を占める保守の時間によってすぐに埋め合わせされます。

適切な習慣: 継承を活用する

リスト 13 では新しい Employee クラスが Person クラスを継承しています。この Employee クラスは今やすべての共通メソッドを継承しており、それらのメソッドを再実装することはしていません。またリスト 13 は、抽象メソッドを使うことによって基本的な機能を基底クラスに入れ、特定の機能を実装クラスに任せられることを示しています。

リスト 13. 継承を活用する適切な習慣
<?php
abstract class Person
{
    private $givenName;
    private $familyName;
    
    public function setGivenName($gn)
    {
        $this->givenName = $gn;
    }
    
    public function getGivenName()
    {
        return $this->givenName;
    }
    
    public function setFamilyName($fn)
    {
        $this->familyName = $fn;
    }
    
    public function getFamilyName()
    {
        return $this->familyName;
    }
     
    public function sayHello()
    {
        echo("Hello, I am ");
        $this->introduceSelf();
    }
    
    abstract public function introduceSelf();
    
}

class Employee extends Person
{
    private $role;
    
    public function setRole($r)
    {
        $this->role = $r; 
    }
    
    public function getRole()
    {
        return $this->role;
    }
    
    public function introduceSelf()
    {
        echo($this->getRole() . " " . $this->getGivenName() . " " . 
            $this->getFamilyName());
    }
}
?>

パターンで考える

デザイン・パターンはオブジェクトとメソッドによる一般的な対話動作であり、デザイン・パターンによってある種の問題を解決できることが長年の間に実証されています。デザイン・パターンで考える場合には、クラス同士がどのように対話動作するのかを無理にでも考えます。デザイン・パターンはクラスやクラスの対話動作を作成するための容易な方法であり、これまで他の人達がおかした同じ誤りをおかす心配がなく、また実証済みの設計によるメリットを生かすことができます。

不適切な習慣: 1 度に 1 つのオブジェクトのみを考える

パターンで考える方法を示す適切なコード・サンプルは実際にはありません (ただしパターンの実装を示す好例は豊富にあります)。しかし一般的に、次のような場合には 1 度に 1 つのオブジェクトしか考えないものだということはわかるはずです。

  • 事前にオブジェクト・モデルを図式化しない。
  • モデル化できる多くの部分を抽出せずに、各メソッドの実装のコーディングを始めてしまう。
  • 設計の話題の中でデザイン・パターンの名前を使わず、実装の話をしてしまう。

適切な習慣: パターンの中で作成されたオブジェクトを調和の取れた形で追加する

一般的に、パターンで考えるのは次のような場合です。

  • クラスとクラス同士の対話動作を事前にモデル化する場合。
  • クラスのパターンに従ってクラスをステレオタイプに当てはめる場合。
  • パターン名 (Factory、Singleton、Facade など) を使う場合。
  • モデル化できる大部分を抽出してから実装の追加を始める場合。

まとめ

PHP での適切な OO の習慣を身につけることによって、より安定していて、保守が容易で、拡張も容易にできるアプリケーションを作成することができます。そのためには、次のことを忘れないでください。

  • 控え目である
  • 良き隣人である
  • メドゥーサを見ないようにする
  • 結びつきを極力弱くする
  • 結束性を高める
  • 家族の一員として扱う
  • パターンで考える

こうした習慣を身につけ、使いこなせるようになると、皆さんはきっとアプリケーションの品質が変わったことに驚くはずです。

参考文献

学ぶために

製品や技術を入手するために

  • 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
  • IBM 製品の評価版をダウンロードし、DB2® や Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品をお試しください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=354447
ArticleTitle=PHP でオブジェクト指向の設計をするための 7 つの良い習慣を身につける
publish-date=10282008