オブジェクト指向プログラミングにおいて重要な概念である、abstract、interface、traitについて、それぞれの使い分けと具体的な実装方法について解説します。
※言語はPHPですが、他のものでも応用できるかと思います。
abstract(継承全般)
いわゆる継承ですね。
使うべきケース
- 親と子に関してプログラムの骨組みそのものが近いこと
- abstractを使う場合については子で分岐させるような処理がある場合(interfaceを使うより、子供で処理を分岐させた方が良い)
実装例
<?php
// 抽象クラス:共通の処理と構造を定義
abstract class Animal
{
protected $name;
public function __construct($name)
{
$this->name = $name;
}
// 共通メソッド(実装を持つ)
public function getName()
{
return $this->name;
}
// 抽象メソッド(子クラスで必ず実装する)
abstract public function makeSound();
// 共通メソッド(実装を持つ)
public function introduce()
{
return $this->name . "は" . $this->makeSound() . "と鳴きます";
}
}
// 具象クラス:抽象メソッドを実装
class Dog extends Animal
{
public function makeSound()
{
return "ワンワン";
}
}
class Cat extends Animal
{
public function makeSound()
{
return "ニャー";
}
}
// 使用例
$dog = new Dog("ポチ");
echo $dog->introduce(); // ポチはワンワンと鳴きます
$cat = new Cat("タマ");
echo $cat->introduce(); // タマはニャーと鳴きます
abstractの特徴:
- 共通の処理(メソッドの実装)を持てる
- プロパティを持てる
- コンストラクタを持てる
- 単一継承のみ(1つの親クラスのみ)
interface
最初の学習時にabstractとの使い分けが一時不明瞭でしたが、以下のように整理できます。
使うべきケース
- 親子関係などがあるかどうか(プログラム全体の主従関係)は関係ない
- あくまでメソッド名と引数と戻り値の型を統一させたい場合に使う
単純なモデルなんかで従属関係はないんだけど、汎用的なメソッドを定義して、引数、戻り値を統一させたい場合に使うなんてケースがあります。
実装例
<?php
// インターフェース:メソッドのシグネチャのみを定義
interface Serializable
{
public function serialize();
public function unserialize($data);
}
interface Loggable
{
public function log($message);
}
// 複数のインターフェースを実装可能
class User implements Serializable, Loggable
{
private $name;
private $email;
public function __construct($name, $email)
{
$this->name = $name;
$this->email = $email;
}
public function serialize()
{
return json_encode([
'name' => $this->name,
'email' => $this->email
]);
}
public function unserialize($data)
{
$array = json_decode($data, true);
$this->name = $array['name'];
$this->email = $array['email'];
}
public function log($message)
{
echo "[User: {$this->name}] {$message}\n";
}
}
class Product implements Serializable, Loggable
{
private $name;
private $price;
public function __construct($name, $price)
{
$this->name = $name;
$this->price = $price;
}
public function serialize()
{
return json_encode([
'name' => $this->name,
'price' => $this->price
]);
}
public function unserialize($data)
{
$array = json_decode($data, true);
$this->name = $array['name'];
$this->price = $array['price'];
}
public function log($message)
{
echo "[Product: {$this->name}] {$message}\n";
}
}
// 使用例
$user = new User("田中", "tanaka@example.com");
$user->log("ユーザーが作成されました");
$product = new Product("ノートPC", 100000);
$product->log("商品が登録されました");
interfaceの特徴:
- メソッドの実装を持てない(シグネチャのみ)
- プロパティを持てない
- 複数のインターフェースを実装可能(多重実装)
- 「〜できる」という能力を表現
trait
PHPでは多重継承が禁じられており(親は1つしかもてない)、共通性のあるコードを書こうと思った時に親に書いていない場合限界があります。
そんな時に使えるのがTraitという考え方で、これを使いますと親ではないのに、コードを再利用することができます(PHP5.4から有効)。
Laravel使っている方であればModelでHasFactory(Factoryへの使用)、SoftDeleted(論理削除)などがつかわれているのを目にしたことがあると思います。
使うべきケース
- 継承するほどではないけど、一部汎用的なメソッドを使いたい
- ワンポイント的な形のメソッドなどで使われることが多い
- 比較的汎用的なメソッドを水平展開させたい時
意外にうまく使えるケースが多く、なかなか使いどころいかんによっては効果を発揮してくれます。
Traitの実装例
通常は以下のようなファイル構成になります。
- Traitそのもの
- Traitを使う一般のクラス
- 一般のクラスを使うプログラム
以下に実際のサンプルを書いて見ます。
sampleTrait.php (Traitそのもの)
<?php
trait Calculator
{
private $tax = 0.08;
public function samplefunc1($price)
{
return $price * $this->tax;
}
}
SampleClass.php (Traitを使う一般のクラス)
<?php
require_once 'sampleTrait.php';
class SampleClass
{
use Calculator;
public function __construct()
{
}
public function hoge()
{
return 'hoge';
}
}
sampleProgram.php (一般のクラスを使うプログラム)
<?php
require_once 'sampleClass.php';
$sampClass = new SampleClass();
//クラス内部のメソッド
echo $sampClass->hoge(). "\n";
//hoge
//traitしたクラスのメソッド
echo $sampClass->samplefunc1(1000). "\n";
//80
traitの特徴:
- メソッドの実装を持てる
- プロパティを持てる
- 複数のtraitを使用可能
- 継承関係を持たない水平的なコード再利用
abstract、interface、traitの違い(比較表)
| 項目 | abstract | interface | trait |
|---|---|---|---|
| メソッドの実装 | ○ 持てる | × 持てない | ○ 持てる |
| プロパティ | ○ 持てる | × 持てない | ○ 持てる |
| コンストラクタ | ○ 持てる | × 持てない | × 持てない |
| 継承・実装の数 | 1つのみ | 複数可能 | 複数可能 |
| 継承関係 | 必要(親子関係) | 不要(契約関係) | 不要(水平展開) |
| 用途 | 「is-a」関係(継承) | 「can-do」能力(実装) | コードの再利用 |
| 例 | Dog is an Animal | User can be Serializable | use Calculator |
まとめ
まとめると以下のような感じですかね。
- abstract/継承: 明確な親子関係(is-a関係)がある場合。共通の処理を持ちたい時
- interface: 型やメソッドシグネチャを統一したい場合。複数の能力を持たせたい時
- trait: 汎用的なメソッドを水平展開したい場合。継承関係なしにコードを再利用したい時
親とまでは行かないけど汎用的なメソッドを実装させたい時にtraitは特に便利で、以前のプロジェクトでもちょこちょこ活用することができました。