『オブジェクト設計スタイルガイド』のススメ
2023/12/12
2024/02/25
この記事はPHPアドベントカレンダー13日目の記事です。
普段はPHPのレガシープロジェクトの機能追加や改善をしたり、PHPのソースコードからクラス図を自動生成するためのツール(php-class-diagram)を作ったりしている、しめじ(smeghead)です。
オブジェクト設計スタイルガイド
オブジェクト設計スタイルガイド(amazon)
変更後
今年読んだ本『オブジェクト設計スタイルガイド』が、実用的で有効な指針が網羅的に紹介されていてとても良い本でした。自分の最近の仕事でも、紹介されている指針をおおいに参考にさせてもらってます。『オブジェクト設計スタイルガイド』の中でも簡単に実践できる指針を紹介してみたいと思います。
サンプルコードについて
この本に出てくるサンプルコードは、PHPのようですがPHPではありません。以下のような幾つかの点でPHPではない架空の疑似コードで書かれています。
- 変数の先頭に付ける
$
が無い -
メソッド呼び出しの
->
が.
になっている
これは、PHPコードを見難いと感じる人に配慮した結果なのかもしれません。しかし普通に
json_encode
のようなPHP特有の関数が出てきたりするので、完全にPHPを前提にして構成されていると思います。(個人的には動作するPHPで書いてくれた方が嬉しかったです)指針の例
オブジェクトの設計についての説明の最初に、オブジェクトの種類は、サービスオブジェクトとそれ以外のオブジェクトの2種類に分類できるとされています。各章毎に指針が分類されて紹介されています。いくつか簡単な指針を紹介してみます。
9.8 デフォルトでメソッドやプロパティをprivateとする
オブジェクトの種類に依らず、オブジェクトのメソッドのプロパティのアクセス修飾子は、なるべく公開するものを減らすため、デフォルトを
private
にすることが推奨されています。オブジェクトを使う側から見ても、公開するプロパティは少ない方が使い方が明確になり、意図しない使われ方をするリスクを減らすことができるので、単純ですが効果的な指針だと思います。9.7 デフォルトでクラスをfinalとする
可能であれば全てのクラスの定義を
final
にすることが推奨されています。実際にこのガイドを実践してみると、ここ最近は継承があまり使われなくなっていることも影響してると思いますが、新規クラスを作る時にfinal
を付けることができない場面は、想像以上に少ないです。クラス宣言に
final
を付けるということは「このクラスを継承して機能拡張をしてくれるな」という表明なので、デフォルトでクラスをfinal
にすることは理に適っていると思います。<?php declare(strict_types=1); final class Product
{
...
}
9.1 振る舞いを設定可能にするためのコンスタラクタ引数を導入する
ファイルにログを出力するサービスオブジェクトのクラスに、ログファイルのパスが直書きされていると、いろいろ不都合があります。実行するOS環境によって違うパスに出力したかったり、テスト時に別のパスに出力して検証したいかもしれません。メソッドの振る舞いを設定可能にするために、コンストラクタで出力ファイルのパスを指定する形にする方法が推奨されています。
この例は、オブジェクトの設定(
string
の値)をコンストラクタで注入するという簡単なものですが、より複雑な要求に応えるための類似する指針も多くあり参考になりました。<?php declare(strict_types=1); final class FileLogger
{
public function log($message): void
{
file_put_contents(
'/var/log/app.log',
$message,
FILE_APPEND
);
}
}
<?php declare(strict_types=1); final class FileLogger
{
public function __construct(string $filePath)
{
$this->filePath = $filePath;
}
public function log($message): void
{
file_put_contents(
$this->filePath,
$message,
FILE_APPEND
);
}
}
$logger = new FileLogger('/var/log/app.log');
2.10 コンストラクタの中ではプロパティへの代入以外は何もしない
この指針は、サービスオブジェクトに対する指針です。
サービスオブジェクトのコンストラクタは依存関係を注入することが目的であり、それ以外の副作用を伴なう処理は行うべきではないと理解しました。
コンストラクタの引数が、オブジェクトが動作するために適切な値であるかどうかのチェックは行なうが、コンストラクタの引数をプロパティに代入するに留めるのが良いということです。例として出てきていた避けるべきものは、「
FileLogger
サービスのコンストラクタで、出力対象のディレクトリが存在しなければディレクトリを作成するという処理が副作用を伴なう処理である。」というものでした。どうしても必要な副作用を伴なう前処理が必要なら、外部の
LoggerFactory
でそれらの処理を行なうようにすることで、FileLogger
サービスのコンストラクタは、プロパティへの代入だけを行なう形にすることもできます。