「継承がいまいち分からない」というリクエストがあり実施した社内研修用の資料を共有します。
対象:いわゆるコーダー、CMSのテンプレートや簡易なカスタマイズはできる
解説範囲:concrete5 CMS で頻出する範囲
なぜクラスという仕組みがあるのか
クラスの説明のために「そもそもオブジェクト指向とは何か」から始める解説がよくありますが、今回はそこは省きます。それは例えばスマホ決済の説明のために「そもそも貨幣とは何か」から説明を始める様なものだからです。
多くの人は、貨幣経済がなぜ生まれどういう意味があるのか、など理解してなくても、便利だから、みんなが使っているから、という理由でお金を使うでしょう。お金とは便利なもので、スマホ決済はさらにめっちゃ便利なもの。そういう理解で実際には事足ります。
クラスもそういうもので、そもそもプログラミング自体がソフトウェアを作れる便利な代物で、クラスはプログラミングのためのめっちゃ便利なツール、そういう感じです。もちろん、オブジェクト指向が生まれた歴史を調べると面白いと思いますが、ある程度使いこなしてからの方が実感も湧くでしょう。
ではクラスはなぜ便利なのか。ひとことで言うと、人間の脳が高性能ではないから、と言うことに尽きます。
仮に、ソフトウェアが何十万行もの一つのPHPファイルに書いてあったとしましょう(決してこの行数は誇張ではありません。例えば concrete5は5000以上のPHPファイルで構成されています が、それぞれに100行のプログラムがあったら50万行です)。
もちろん、それで動かないと言うことはないのですが、「冒頭に書かれたこの変数の内容を変更した時に、どこまで影響があるんだろう」「この関数にバグがあって修正したいんだけど、副作用で別の場所にバグが起こったらどうしよう?」「どうもこの関数でエラーが起きてるっぽいけど、中身はどこに書いてあるんだろう?」
何十万行のプログラムが全て頭に入っていて、それらの疑問に即座に答えを出せる人はまずいませんよね。一度に把握できる程度の適切な量にプログラムを分割できること、それがクラスと言う仕組みの便利なところです。
言い換えれば、良いプログラムとは「人間にやさしいプログラム」とも言えます。なるべく「ここを変えても壊れないかな?」「こう言う時はどうなるんだろう。エラーになったら嫌だな」と言った心配事をなくすことが重要です。
基本的なクラス
と言うことで、クラスの基本機能は「プログラム全体から関連する変数と関数を切り出してひとまとまりにすること」です。そのひとまとまりに名前をつけることができるのがクラスといえます。
下記の例では「BasicClass」と言う名前になっています。そして、クラスの中に入れると変数が「プロパティ」関数が「メソッド」と言う名前に変わりますが、基本的には同じものだと思ってOKです。
<?php
class BasicClass
{
// プロパティの宣言
public $var;
// メソッドの宣言
public function getVar() {
// プロパティには $this->プロパティ名 でアクセスできる
return $this->var;
}
}
このクラスは、1つのPHPファイルにいくつでも書くことができますので、何十万行のPHPファイルに数千個のクラスが書いてある、と言うことも仕様上はできてしまいます。しかし、せっかくプログラムを分割してわかりやすくしたのですから、ついでにファイルも別にした方がいいですよね。と言うことで、コーディング規約上で1ファイルにクラスはひとつ、と定められていることが多いです。
インスタンス
また、「クラスは設計図のようなもの」と言う説明も聞いたことがあるでしょう。
設計図を使って実体を作成し、それぞれ別の情報をもったモノとして扱うことができます。
これをインスタンスとも呼びます。
// インスタンスを作成
$a = new BasicClass();
// プロパティ $var に文字を代入
$a->var = 'Apple';
$b = new BasicClass();
$b->var = 'Banana';
echo $a->getVar(); // Apple
echo $b->getVar(); // Banana
名前空間
プログラムをクラスで整理することにしたとしても、クラスが何百個もあったら、やっぱり把握するのが大変そうです。そこで、フォルダで整理できるようにしたのが名前空間という仕組みです。 namespace
というキーワードで、フォルダ名を書くというイメージです。パソコンのフォルダの仕組みと同様、同じ名前空間内に同じ名前のクラスを複数定義することはできませんが、名前空間が違えば同じ名前のクラスを作成することができます。
<?php
namespace Vendor\Package\Apple;
class BasicClass()
{
}
<?php
namespace Vendor\Package\Banana;
use Vendor\Package\Apple\BasicClass as Apple;
class BasicClass()
{
public function getApple()
{
// 他の名前空間のクラスは、名前空間を含むフルネームで指定するか、
// use キーワードで読み込むことで使えます。
// as キーワードでエイリアス指定することで、短い別名を指定することもできます。
return new Apple();
}
}
$a = new \Vendor\Package\Apple\BasicClass;
$b = new \Vendor\Package\Banana\BasicClass;
この名前空間は、PHPファイルそれ自身が入っているフォルダとは関係がありません。そのため、名前空間が \Apple
で入っているフォルダが /Banana
ということも可能です。しかし、せっかくPHPファイルをフォルダ分けして整理するための仕組みなのですから、実際のPHPファイルそれ自身も、同じ名前のフォルダに入れて整理するのが一般的です。
アクセス権
クラスを使っても、実際にはクラスの外部から、内部のプロパティに影響を与えることができてしまいます。上の例がまさに、クラス内のプロパティ $var
の値を変更しています。これでは当初の目的が達成できませんので、プロパティやメソッドはアクセス権を定義することができ、外部からの参照を拒否することができます。
<?php
class PrivateClass
{
// クラス内部からしかアクセスできない
protected $var;
}
$a = new PrivateClass();
$a->var = 'Apple'; // PHP Fatal error
このように、外からアクセスされてもいいもの、されたら困るもの、を分けることで、できるだけ副作用の少ない、安心安全なプログラムを作っていくことができます。
static
プロパティやメソッドを static
キーワードをつけて定義することで、インスタンスを作成せずにアクセス可能になります。静的(static)プロパティやメソッドにアクセスするには、矢印 ->
ではなくダブルコロン ::
を使用します。
<?php
class StaticSample
{
public static $message = 'Hello World!';
public static function displayHelloWorld()
{
echo self::$message;
}
}
StaticSample::displayHelloWorld(); // Hello World!
インスタンスを作成しないので、状態に関わらず常に同じ処理を行うという場合に使われます。
型
型 は安心安全なプログラミングに不可欠なものです。クラスを利用するときに、いちいちこんなチェックをしなきゃならない、それを忘れたためにエラーが起こる、と言われたら、誰だってめんどくさ!って思うでしょう。
$obj = new BadClass();
$result = $obj->getResult();
// nullでもなく、かつ文字列かどうかをいちいちチェックしてから表示…めんどっ
if ($result !== null && is_string($result)) {
echo $result;
}
クラスのメソッドは、引数にどんな型を受け入れるのか、返り値はどんな型になるのかを強制することができます。強制することで、いざ利用するときは余計な心配をしなくて済みますね。
<?php
class Foo
{
protected $bar;
protected $number = 0;
protected $string;
// 数値のみ受け付けます。
public function setNumber(int $number)
{
$this->number = $number;
}
// メソッドの返り値の型を強制できます。
public function getNumber(): int
{
return $this->number;
}
// 何も返さないという特殊な型指定 void も可能です。
public function setString(string $string): void
{
$this->string = $string;
}
// null または string 型
public function getString(): ?string
{
return $this->string;
}
// クラス名を指定することもできます。
public function setBar(Bar $bar): void
{
$this->bar = $bar;
}
public function getBar(): ?Bar
{
return $this->bar;
}
}
PHP 7.4 以降では、プロパティに型宣言を含めることができます。
<?php
// https://www.php.net/manual/ja/language.oop5.properties.php#example-208 から引用
class User
{
public int $id;
public ?string $name;
public function __construct(int $id, ?string $name)
{
$this->id = $id;
$this->name = $name;
}
}
$user = new User(1234, null);
継承
次に、クラスの便利な仕組みである継承と、継承に似ているけど異なる機能について紹介します。
クラスを宣言する際に、続けて extends
キーワードを使って、他のクラスを継承することができます。継承とは、かんたんにいえば、他のクラスをもとに一部を改造したクラスを作成できる機能です。
まず、下記のようなクラスがあったとします。
<?php
class BaseClass
{
protected $var;
// コンストラクタは、new キーワードでクラスのインスタンスを作成する際に呼ばれる特殊なメソッド。
public function __construct($var)
{
$this->var = $var;
}
public function getVar()
{
return $this->var;
}
}
extend
を使って、この BaseClass
をもとに改造クラスを作りましょう。
<?php
// BaseClass を継承して作成
class ExtendClass extends BaseClass
{
public function getVar()
{
return $this->var . '!!!';
}
}
実際に使ってみましょう。
$b = new BaseClass('Apple');
$e = new ExtendClass('Apple');
echo $b->getVar(); // Apple
echo $e->getVar(); // Apple!!!
ExtendClass
の方では、 getVar()
メソッドしか定義していないのに、 BaseClass
と同じようにコンストラクタに値を渡したり、プロパティの値を取得することができました。このように、書いてないものは継承元のクラスのまま、という形になります。
しかし、改造できるのは便利ですが、魔改造されるのも困りものですよね。そこで、継承で改造できる範囲とできない範囲を明確にすることができます。
<?php
class BaseClass
{
// アクセス権で private を指定すると、継承したクラスからは触れなくなります。
private $foo = 'foo';
// protected では、継承したクラスからは触れますが、外部からは触れません。
protected $bar = 'bar';
// public は継承できますし、外部からもアクセスできます。
public $baz = 'baz';
// 継承したクラスから呼べなくなります。
private function getFoo()
{
return $this->foo;
}
// 継承したクラスから呼べます。
protected function getBar()
{
return $this->bar;
}
// 継承したクラスと外部から呼べます。
public function getBaz()
{
return $this->baz;
}
}
class ExtendClass extends BaseClass
{
public function getPrivateProperty()
{
// エラーになります。
return $this->foo;
}
public function getProtectedProperty()
{
// 動きます。
return $this->bar;
}
public function getPublicProperty()
{
// 動きます。
return $this->bar;
}
public function callPrivateMethod()
{
// エラーになります。
return $this->getFoo();
}
public function callProtectedMethod()
{
// 動きます。
return $this->getBar();
}
public function callPublicMethod()
{
// 動きます。
return $this->getBaz();
}
}
抽象クラスと抽象メソッド
設計時点で、そもそも継承によって色々なクラスのバリエーションを作ることを想定しておきたい場合に使われるのが abstract
キーワードです。
<?php
abstract class AbstractClass
{
// このクラスを継承したクラスは、必ずこのメソッドを実装する必要があります。
abstract protected function getValue();
// このクラスを継承したクラスは全て、このメソッドが利用できます。
public function displayValue() {
echo $this->getValue();
}
}
abstract
キーワードをつけた抽象クラスは、インスタンスを作成することができません。
$obj = new AbstractClass(); // エラーになります
また、 abstract
キーワードをつけたメソッドは、継承したクラスで必ず実装する必要があります。
<?php
class ExtendClass extends AbstractClass
{
}
$obj = new ExtendClass(); // getValue() を実装していないのでエラーになります
このように制限を設けることで、同じ抽象クラスを継承したクラスたちの挙動をある程度共通かことができますので、兄弟として似たような使い勝手で使うことができます。
インターフェース
抽象クラスは便利ですが、制限もあります。それは、クラスは複数のクラスを継承できないということです。この制限は初学者には実感しづらいと思いますが、設計しだすと結構困ります。
そこで、「特定のメソッドの実装を強制する」という抽象メソッドの機能のみを取り出して、複数継承できるようにしたのがインターフェースです。
<?php
interface NameInterface
{
public function getName();
}
interface DescriptionInterface
{
public function getDescription();
}
// インターフェースの場合は extends ではなく implements を使います。
class ExampleClass implements NameInterface, DescriptionInterface
{
public function getName()
{
return 'Example';
}
public function getDescription()
{
return 'This is an example class';
}
}
気軽に複数指定できるという特徴を活かして、複数のクラスをカテゴリー分けするのに使うこともあります。
$obj = new ExampleClass();
if ($obj instanceof NameInterface) {
echo 'This class implements NameInterface';
}
トレイト
抽象メソッドも便利なものですが、複数のクラスで似たようなメソッドを使い回すという用途に使うには、複数のクラスを継承できないという制約がやはり邪魔になってきます。そのような時のために、特定のプロパティやメソッドを使い回すための仕組みがトレイトです。Sassでいうmixinみたいな感じです。
<?php
trait NameTrait
{
protected $name;
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
class Foo
{
use NameTrait;
}
$foo = new Foo();
$foo->setName('Apple');
echo $foo->getName(); // Apple
質疑応答
concrete5 で Page や \Page という表記が色々あるのはなぜ?
concrete5 でページを表現するクラスは、名前空間を含めたフルネームでは \Concrete\Core\Page\Page
です。このクラスには エイリアス が設定されており、 \Page
という短いクラス名でも使えるようになっています。
次に、テンプレートファイルは特に名前空間が宣言されていないため、フルネームじゃなくてもこのクラスを呼ぶことができます。
<?php
namespace Foo;
// こう書くと、宣言されている名前空間が適用されるので
// \Foo\Page というクラスを呼び出したことになります。
$c = new Page();
// バックスラッシュから始めると、フルネームでクラスを呼び出したことになります。
$c = new \Page();
// エイリアスはクラスの別名を定義するものなので、上の呼び出しと下の呼び出しは結果は全く同じです。
$c = new \Concrete\Core\Page\Page();
<?php
// 名前空間が宣言されていないPHPファイル内では、次の2つは全く同じです。
$c = new Page();
$c = new \Page();
名前空間は見た目はとっつきにくいですが、フォルダと同じと考えれば、CSSから画像を呼び出すのと同じです。
/* 絶対パスで書く場合はスラッシュではじめますし */
background-image: url(/asset/images/example.png);
/* 相対パスで書くこともありますよね。名前空間も同じような仕組みです */
background-image: url(../images/example.png);
extends と implements は同時に使える?
使えます!
<?php
namespace Concrete\Core\Page;
use Concrete\Core\Attribute\ObjectInterface as AttributeObjectInterface;
use Concrete\Core\Export\ExportableInterface;
use Concrete\Core\Permission\AssignableObjectInterface;
use Concrete\Core\Permission\AssignableObjectTrait;
use Concrete\Core\Site\SiteAggregateInterface;
use Concrete\Core\Site\Tree\TreeInterface;
use Concrete\Core\Summary\Category\CategoryMemberInterface;
class Page extends Collection implements CategoryMemberInterface,
\Concrete\Core\Permission\ObjectInterface,
AttributeObjectInterface,
AssignableObjectInterface,
TreeInterface,
SiteAggregateInterface,
ExportableInterface
{
use AssignableObjectTrait;
...
extends で孫は作れる?
作れます!
<?php
class MyClass
{
}
class ChildClass extends MyClass
{
}
class GrandChildClass extends ChildClass
{
}
trait で private は使える?
使えます!トレイトはあくまで一部のメソッドやプロパティを「そのクラス自身の一部として読み込む」挙動になりますので、親だったり子だったりといった関係性からは独立したものになっています。
オブジェクト指向の考えから派生して生まれた継承という仕組みですが、その便利なポイントが実は、複数のクラスで共通点を作れるというところだった。この共通点を作るのに実は親子のような関係性は不要だしむしろ邪魔、ということが、インターフェースやトレイトが生まれた背景なんでしょうね。