オブジェクト指向プログラミング入門 for PHP programmers (1)〜前提と概念編


この記事について

PHP でプログラミングをしているけど、オブジェクト指向よく分からん、というひとが対象です。

が、オブジェクト指向の猛者の方々にも読んでいただき、間違いや改善提案があれば指摘していただきたいです。

オブジェクト指向とは何であるか、という部分は長くなるし、他にも解説している記事があるので、参考文献として載せるにとどめます。

この記事では、実際に業務でどう使うか、という観点に絞ってコードを交えながらオブジェクト指向プログラミングの仕方を解説していきます。


前提


はじめにお読みください

オブジェクト指向と20年戦ってわかったこと - Qiita

2016年の記事ですが、オブジェクト指向の現状を端的に分析しており、オブジェクト指向プログラミングを学びはじめたひとにも、これから学ぶひとにも、ぜひ読んでいただきたいです。

読んでも意味が分からなければ、オブジェクト指向に強い先輩や知人に尋ねてみてください。だれもそんな知り合いいない、ということであれば私が答えますので、Twitter で聞いてください。

まぁ、最悪、20年経ってもいまだにオブジェクト指向とは何か、という議論がされていて、どうすればいいコードが書けるか、多くのひとが試行錯誤しているのだ、ということだけ知っておいていただければと思います(道は長く険しいものだ、ということですが、プログラミング自体がそういうものだと思うんですよね)。


参考文献


  • 「オブジェクト指向入門」バートランド・メイヤー著

  • 「オブジェクト指向でなぜつくるのか」平澤 章著

  • PHP: The Right Way


以上を踏まえて、前提

オブジェクト指向プログラミングを語る際には色々な前提条件を事前に表明しておかないと、文脈や著者の知識やバックグラウンドによって、同じ言葉を使っても微妙にそれの指す内容や意味が変わってきたりするのがやっかいです。

オブジェクト指向プログラミングを学んで日の浅い方は「だれが」「どういう文脈で」そう言っているのかというのを踏まえて、セカンドオピニオン的なものを求めるようにするといいんじゃないかと思います(世の中的にはオブジェクト指向そのものに対する批判もあったりするので、ひとりの有識者の意見や考え方に盲目的に従うのではなく、様々な論点を知っておくといいと思います)。

ちなみに、私は C++、Java でオブジェクト指向プログラミングの基礎を学びましたが、C (手続き型)を書いていた時期もありますし、非オブジェクト指向という点では、趣味で Clojure (関数型)を書いていたりもする、パラダイムに特にこだわりはなく、目的や環境に応じて言語や書き方を選べばいいんじゃない?と思っている派です。

前置きが長くなりましたが、これから PHP によるオブジェクト指向プログラミングについて書くにあたって、前提となる私の考え方や価値観に関して挙げておきたいのは以下の点です:


  • オブジェクト指向で書く第一の目的は、保守性を高めるためである


    • ソフトウェアの品質を測る指標はいくつかありますが、個人的に感じるのは、オブジェクト指向でつくったものの保守性の高さが手続き型に比べるとインパクトが大きい、ということです(手続き型でも保守性の高いソフトウェアをつくることは可能ですが、難易度が高くなると思います)。実際には、品質に関わる様々な側面に同時に気を配らなければならないわけですが、このシリーズではまず保守性という点にフォーカスしていきたいと思います。

    • 素早く使い捨てのプログラムを書きたい局面では、オブジェクト指向は重すぎると思います。PHP は手続き型でも書けます(クラスを書く必要がない)ので、保守が不要あるいはさして重要ではないプログラムなら、オブジェクト指向にこだわらずざっと書いてざっと動かしましょう。



  • オブジェクト指向で書いたからといって、保守性が高くなるわけではない


    • オブジェクト指向は「分析」「設計」がキモだと思っていて(本記事では両方をひっくるめて「設計」と記載します)、クラスに分割したからといって、その分割の仕方を間違えると途端に保守コストが上がるケースがあったりするのがやっかいです。



  • クラスを使っているからといってオブジェクト指向になるわけではない


    • もっというと、クラス化したからといって、それオブジェクト指向と呼んでいいの?というようなコードになる可能性もあります。もちろん広い意味ではクラスを使えばオブジェクト指向なんですが、本記事では、オブジェクト指向的であるか、オブジェクト指向をまとった手続き型であるか、というのはできるだけ区別して言及していきたいと思っています。



  • クラスやオブジェクトは現実のモノやコトを表現したものではない


    • たまにオブジェクト指向は現実を表現したものという説明を見かけますが、これは間違いだと思っています。現実をモデル化すると却って弊害があるとすら思います。私たちがつくるのはソフトウェアなので、モデル化する対象は、ソフトウェアが実現したいもの、という捉え方をしてほしいです。




コード例における文脈とフレームワーク

いくつかコード例を載せますが、特に断りがない場合を除いて、ウェブアプリケーションを想定しています。ウェブアプリケーションフレームワークの使用を前提としているような場合は、Laravel を使います(とはいえ、MVC の基本構造や PSR-7 のような入出力の仕様からは遠く離れはしないと思うので、別のフレームワークをお使いの場合は、適宜置き換えていただければと思います)。


おことわり

本記事のサンプルコードは、行数を短くする目的で、コードフォーマットが PSR-2 に違反している箇所がありますが、実際にコードを書く際は、PSR-2 に準拠することを強くおすすめします。

PSR-2: Coding Style Guide - PHP-FIG

PSR-2 コーディングガイド(日本語)|北海道札幌市のシステム開発会社インフィニットループ

また、本記事では PHP7 の使用を前提としており、 declare(strict_types=1) を先頭に書くことも推奨しますが、同じく行数を短縮するために省略しています(同じ理由で <?php も省略しています)。


1. Hello, world!

最初は簡単なプログラムを書いてみます。お題は「指定された文字列を標準出力に出力する」です。文字列を保持する「Message」というクラスをつくります。PHP でオブジェクト指向プログラミングするにあたっては、たとえ実際にはスカラー型や配列を利用することになったとしても、常に何らかの「クラス」をイメージしてください。「純粋な」オブジェクト指向ではない PHP ですが、「純粋に」考えることがトレーニングになります。

オブジェクト指向と比較するために、先に手続き型で書いたものを載せます。

手続き型

$message = 'Hello, world!';

echo $message . PHP_EOL;

オブジェクト指向

class Message {

private $body;

public function __construct(string $message) {
$this->body = $message;
}
public function __toString() {
return $this->body;
}
}

$message = new Message('Hello, world!');
echo $message . PHP_EOL;

上のコード例では、「指定された文字列を標準出力に出力する」という目的を2つの手法で達成してみましたが、同じ目的を達成するのに、オブジェクト指向で書いたほうが 5 倍近い行数になっています。これは大体のケースにおいてこうなると思っていいと思いますが、我々の目的は保守性の向上なので、目先の生産性のことは忘れてください。

完全にネタですが、こういうのもあります。Java で書かれていますがここに書かれていることのバカバカしさというのは分かると思うので、お時間があれば見てみてください。

Hello World Enterprise Edition


クラス名

さて、上のコード例を解説していきます。

まずはクラス名ですが、クラス名をどうするか、というのは非常に重要です。個人的には、変数名や関数名よりも重要と考えています(いや、関数名は同じくらい重要かもしれませんが)。プログラミング言語といえど、自然言語(ほとんどの場合英語)を書くように書く必要がある場所がたくさんあります。命名がその最たるもので、クラス名に選択された単語が、そのクラスが表現しようとしている概念を端的に表していなければなりません。上の例では Message という単語を選択しました。私はほとんどのケースで、命名に使おうとする単語は辞書で調べます。和英、英和だけでなく、必ず英英辞書も確認します。

メッセージという単語は外来語として日本語でも使われていますが、みなさんはその概念をどのように理解しているでしょうか。

まずは英和辞書から見てみましょう。


(口頭・文書・信号などによる)通信、メッセージ、伝言、ことづけ、電報、書信、(公式の)メッセージ、(大統領の)教書、(文学作品・音楽・演劇などの)主旨、意図

messageの意味・使い方・読み方 | Weblio英和辞書


続いて英英辞書です。


a piece of written or spoken information that you send to someone, especially when you cannot speak to them directly

message (noun) definition and synonyms | Macmillan Dictionary


指定した文字列を出力するクラス、という点では、なんとなく外れてはいないような気がしますが、端的に表しているかどうかは自信を持って首肯できない感じもします。

理由は分かっています。

文脈の情報が一切ないからです。このクラスのすること(できること)ははっきりしていますが、何のために使われるのか、どういうクラスがクライアントとなって使われるのか、というのが分からないから、この名前が適切なのかがはっきりしないのです。

これが例えば、「チャットアプリケーションにおいて、ユーザ同士が送り合うメッセージ」であればどうでしょう?目的がはっきりしているので、メッセージに必要な情報が明確になっていくような気がします(のちほどこのクラスを拡張していきます)。


可視性について

可視性(public, protected, private)は以下の基準で決めます。


  • private: 定義されたクラスからしか呼ぶことはできない

  • protected: 定義されたクラス、および継承先のクラスから呼ぶことができる

  • public: 定義されたクラス、継承先のクラス、および外部のクラス(注)から呼ぶことができる

注:PHP は Java と違ってクラスを定義する必要はないので、最上位の呼び出し側はクラスではないですが、ややこしいのでこれに含めています。

アクセス違反の例

<?php

class Message {
private $body;
}
// 最上位の呼び出し側
$message = new Message();
echo $message->body . PHP_EOL; // private なのでアクセスできずエラーになる


プロパティ

基本的にプロパティは private で定義します。継承を使う場合は protected になるケースがありますが、いまは考えなくて大丈夫です。フレームワークを使っていると(たとえば Laravel の Eloquent)、 public になることもありますが、できるだけ読み取り専用として扱うようにしてください。PHP にはプロパティを読み取り専用する機能がないので、このようなケースではプログラマの分別が求められます。

上記の例では、メッセージの文字列を保持する $body という変数のみ定義しました。このデータはコンストラクタで渡された際に初期化され、それ以降は変化しません。プロパティが外部から変更されうるか、内部で変更されうるか、または初期化時以降変更されないか、というのは非常に重要です。この3つのパターンを念頭に置いて、プロパティの定義と扱いを決めるようにしてください。

目的が「チャットアプリケーションにおいて、ユーザ同士が送り合うメッセージ」であったとしたら、プロパティはどうなるでしょう。アプリケーションの要件にもよりますが、チャットアプリケーションにおける「メッセージ」には、送信元ユーザ、送信先ユーザ(あるいはグループかもしれません)、送信日時、といった情報が必要になりそうです。


メソッド

オブジェクトを文字列として扱う __toString() というメソッドのみが定義されています。これは PHP の組み込みのメソッドで、文字列を扱う関数の引数に渡すと自動的に呼ばれます( __ で始まる組み込みのメソッドは他にもありますが、すべて明示的に呼ばれることはありません)。

いまはこのクラスの用途が分からず「標準出力に出力する」ことができればいいだけなので、メソッドはこれだけです。のちほど「チャットアプリケーションにおいて、ユーザ同士が送り合うメッセージ」にアレンジする際に他のメソッドを追加します。


2. Message

では、上記のコード例に「送信元ユーザ」、「送信先ユーザ」、「送信日時」の3つの情報を加えて、Message クラスをアレンジしてみます。今回も比較対象として手続き型のコードも載せておきます。

手続き型

function send_message(string $message, string $sender, string $receiver, int $sent_at) {

// ここに実際の送信処理を書く
}
$message = 'Hello, world!';
$sender = 'nunulk';
$receiver = 'john';
$sent_at = time();
send_message($message, $sender, $receiver, $sent_at);

オブジェクト指向

class Message {

private $body;
private $sender;
private $receiver;
private $sentAt;

public function __construct(string $message, User $sender, User $receiver, \DateTimeInterface $sentAt) {
$this->body = $message;
$this->sender = $sender;
$this->receiver = $receiver;
$this->sentAt = $sentAt;
}
public function __toString() {
return $this->body;
}
public function send() {
// ここに実際の送信処理を書く
}
}
// sender と receiver は User のインスタンスであり、これより前に初期化されている必要がある
$message = new Message('Hello, world!', $sender, $receiver, new \DateTime());
$message->send();

ひとまず、メッセージの送信に必要な情報は、手続き型の例では関数に、オブジェクト指向の例ではコンストラクタに、すべてを渡す方式にしました。しかし、おそらく別のより適切な組み立て方がありそうです。コンストラクタやメソッドにどうやってデータを渡すか、という点については後述する予定です。


3. 演習: クラスをつくる

メッセージの例に倣って、以下の概念をオブジェクト指向な PHP で表現してください。必要と思われるプロパティの宣言、コンストラクタ、それと、どれかひとつ実際のアプリケーションで使われるであろうメソッドをひとつ定義してください(実装は不要です、名前と引数と戻り値があれば)。


  • TODO アプリケーションにおける「タスク」

  • 転職支援サービスにおける転職希望者の「職歴」

  • 宿泊施設予約サイトにおいてユーザーが行う「予約」

アプリケーション、サービス、サイト、という語に意味はありませんので(これらの語が、それぞれのドメインとのカップリングがしっくりくるものを選びました)、それぞれの文脈(言外にある文脈は想像で補う)と括弧でくくられた部分にだけ、フォーカスしてください。

正解のない演習ではありますが、このシリーズの最後に、回答例的なものは載せようと思います。


おわりに

このシリーズの前提となる私の考えや価値観と、PHP におけるクラスの基本的な概念を説明しました。

間違いの指摘や、もっとこうしたらいい、みたいな提案をコメント欄にていただけると、よりよい記事になると思いますので、何かあればお願いします :bow:

次回は、モデリングの基礎と題して、オブジェクトのライフサイクル、プロパティとメソッドの関連について書く予定です。

余談ですが、経験の浅い同僚に、PHP を使ったオブジェクト指向設計・プログラミングについての資料を探して紹介しようと思ったんですが、PHP だとほとんどないんですよね…。もし、書籍やウェブ上のリソースで、おすすめのがあったら、教えていただけると助かります。