PHPのコード例でSOLID原則を理解する(LSP編)~保守性の高いソフトウェア開発の実現を目指して~
ご無沙汰しております。
今回も前回の「PHPのコード例でSOLID原則を理解する(SRP・OCP編)」の続きで、理解しているつもり、使えているつもり、になっているかもしれないSOLID原則をおさらいしていきます。
今回はSOLID原則のうちの、「L」にあたる、
LSP(Liskov Substitution Principle、リスコフの置換原則)
を扱います。
個人的に最も理解に苦しんだ原則なので、今回はこのLSPだけを解説していきます。
前提
- PHP8.1.6
- スーパークラス、親クラス、基底クラス、スーパータイプ等をここでは、「スーパークラス」と代表して呼びます。
- サブクラス、子クラス、派生クラス、サブタイプ等をここでは、「サブクラス」と代表して呼びます。
LSP(Liskov Substitution Principle、リスコフの置換原則)とは?
「PHPで理解するオブジェクト指向の活用 ちょうぜつソフトウェア設計入門」によると、
派生クラスの振る舞いは、基底クラスの振る舞いを完全にカバーしなければならない
ということだそうです。つまり、サブクラスはそのスーパークラスと置換可能であり、サブクラスはスーパークラスの代わりとして、振る舞いを変更せずに使用できるべきである、ということです。
また、サブクラスはスーパークラスよりも事前条件は強めてはいけなく、事後条件は弱めてはいけません。こちらはこの記事の最後に改めて説明します。
では、コードを見て理解していきましょう。
原則違反コード
飛ぶというメソッドを持つ鳥クラスがあります。ペンギンは鳥の1種であるので、鳥クラスを継承してペンギンクラスが作れます(ペンギンは飛べないので fly() メソッドは例外を発生させます)。鳥はもちろん飛ぶだろうということを信じて、鳥を飛ばせる makeBirdFly() という関数を作りました。引数には Bird 型のオブジェクトを受け取ります。
Bird クラスのインスタンスを作成し、 makeBirdFly() 関数で飛ばすと、無事飛びました。Penguin クラスのインスタンスを作成して、 makeBirdFly() 関数で飛ばすと、飛ばずに「飛べません」という例外が発生しました。
<?php
class Bird
{
public function fly()
{
// flying logic
}
}
class Penguin extends Bird
{
public function fly()
{
// Penguin can't fly
throw new Exception('ペンギンは飛べません');
}
}
function makeBirdFly(Bird $bird)
{
$bird->fly();
}
$bird = new Bird();
$penguin = new Penguin();
makeBirdFly(bird: $bird); // OK
makeBirdFly(bird: $penguin); // Uncaught Exception: Penguin can't fly!
原則違反箇所
Pengiun クラスは Bird クラスの(サブクラス)であるのにもかかわらず、 Bird クラスのオブジェクトを引数に取る makeBirdFly() 関数を使えません。この点が原則違反です。繰り返しますが、LSPはスーパークラスとが完全に代替可能でなければいけません。
では、原則に則るようなコードとはどんなものでしょうか。
原則準拠コード
「継承を使ったLSPに準拠したコード」と「インターフェースを使ったLSPに準拠したコード」の2つのパターンを説明します。
継承を使ったコード
<?php
class Bird
{
public function eat()
{
// eating logic
}
}
class Sparrow extends Bird
{
public function fly()
{
// flying logic
}
}
class Penguin extends Bird
{
// don't have a fly() method because penguin can't fly
}
function makeBirdEat(Bird $bird)
{
$bird->eat();
}
$sparrow = new Sparrow();
$penguin = new Penguin();
makeBirdEat(bird: $sparrow); // OK
makeBirdEat(bird: $penguin); // OK
やったこと
- 鳥は生きるために絶対に食べるので、「食べる」能力を持つ eat() メソッドを持つ Bird クラスを作成します。
- Sparrow クラスも Penguin クラスも Bird クラスを継承します。
- makeBirdEat() 関数は引数に Bird クラスのインスタンスを引数に取ります。
思考の流れ
- 鳥でも、飛ぶ鳥と飛べない鳥が存在するが、「食べる」能力を共通で持つ。
- 鳥の共通の能力である「食べる」を表現するスーパークラス(サブクラス) Bird を作成。
- 飛ぶ鳥は「食べる」能力以外に、「飛ぶ」能力を持つので、例えばスズメの Sparrow クラスを作り、 Bird を継承した上で、 fly() メソッドを持たせる。
飛ばない鳥は「食べる」能力のみを持ち、例えばペンギンの Penguin クラスを作り、 Bird を継承する。 - 実際に食べさせる makeBirdFly() 関数を作成し、引数に Bird クラスが渡される。
- makeBirdEat() 関数は Sparrow クラスも Penguin クラスも問題なく受け取れる。
ポイント
Bird クラスの(サブクラス)である Sparrow クラスも Penguin クラスも、 makeBirdEat() 関数の引数に問題なく渡されます。言い換えると、 Bird クラスが保証する全ての能力(今回は「食べる」)が Sparrow クラス、 Penguin クラスによって満たされている状態です。つまり(サブクラス)でスーパークラス(スーパークラス)を完全に代替可能な状態になっています。インターフェースを使ったコード
<?php
interface FlyingBird
{
public function fly();
}
class Sparrow implements FlyingBird
{
public function fly()
{
// flying logic
}
}
class Penguin
{
// Can't implement FlyingBird interface because penguin can't fly
}
function makeBirdFly(FlyingBird $bird)
{
$bird->fly();
}
$sparrow = new Sparrow();
$penguin = new Penguin();
makeBirdFly(bird: $sparrow); // OK
makeBirdFly(bird: $penguin); // Uncaught TypeError: makeBirdFly(): Argument #1 ($bird) must be of type FlyingBird
やったこと
- 飛ぶ鳥と飛ばない鳥がいることを認識しているので、「飛ぶ」という動作を表現する fly() メソッドを考え、それを定義する FlyingBird インターフェースを作成し、飛ぶ鳥は全てこのインターフェースを実装するようにします。
- makeBirdFly() 関数は引数に飛ぶ振る舞いを持っている FlyingBird インターフェースを実装したクラスだけ受け取るようにします。
思考の流れ
- 鳥でも、飛ぶ鳥と飛べない鳥が存在するので、「飛ぶ」動作ができる鳥達を FlyingBird インターフェースで表現します。
- スズメは「飛ぶ」動作ができるので FlyingBird インターフェースを実装します。
一方、ペンギンは「飛ぶ」動作ができないので実装しません。 - 実際に飛ばさせる makeBirdFly() 関数を作成し、引数には FlyingBird インターフェースが渡されるようにする。
- makeBirdFly() 関数を使って、「飛ぶ」動作ができる鳥は実際に飛び、そうでない鳥は飛ばない。
ポイント
「飛ぶ」動作ができる鳥とできない鳥を認識し、「飛ぶ」動作ができる鳥を表現するために、 FlyingBird インターフェースを実装する。このインターフェースを実装した鳥はもちろん makeBirdFly() 関数に問題なく渡されます。どんな鳥のクラスであっても、このインターフェースを実装していれば必ず飛べますし、実装していなければ飛べません。
事前条件・事後条件
事前条件とは、メソッドが呼び出される処理やインスタンスが生成される処理等何かの処理の前に満たされるべき条件のセットです。その処理が正常に機能するために必要な条件を定義します。
事後条件とは、メソッドが呼び出された後の処理やインスタンスが生成された後の処理等何かの処理の後に満たされるべき条件のセットです。その処理の後に続く処理が正常に機能するために必要な条件を定義します。
その上で、
- サブクラスの事前条件はスーパークラスより強めることは出来ない
- サブクラスの事後条件はスーパークラスより弱めることは出来ない
についてみていきます。具体的なコードで見ると分かりやすいです。
事前条件のコード例
<?php
class Bird
{
public function setSpeed($speed)
{
if ($speed < 0) {
throw new Exception("速度は正の数である必要があります。");
}
}
}
class Sparrow extends Bird
{}
class Penguin extends Bird
{
public function setSpeed($speed)
{
if ($speed > 5) {
throw new Exception("ペンギンはそんなに速く走れません。");
}
}
}
スーパークラスである Bird クラスは速度が正の数であればなんでも良いが、サブクラスである Penguin クラスは速度が正の数である、かつ、5より小さくないといけないことになってます。サブクラスの受け取れる引数の条件が厳しくなっていますよね(事前条件が強まった)。
つまり、この例では「事前条件を強める=スーパークラスのメソッドが受け付ける引数やその範囲を、サブクラスがより狭く制約することを意味する」 と理解でき、これはLSPの違反に繋がことになってしまいます。
事後条件のコード例
<?php
class Bird
{
protected $altitude = 0;
public function fly()
{
return $this->altitude;
}
public function setAltitude($altitude)
{
$this->altitude = $altitude;
}
}
class Sparrow extends Bird
{}
class Penguin extends Bird
{
public function setAltitude()
{
$this->altitude = 0; // penguin can't fly so the altitude is 0
}
}
$bird1 = new Bird();
$bird2 = new Penguin();
$bird1->setAltitude(10);
$bird2->setAltitude(10);
echo $bird1->fly(); //=> 10
echo $bird2->fly(); //=> 0
この時、 Bird クラスと Penguin クラスのインスタンスをそれぞれ作成して、高度を設定して、いざ飛ばせた時に、返ってくる結果が違いました。 Bird クラスでは任意の高度を設定できたのに、 Penguin クラスでは問答無用に0が設定されます。これはスーパークラスの動作をサブクラスで保証できていないことを意味しています。
つまり、この例では「事後条件を弱める=スーパークラスのメソッドが保証していた条件を、サブクラスのメソッドが保証しなくなることを意味する」と理解でき、これはLSPの違反に繋がことになってしまいます。
おわりに
今回はSOLID原則のうちのLに当たるLSPに関して説明しました。原則違反コード→原則準拠コードの順で追うと結構理解しやすいのではないか?と思います。
次回はIとDに当たるISPとDIPに関して説明出来ればと思います。
この記事を書いた人
-
大学4年時春に文系就職を辞め、エンジニアになることを決意し、独学でRuby、Ruby on Railsを学習。
約1年間の独学期間を経てアーティスへWebエンジニアとして入社。現在はWebエンジニアとして、主にシステムの開発・運用に従事している。
抽象的なもの、複雑なものを言語化して文章にするのが好きで得意。
この執筆者の最新記事
- 2024年10月8日WEBVue3でjQueryのdatepickerを使いたい!実装手順と注意点を解説します。
- 2024年8月21日WEBVue3の非同期コンポーネントを使ってみる
- 2024年5月28日WEBLaravel×Inertia×Vue3でファイルアップロード機能を作ってみた
- 2024年4月15日WEBLaravel×Inertia×Vue3でCRUD機能を持つSPAを作ってみた
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー