【オブジェクト指向ってわかりにくくない?】
オブジェクト指向の3大要素:継承・カプセル化・多態性って何度勉強してもふわっとしか理解できなかったのは私だけでしょうか?
勉強中はわかったような気にはなるのですが、実際にコードを書く段階でどう使えばいいの?という状態になりました。
継承の説明で「勇者というclassから上位互換のスーパー勇者classを作る」と聞いて、実務で勇者はでてこないしなぁ、良さや使う場所のイメージが難しいなぁと思っていました。
しかし、実務を積んでいくと、良いコードを作る上で大事な考え方であることがわかってきました。
実務に近い内容で説明される方がわかりやすいと思うことが多いので、この記事では具体的なWebアプリの実装を元に継承・カプセル化・多態性を解説していきたいと思います。
【この記事で説明すること】
この記事では、書籍管理アプリを作ることを例にオブジェクト指向の継承・カプセル化・多態性を解説していきます。
書籍管理アプリは書籍の貸出・返却ができ、貸出時にMailを送信する機能に限定します。
オブジェクト指向の解説を目的とするので、書籍管理アプリとしての詳細なコードは書いていません。
バックエンド寄りの内容になります。
自作、独学問わず、Webアプリの開発経験のある方向けの内容になります。
コード例はPHPで記載していますが、ややこしいコードはないので、他言語を使われている方でも理解できる内容と思います。
【アプリの概要】
今回解説に使用する書籍管理アプリの要件は以下です。
・本の貸出ができる
・本の返却ができる
・貸出が完了するとメールを送る
これだけです。できるだけシンプルにしました。
処理の流れをざっくり説明すると、以下のようになります。
- ユーザーからのリクエストを受けてBookControllerクラスが呼び出される。
- BookControllerクラスはリクエストに応じたUsecaseクラスを呼び出す。
- UsecaseクラスはmodelであるBookをリクエストに合わせて操作する。
※3の後に、メールを送ったり、DBに保存したりという処理はありますが、説明を簡単にするために省略しています。
ここでクラス図の矢印の意味を解説しておきます。
(この矢印が重要です)
▼実線の矢印
BookControllerからBorrowUsecaseをつなぐ実線の矢印は依存を示しています。
BookControllerがBorrowUsecaseを呼び出して使っています。
メンバ変数として持ったり、メソッド内でインスタンス化して使ったりする場合はこの矢印になります。
▼実線の白抜き三角(△)の矢印
BookControllerからBaseControllerをつなぐ実線の白抜き三角(△)の矢印は汎化を示しています。
BaseControllerを親としてBookControllerを作成しています。
コードでは以下のようにextends
することになります。
class BookController extends BaseController
▼点線の白抜き三角(△)の矢印
MailSenderからISenderをつなぐ点線の白抜き三角(△)の矢印は実現を示しています。
ISenderに定義されている関数をMailSenderが具体化して実装しています。
コードでは以下のようにimplements
することになります。
class MailSender implements ISender
■継承
継承とは,あらかじめ定義されたクラスを引き継いで新しいクラスを定義することである。
引用元:http://teacher.nagano-nct.ac.jp/fujita/LightNEasy.php?page=oopElements
継承はプログラムの再利用を目的に使用することが多いです。
今回は下図の赤枠部分で使用します。
MVCやクリーンアーキテクチャで登場するControllerクラスは、大規模なシステムではたくさん作られます。
たくさん作られるControllerの中で共通化できるものをBaseControllerに記述しておき、継承先から使うようにします。
例えば、ControllerのレスポンスはJSON形式など何か決まった形にデータを整形すると思いますが、そのレスポンスの整形処理をBaseContollerに持たせるのが良いと思います。
以下にコード例を記載します。
class BaseController{
private function sendResponse(array $responseData){
// レスポンス送信処理
・・・
}
private function sendError(int $statusCode, string $errorMessage){
// エラーレスポンス送信処理
・・・
}
}
class BookController extends BaseController{ // BaseControllerを継承
public function bollow(array $requestData){
// 何かしらの処理
・・・
try {
$result = $borrowUsecase->handle($inputData);
} catch(Exception e) {
return $this->sendError(500, $e->getMessage()); //継承しているので親のsendErrorメソッドが使える
}
// 貸出の実行結果をsendResponseで送る
return $this->sendResponse($result); //継承しているので親のsendResponseメソッドが使える
}
}
こうすることで、保守性や統一性が向上します。
BaseContollerがない場合、複数のControllerにデータの整形処理を記述することになり、もし要件が変更になった際にすべてのControllerを確認し修正する必要があります。
また、開発メンバーが多い場合、各々が好きなようにデータを整形してしまい、レスポンスの形式がContollerによって異なるということにもなるかもしれません。
Controllerに限らず、同じ役割のクラスを複数作成する場合は継承が使えないか考えてみてください。
■カプセル化
カプセル化とは,オブジェクトのメンバーを保護することで,オブジェクトの安全性を高めることができる.
引用元:http://teacher.nagano-nct.ac.jp/fujita/LightNEasy.php?page=oopElements
カプセル化はデータの整合性を保つためにかなり大事な要素です。
今回は下図の赤枠部分で解説します。
カプセル化の基本は、クラスのフィールド(メンバ変数)はprivate、外部からはメソッドを使ってメンバ変数を操作することです。
上記を守れば、かなり安全なコードになります。
書籍管理アプリのデータの整合性の要はBookクラスです。
class Book
{
// フィールド(メンバ変数)はprivateにする
private int $id;
private string $title;
private boolean $isAvailable;
private DateTime $dueDate;
public function __construct(int $id, string $title, boolean $isAvailable, DateTime $dueDate)
{
// インスタンスを作成する際に必ず正しいデータが入っているかを確認することになるので安全性が向上する
if($id <= 0){
throw new Exception('idは1以上で入力してください。');
}
$this->id = $id;
if(mb_strlen( $title, "UTF-8" ) > 255){
throw new Exception('titleは255文字以下で入力してください。');
}
$this->title = $title;
$this->isAvailable = $isAvailable;
$this->dueDate = $dueDate;
}
// 外部から使うメソッドはpublicにする。
// 内部メソッドでメンバ変数の値を操作する。
public function borrow(DateTime $dueDate)
{
if(!$this->isAvailable) {
throw new Exception('貸出中です');
}
if($dueDate < new DateTime()) {
throw new Exception('返却期限は本日以降の日付をしていしてください');
}
$this->isAvailable = false;
$this->dudate = $dudate;
}
public function return() // returnは予約後になっている可能生が高いですが読みやすさ重視で命名しています。
{
if($this->isAvailable) {
throw new Exception('返却済みです');
}
$this->isAvailable = true;
$this->dudate = null;
}
}
本を借りるには、まず正しい本のデータでインスタンス化しなくてはなりません。
constructでインスタンス化する値の検証をするので、バグにつながるような値が混在しにくくなります。
インスタンス化してもメンバ変数がprivateなため、外から無理な変更は加えられません。
決められたメソッドでしかデータの操作ができないため、データの整合性を保ちやすくなります。
このBookクラスの使用方法は以下のようになります。
class BorrowUsecase
{
public function handle(array $inputData)
{
// DBから本のデータを取得
・・・
// 取得したデータからbookインスタンスを作成
$book = new Book($id, $title, $isAvailable, $dueDate);
// 貸出処理
$book->borrow($inputData['dueDate']);
// DBへの保存処理
・・・
}
}
インスタンス化してメソッドを呼び出して、データを操作という流れです。
開発をやっているといたるところでデータの操作をすると思います。
カプセル化ができていないと、色んな場所でデータの整合性を保つためのコードを書くことになり、データの知識がどんどん他のクラスに漏れ出ていきます。
もし、要件が変更になった場合、漏れ出た知識をすべて検証し変更する必要があります。
変更漏れのリスクがが高くなります。
最初は直接データを変更したくなると思いますが、ぐっと我慢して、クラスのメンバ変数はprivate、外からはメソッドを使ってメンバ変数を操作することを意識して、実装してみてください。
■多態性(ポリモーフィズム)
ポリモーフィズムは,オーバーライドやオーバーロードにより,一つのメソッド状況に応じた振る舞いをすることである.
引用元:http://teacher.nagano-nct.ac.jp/fujita/LightNEasy.php?page=oopElements
勉強中、この多態性が一番、よくわからん状態になりました。
いろいろな用途がありますが、初めは外部サービスを繋ぐ部分に使用するイメージを持っていただくと良いと思います。
今回は下図の赤枠部分で解説します。
ここでは、書籍を借りる処理の後に完了メールを送信することを考えます。
多態性を考えずに実装すると以下のようになりました。
class MailSender
{
public function send(string $message)
{
// Mailの送信処理
・・・
}
}
class BorrowUsecase
{
private MailSender $mailSender;
public function __construct(MailSender $mailSender)
{
$this->mailSender = $mailSender
}
public function handle(array $inputData)
{
// 貸出処理
・・・
// mailで完了通知
$mailSender->send($message);
}
}
class BookController
{
private MailSender $mailSender;
public function bollow(array $requestData){
// 何かしらの処理
・・・
$bollowUsecase = new BorrowUsecase($mailSender);
$bollowUsecase.handle($inputData);
}
}
UsecaseでMailSenderを持ち、そのsendメソッドを呼び出してメールを送信しています。
次に上記のコードを多態性を考慮した形に修正してみます。
// interfaceを用意する
interface ISender
{
public function send(string $message);
}
// ISenderの実装にする
- class MailSender
+ class MailSender implements ISender
{
public function send(string $message)
{
// Mailの送信処理
・・・
}
}
class BorrowUsecase
{
// UseCaseではInterfaceを持っておく
- private MailSender $mailSender;
- public function __construct(MailSender $mailSender)
+ private ISender $sender;
+ public function __construct(ISender $sender)
{
- $this->mailSender = $mailSender
+ $this->sender = $sender
}
public function handle(array $inputData)
{
// 貸出処理
・・・
// 何かしらの送信で完了通知(このUsecaseはどこに送信するかは知らない)
- $mailSender->send($message);
+ $sender->send($message);
}
}
class BookController
{
private MailSender $mailSender;
public function bollow(array $requestData){
// 何かしらの処理
・・・
// Controller側でISenderの実装であるMailSenderを渡してあげる。
$bollowUsecase = new BorrowUsecase($mailSender);
$bollowUsecase.handle($inputData);
}
}
ISenderの実装としてMailSenderを作成します。
MailSenderにメール送信の具体的な処理を記述します。
Usecaseでは何かしらで送信することはわかりますが、何を使って送るか詳細は知りません。
ControllerからMailSenderを渡されて初めてわかります。
この書き換えだけを見ても良さがピンと来ないと思いますので、要件が変更された場合を考えます。
メールで送っていたものをSlackで送るように要件が変更された場合は、以下のようなSlackSenderを作成します。
class SlackSender implements ISender
{
public function send(string $message)
{
// Slackへの送信処理
}
}
ISenderの実装としてSlackSenderに具体的な処理を記載します。
既存コードの修正は以下のようにBookControllerのみになり、Usecaseの修正は不要になります。
class BookController
{
- private MailSender $mailSender;
+ private SlackSender $slackSender;
public function bollow(array $requestData){
// 何かしらの処理
・・・
- $bollowUsecase = new BorrowUsecase($mailSender);
+ $bollowUsecase = new BorrowUsecase($slackSender);
$bollowUsecase.handle($inputData);
}
}
多態性を使うことで、外部サービスの要件変更があっても、変更箇所を限定することができます。
また、外部サービスの差し替えが容易になることで、ユニットテストも実装しやすくなるというメリットもあります。
たとえば、多態性を考慮せずにBorrowUsecaseのhandleをテストする場合は、MailSenderを使用しているため、テストでもメール送信が行われてしまいます。
しかし、多態性を考慮しているとユニットテスト時にはMockSenderを使い、送信処理を別の形に変えるということが簡単にできます。
class MockSender implements ISender
{
public function send(string $message)
{
// ローカルファイルに書き出すなどテストしやすい処理
}
}
// Unitテスト時にHogeUsecaseのインスタンス作成時にMockSenderを渡す
$bollowUsecase = new BorrowUsecase($mockSender);
多態性を考慮することで、外部要因による変更が内部に影響することとをせき止めする役割や、差し替えによりテストが書きやすくなったりといろいろなメリットがありますので、外部サービスを使用する部分は多態性を考慮した実装をしてみてください。
まとめ
書籍管理アプリを例にオブジェクト指向の3大要素:継承・カプセル化・多態性を解説してきました。
今回説明した使い方を簡潔にまとめると以下のようになります。
継承:同じような役割のクラスを複数作成する場合に、共通化できるメソッドを切り出して親クラスにまとめる
カプセル化:クラスのフィールド(メンバ変数)はprivate、外からはメソッドを使ってメンバ変数を操作する
多態性:外部サービスを使用する部分はInterface化して、上位から具体的な処理を渡す
上記をオブジェクト指向の第一歩として、取り入れていただければと思います。
上記以外にも、使い方を知りたくなった方は、デザインパターンの勉強をしていただくと実装の幅が増えると思います。
今回のコードにはデータの永続化処理(RDBやファイルなどへのデータの保存)を記載していません。
多態性で説明した実装例を応用して永続化処理を実装すると、変更が用意で、テストのし易いコードを作成できますので、チャレンジしてみてください。
クリーンアーキテクチャやドメイン駆動設計では多態性を使って永続化処理を実装する例がでてきいますので、そちらも参考にすると理解が深まると思います。
本記事がオブジェクト指向を概念的にしか理解できず、実務に応用できていなかった方の助けになれば幸いです。