こんにちは。
最近は、バーチャルキャストというVRライブ・コミュニケーションサービスで美少女になって配信をすることにハマっているゆいもっぷです。
さて
オブジェクト指向。
うまく扱えれば、保守性、機能追加・修正などにすぐれ
コードの品質を保ちやすくなります。
逆にうまく扱うことができなければ、当然保守性は悪くなり、機能追加・修正はしにくくなります。
そしてスパゲティコードのできあがりです。
そんなコード、ゆいさんは触りたくないです
ゆえに、自分の昔のコードを見ると吐き気に襲われます←
本来、保守性を高めて管理しやすくしようというのが特性であるのに、
コードが増えれば増えるほど汚くなっていく……。
もっときれいなコードを書きたいんだ!
っていうそこのあなた!
基本的な原則を覚えましょう!
ということで、SOLID原則ですっ!原則原則ぅ〜!
SOLIDの原則 #とは
ソリッドスネークのことである
5つの原則の頭文字をとったもので、それぞれ
・Single Responsibility Principle:単一責任の原則
・Open/closed principle:オープン/クロースドの原則
・Liskov substitution principle:リスコフの置換原則
・Interface segregation principle:インターフェース分離の原則
・Dependency inversion principle:依存性逆転の原則
となっています。
単一責任の原則(Single Responsibility Principle)
Classを変更する理由は1つでなければならない
class とある社員
{
public function getName()
{
return '山田太郎';
}
public function getBelogsTo()
{
return '総務';
}
public function pourDrink()
{
return 'オチャドゾー';
}
public function copyDocument()
{
return 'コピーしました!(`・ω・´)ゞ';
}
}
こんなクラスがあったとします。シンプル!
名前を聞けば答えてくれるし、飲み物を欲すれば注いでくれます。
すごく小規模なものなので、この程度であれば特に気にするところはないでしょう。
が、後々規模が大きくなって複雑になったときに問題は出てきます。
例えば上記の社員さん、人事異動となり『総務』から『人事』に所属先が変わったとします。
その場合getBelogsTo()メソッドに変更が入るでしょう。
そして、飲み物を注いでほしい時はお茶ではなくてコーヒーが欲しくなったとしましょう。
その場合pourDrink()メソッドにも変更が入ります。
これではクラスを変更する理由が2つになってしまいますね。
では、役割ごとにクラスを分割してみるとどうでしょうか?
class とある社員
{
public function getName()
{
return '山田太郎';
}
public function getBelogsTo()
{
return '総務';
}
public function pour(Drink $drink)
{
return $drink->pour;
}
public function copy(Copy $copy)
{
return $copy->copy;
}
}
Interface DrinkInterface
{
public function pour();
}
class Drink implements DrinkInterface
{
public function pour()
{
return 'オチャドゾー';
}
}
interface CopyInterface
{
public function copy();
}
class Copy implements CopyInterface
public function copy()
{
return 'コピーしました!(`・ω・´)ゞ';
}
}
これで飲み物を入れる動作や所属先を変更したとしても、一方のクラスへの変更はないため
保守性・拡張性が高くなります。
2020/01/15追記
Interface使ってるんだから タイプヒンティングもInterFaceを指定しようね!
飲み物ドゾーするのは『とある社員』の仕事だよぉ!!!おら!!はよドゾーしろ!!
class とある社員
{
public function getName(): string
{
return '山田太郎';
}
public function getBelogsTo(): string
{
return '総務';
}
public function pour(DrinkInterFace $drink): string
{
return sprintf('%sドゾー', $drink->getName());
}
}
Interface DrinkInterface
{
public function getName(): string;
}
class Tea implements DrinkInterface
{
public function getName(): string
{
return 'オチャ';
}
}
class Milk implements DrinkInterface
{
public function getName(): string
{
return 'ギウニウ';
}
}
オープン・クローズドの原則(Open/closed principle)
Classは拡張に対して開かれて、修正に対して閉じられていなければならない
public function print($type)
{
$printer = new Printer();
if ($type == 'HTML') {
$printer->printHtml();
} elseif ($type == 'TEXT') {
$printer->printText();
} else {
$printer->printPlain();
}
}
class Printer
{
public function printHtml()
{
}
public function printText()
{
}
public function printPlain()
{
}
}
印刷したいものに応じて処理を変えるメソッドがあったとします。
さてこのメソッドですが、もし印刷したい種類が増えた場合どうしましょう?
まぁ既存のコードに沿ってelseifを増やすでしょうか?
あるいは少しでも見やすくするためにswitch文に変更するでしょうか?
いずれにしても一方を修正したときにはもう一方も修正しなければならなくなりますね。
これはいかんです。
修正に対して閉じてあげるにはなのですが、
インターフェースを使用すれば解決します。
public function print(Printer $printer)
{
$printer->print();
}
Interface PrinterInterface
{
public function print();
}
class HtmlPrinter implements PrinterInterface
{
public function print()
{
}
}
class TextPrinter implements PrinterInterface
{
public function print()
{
}
}
class PlainPrinter implements PrinterInterface
{
public function print()
{
}
}
これで拡張をする時は『PrinterInterface』 を実装したクラスを作ればいいだけですし、
修正するときも実際に使用するprint()メソッドはいじらずに、各クラスを修正すればいいだけとなります。
これが、拡張には開かれていて修正には閉じている、というやつです。
リスコフの置換原則(Liskov substitution principle)
派生型は基本型と置換可能でなければならない
基本型とか派生型とかいわれても分かりづらいかと思いますが、
『継承したクラスは、継承元クラスと同じ動作をしなければならない』ということです。
public function 働かせる(UserA $user)
{
$働いた時間 = $user->働く();
$合計 = $働いた時間 + 休憩した時間;
return $合計;
}
class UserA
{
public function 働く()
{
$time = 8; //実際には代入は必要なし
return $time;
}
}
class UserB Abstruct UserA
{
public function 働く()
{
return '働きました!';
}
}
単純な例ですが、UserBを渡したときにUserAと同じ動きをするでしょうか?
これは意図した結果とは違いますよね。
継承したクラスは、元のクラスと同じ対応できるようにしてね、っていうことです。
上記の場合はインターフェースを用意して、戻り値の型指定をするというのも1つの解決策だと思います。
インターフェース分離の原則(Interface segregation principle)
クライアントが利用しないメソッドへの依存を強制してはならない
Interface AnimalInterface
{
public function 寝る();
public function 起きる();
public function 食べる();
public function 飛ぶ();
}
class Human implements AnimalInterface
{
public function 寝る() {}
public function 起きる() {}
public function 食べる() {}
public function 飛ぶ() {} // ←!?
}
使わないのに実装するのは意味がないし、無駄。
きちんとインターフェースをグループごとに分けて適切に使いましょう。
依存性逆転の原則(Dependency inversion principle)
上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである
依存 #とは
クラスのインスタンスを生成する時って
$obj = new Obj();
ってやりますよね。
class HogeClass
{
public function fuga()
{
$obj = new Obj();
$obj->piyo();
}
}
この時、上位のHogeClassは 下位Objクラスがないとfuga()の処理を行うことができません。
つまり上位HogeClassは下位Objに依存しているということになります。
(わたしはあなたがいないと何もできないのよ〜〜〜)
これが違反していると何が一番困るのかって、
Objクラスができていないとfuga()の処理を記述することができないということ。
それと、Objクラスに変更があった場合に、HogeClassにも変更が及ぶことなどなど。
つまり先述のオープン・クローズドの原則にも違反するわけです。
これの解消法は上記の通り、「抽象」に依存させればいいわけです。
class HogeClass
{
public function fuga(ObjInterface $obj)
{
$obj->piyo();
}
}
これでHogeClass → Obj への一方的な依存から、
HogeClass → ObjInterface ← Obj の依存と関係が変わりました。
まとめ
書いててまだまだ勉強しなきゃいけないなとおもいましたまる
初学者は少なくともインターフェースを覚えることで、幅が広がるかなと思います。
ただ、いずれにせよ乱用はだめです。
適切なものを適切に使用しましょう。
お疲れ様でした。