implの藤谷です。
抽象クラスや具象クラスの使い方、オブジェクト指向的な設計について解説します。
interfaceと具象クラス
interface(抽象クラス)は言語に関わらずoop共通の機能で、継承したクラスにメソッドの実装を強制することを目的としたクラスです。
この後Animalクラスを際に解説を行うので、interfaceの例はそこで使用するAnimalInterfaceになります。
<?php
interface AnimalInterface
{
public function getName(): void;
}
上記のinterfaceを継承(implements)したクラスは、void型を返却してpublicアクセスを持ったgetNameメソッドを実装していなければコンパイル時点でエラーを出すようになります。
次に全体の親クラス(Animal)と、AnimalInterfaceの具象クラスとなるDogクラスとCatクラスをmain.php
に実装します。
先にサンプルで出したAnimalInterfaceも含めます
<?php
class Animal
{
public function renderAnimalName(Dog $dog)
{
return $dog->getName();
}
}
interface AnimalInterface
{
public function getName(): void;
}
class Dog implements AnimalInterface
{
private string $name = 'dog01';
public function getName(): void
{
echo $this->name;
}
}
class Cat implements AnimalInterface
{
private string $name = 'cat01';
public function getName(): void
{
echo $this->name;
}
}
$dog = new Dog();
$animal = new Animal();
return $animal->renderAnimalName($dog);
現状のmain.phpを実行すると、インスタンス化して使用しているDogクラスのgetNameメソッドが実行されて「dog01」が出力されます
$ php main.php
dog01
この時点で、interfaceのメリットである「具象クラスへのメソッド実装強制」が適用されています。
定義するメソッド名や型を間違えればコードがエラーを出してくれるし、新しく同じメソッドを二つの具象クラスに実装する場合も安全に改修できます。
ここまでを見て「実行速度、ステップ数の削減」といったソフトウェアのパフォーマンス向上に、interfaceはあまり寄与していないように思います。
これなんの意味があるんだろう…と色々調べていた時に、下記の一文を見つけました。
アーキテクチャとはソフトウェアのパフォーマンス向上ではなく、ソフトウェア開発のパフォーマンス向上に効果的である
この視点があれば、「タイプミスの予防、命名の統一…」等々、様々な場面での効果が期待できます。
interfaceと依存性の解消
現状のmain.phpではDocクラスとAnimalクラスをインスタンス化していますが、「DogではなくCatを使用したい」という仕様変更があったとします。
要求に応えるべく、コードを改修します。
結果として変更した部分は、下記のコードでコメントアウトのある「2点」です。
<?php
class Animal
{
// DIをDogからCatに変更
public function renderAnimalName(Cat $cat)
{
return $dog->getName();
}
}
interface AnimalInterface
{
public function getName(): void;
}
class Dog implements AnimalInterface
{
private string $name = 'dog01';
public function getName(): void
{
echo $this->name;
}
}
class Cat implements AnimalInterface
{
private string $name = 'cat01';
public function getName(): void
{
echo $this->name;
}
}
// DogではなくCatをインスタンス化
$dog = new Cat();
$animal = new Animal();
return $animal->renderAnimalName($cat);
$ php main.php
cat01
仕様通りの改修に成功しましたが、一つ問題点があります。Catクラスの変更に、Animalクラスの変更が必要だったことです。
これが「依存性が高い」ということであり、具体(具象クラス)に依存している状態です。
SOLID原則の一つである依存性逆転の原則に反していて、変更の度にこのような複数箇所の修正を余儀なくされます。
この課題を解決する為に、interfaceを使用します。
<?php
class Animal
{
// 具象クラスではなく、Interface(抽象クラス)をDIする
public function renderAnimalName(AnimalInterface $animalInterface)
{
return $animalInterface->getName();
}
}
interface AnimalInterface
{
public function getName(): void;
}
class Dog implements AnimalInterface
{
private string $name = 'dog01';
public function getName(): void
{
echo $this->name;
}
}
class Cat implements AnimalInterface
{
private string $name = 'cat01';
public function getName(): void
{
echo $this->name;
}
}
$cat = new Cat();
$animal = new Animal();
return $animal->renderAnimalName($cat);
AnimalクラスのDI部分にコメントアウト通りの変更を加えました。
main.phpを実行してみます。
$ php main.php
cat01
実行できました。
interfaceクラスはimplementsした具象クラスのメソッドにアクセスする機能があり、それを使用して抽象から具象を呼び出すポリモーフィズム的な実装ができています。
この状態で、先程は2点の変更を必要とした「DogとCatの付け替え」の変更を行います。
<?php
class Animal
{
public function renderAnimalName(AnimalInterface $animalInterface)
{
return $animalInterface->getName();
}
}
interface AnimalInterface
{
public function getName(): void;
}
class Dog implements AnimalInterface
{
private string $name = 'dog01';
public function getName(): void
{
echo $this->name;
}
}
class Cat implements AnimalInterface
{
private string $name = 'cat01';
public function getName(): void
{
echo $this->name;
}
}
// CatではなくDogをインスタンス化
$dog = new Dog();
$animal = new Animal();
return $animal->renderAnimalName($cat);
$ php main.php
dog01
AnimalInterfaceが2つの具象クラスを吸収し、実行するインスタンスの付け替えのみで使用通りの変更を行えました。
同じinterfaceを継承しているのでメソッド名が同じであることを保証でき、結果的にDogクラスの変更時にAnimalクラスに影響を及ぼさずに改修ができました。
親のAnimalクラスと、子のDogクラスやCatクラスとの依存性をInterfaceが吸収し、抽象に依存したアーキテクチャになっています。
今後、SheepクラスやMonkeyクラスを追加実装したいという時も、追加クラスの実装とインスタンス化のみでAnimalクラスには変更の影響が及びません。
oopがアジャイルとの相性がいいというのも、この様に後からの改修に強いところが影響していそうです。
最終的にInterfaceの効果で依存性が低く、改修容易性の高いコードにすることができました。
終わりに
最近の開発でDB中心の設計ではなく、オブジェクト中心の設計にする事を意識しています。
バックエンドの実装はコードが汚く責任の分離がされていなくても「DBの読み書きを行い、返却値をView層に渡す」ことができれば、アプリケーションとして最低限の機能を果たすことができます。
そうではなく、オブジェクトの階層や依存性のコントロールを適切に行い、必要に応じてDBとやり取りしてその値をまた適切に扱う。みたいな事を軸に詳細設計レベルやコーディングを考えています。
当たり前のことですが、言葉にしつつ意識してコードを見てみるといい発見が多くありました。
今回は以上になります。
ここまで読んでいただき、ありがとうございました。