diff --git a/public/sample/tips/object-oriented/confirm.php b/public/sample/tips/object-oriented/confirm.php new file mode 100644 index 0000000..1d741ec --- /dev/null +++ b/public/sample/tips/object-oriented/confirm.php @@ -0,0 +1,22 @@ +" . print_r($_POST, true) . ""; + // => + // Array + // ( + // [name] => John Doe + // [email] => john@example.com + // [gender] => man + // [message] => Hello, this is a message. + // ) +} + +if ($_SERVER["REQUEST_METHOD"] === "POST") { + handle_post_request(); +} diff --git a/public/sample/tips/object-oriented/form.html b/public/sample/tips/object-oriented/form.html new file mode 100644 index 0000000..aae5212 --- /dev/null +++ b/public/sample/tips/object-oriented/form.html @@ -0,0 +1,187 @@ + + + + + + + お問い合わせフォーム + + + + + +
+
+

お問い合わせ

+

+ 必要事項を入力して、送信ボタンを押してください。 +

+ +
+ + +
+
+ + +
+
+
性別
+
+ + + +
+
+
+ + +
+
+ +
+
+
+ + + diff --git a/src/pages/tips/object-oriented.mdx b/src/pages/tips/object-oriented.mdx new file mode 100644 index 0000000..e05071e --- /dev/null +++ b/src/pages/tips/object-oriented.mdx @@ -0,0 +1,1221 @@ +--- +# vim: set tabstop=4 softtabstop=4 shiftwidth=4 expandtab: +layout: "@/layouts/MarkdownLayout.astro" +--- + +import Toc from "../../components/Toc.astro"; +import ExampleWrapper from "@/components/ExampleWrapper.astro"; +import RenderFile from "@/components/RenderFile"; +import RenderHtml from "@/components/RenderHtml"; +import Details from "@/components/Details.astro"; + +export const title = "オブジェクト指向とクラス"; + +# {title} + +スマホのボリュームの上げ下げをするために、わざわざ蓋を明けてこの端子を触ってね・・・ということはないですよね。 +大抵は音量を操作するためのボタンが用意されて、そこを操作してもらううことになると思います。 + +今のは極端な例ではありましたが、プログラミングの世界でも同様のことが起こっています。 + +オブジェクト指向とはこのような危険な操作を隠して、ユーザーが安全に操作できるようにするための設計思想です。 + +## TOC + +## 大事な用語 + +- Class (クラス) +- インスタンス +- 継承 + +## データのまとまり + +プログラムを組むとき、データのまとまりを扱うことがよくあります。 + +- 設定ファイル +- データベースから取得したデータ +- 入力フォームから送られてきたデータ +- ...etc + +この中でも入力フォームは、開発者自身であってもどのような値が入ってくるかわからなくなることが多いです。 + +このようなフォームがあったとしても、 + + + +データを受け取る側は、どのような値が入ってきているのかわかりません。 +これを知るために、とりあえず変数の中身を表示する処理を書いてみたり、 + +```php file=public/sample/tips/object-oriented/confirm.php +``` + +
+ +もととなるHTMLの`name`属性を直接確認してみたりすることもあるでしょう。 + +
+ +```php file=public/sample/tips/object-oriented/form.html collapse={1-153, 184-188} /name="[a-zA-Z0-9]+"/ +``` + +
+ +
+ +これらの確認を経て、送られてくるデータには以下のものがあるとはじめてわかります。 + +- name +- email +- gender +- message + +しかし、予めどのようなデータが入ってくるのかを把握したいものです。 + +今回の例のような入力フォームであれば、HTMLの変更によって意図しないデータが入ってくる可能性もありますし、そもそもHTMLを見ないとどのようなデータが入ってくるのかわからないのは、あまり良い状態とは言えません。 + +これを解決するために、あるデータの集まりに対して、具体的になにがあるのかを説明するようなアプローチが存在します。 +その最も原始的な方法が、 `構造体(struct)` や `クラス(class)` を使うことです。 + +これはHTMLから送られてくるデータ全体をひとつのまとまりとして扱う考え方です。 +この構造の中に、具体的に何が入ってくるのかを定義することができます。 + +```php + + +一方でクラスは、データ構造を定義するための型で、設計図のようなものだと説明しました。 +これを具体的なデータとして使うためには、`new` というキーワードを使って変数に代入する必要があります。 + +```php collapse={2-8} + + +この `new クラス名()` で作られたものを、クラスの **インスタンス (instance)** と呼びます。 + +ところでこの時点では、作られたインスタンスには、特に初期値を設定していません。 +なにかしらの手段で、値を入れてあげる必要があります。 + +```php collapse={2-8} + +// NULL +// ["email"]=> +// NULL +// ["gender"]=> +// NULL +// ["message"]=> +// NULL +// } + +$formField->name = $_POST["name"]; +$formField->email = $_POST["email"]; +$formField->gender = $_POST["gender"]; +$formField->message = $_POST["message"]; + +var_dump($formField); +// object(FormField)#1 (4) { +// ["name"]=> +// string(5) "Alice" +// ["email"]=> +// string(17) "alice@example.com" +// ["gender"]=> +// string(3) "woman" +// ["message"]=> +// string(11) "Hello world" +// } +``` + +
+ +これで、クラスを使うための最低限の準備ができました。 + +### 振る舞いを定義する (メソッド / 関数) + +クラスでは、インスタンスが持っているデータを活用して、なにかしらの振る舞いを定義することができます。 +これを **メソッド (method)** と呼びます。 + +例として、`gender` に値を設定するメソッドを定義してみましょう。 + +```php {8-11} +gender = $gender; + } +} + +$formField = new FormField(); +$formField->setGender("man"); +echo $formField->gender . PHP_EOL; +// => man + +$formField->setGender("woman"); +echo $formField->gender . PHP_EOL; +// => woman +``` + +
+ +`$this` という変数が出てきました。 + +通常クラスのインスタンスは、`$formField = new FormField();` のように、変数に代入されます。 +しかしこの `$formField` という変数はクラスの外で定義されている変数で、クラスの中からはどのような変数名で代入されるのかわかりません。 + +とはいえクラスの中から、自分自身が持っているデータにアクセスしたいことはよくあります。 +このときに、クラスのメソッド内でのみ使える特別な変数 `$this` を使うことで、クラスの中から自分自身のインスタンスを指すことができます。 + +この`$this`を経由して、自身が持っている `gender` というプロパティにアクセス、値を代入するということができます。 + +
+
+ +値を設定するメソッドを定義したので、次は値を活用するメソッドも定義してみましょう。 + +```php {12-38} +gender = $gender; + } + + public function reply_template() { + $gender_display = null; + if ($this->gender === "man") { + $gender_display = "男性"; + } else if ($this->gender === "woman") { + $gender_display = "女性"; + } else { + $gender_display = "不明"; + } + + $body = "この度はお問い合わせいただきまして、誠にありがとうございます。 + いただいた内容を確認の上、改めてご連絡いたします。 + + ご入力内容 + + お名前: {$this->name} + 性別: {$gender_display} + メールアドレス: {$this->email} + + お問い合わせ内容 + --- + {$this->message} + --- + "; + + return $body; + } +} + +$formField = new FormField(); +$formField->name = "Alice"; +$formField->email = "alice@example.com"; +$formField->gender = "woman"; +$formField->message = "Hello world"; + +$template = $formField->reply_template(); +// この度はお問い合わせいただきまして、誠にありがとうございます。 +// いただいた内容を確認の上、改めてご連絡いたします。 +// +// ご入力内容 +// +// お名前: Alice +// 性別: 女性 +// メールアドレス: alice@example.com +// +// お問い合わせ内容 +// --- +// Hello world +// --- + +$formField->name = "Bob"; + +$template = $formField->reply_template(); +// 名前がBobに変わっていることがわかる +``` + +### 特別なメソッド + +クラスの中で、特別な意味を持つメソッドが存在します。 +この中では、**constructor (コンストラクタ)** と呼ばれるメソッドが特に重要です。 + +[クラスの使い方](#クラスの使い方) の章で、クラスをインスタンス化するためには、`new クラス名()` という構文を使う必要があると説明しました。 +その書き方は、クラス名の後ろに、括弧 `()` をつける形になっています。 + +まるで関数を呼び出すような書き方になっていますが、これがコンストラクタと呼ばれる特別なメソッドの呼び出しになっています。 + +具体的に見てみましょう。 + +```php + newされました +``` + +このコンストラクタには、引数を定義することもできます。 +その多くの場合、インスタンスを作るときにプロパティの初期値を設定するために使われます。 + +```php +name = $name; + } +} + +// 引数が指定されていないので、エラーになる +// $user = new User(); + +$user = new User("Alice"); // nameプロパティに"Alice"が入ったインスタンスが代入される +echo $user->name . PHP_EOL; +// => Alice +``` + +
+ +なお、このコンストラクタは言語によって書き方が異なります。 + +
+ +```java title=Java +class User { + public String name; + + // クラス名がそのままコンストラクタの名前になる + public User() { + System.out.println("newされました"); + } +} +``` + +
+ +```python title=Python +class User: + # コンストラクタは __init__ という名前になる + def __init__(self): + print("newされました") +``` + +
+ +
+ +### 派生を作る (継承) + +クラスには、**継承 (inheritance)** という機能があります。 +これはあるクラスをもとにして、さらに新しいクラスを作ることができる機能です。 + +継承をすることにより、もとのクラスの一部分だけを変更したり、もとのクラスの機能を拡張したりすることができます。 + +この、継承をするためのクラスを、**派生クラス (derived class)** と呼びます。 +逆に、もとになるクラスを、**基底クラス (base class)** と呼びます。 + +また、基底クラスに存在するメソッドを派生クラスで、同じ名前で再定義することができます。 +これを **オーバーライド (override)** と呼びます。 + +```php +name = $name; + } + + public function greet(User $target) { + echo "こんにちは、{$target->name}さん!"; + } +} + +$owner = new User("オーナー"); + +$user1 = new User("Bob"); +$user1->greet($owner); +// => こんにちは、オーナーさん! + + +class CheerfulUser extends User { + // greet() メソッドをオーバーライド + public function greet(User $target) { + echo "やあ、{$target->name}さん!"; + } +} + +$user2 = new CheerfulUser("Charlie"); +$user2->greet($owner); +// => やあ、オーナーさん! + + +class PoliteUser extends User { + public function greet(User $target) { + super::greet($target); // Userクラスのgreetメソッドを呼び出す + echo "今日もいい天気ですね!"; + } +} + +$user3 = new PoliteUser("Dave"); +$user3->greet($owner); +// => こんにちは、オーナーさん!今日もいい天気ですね! + + +class RudeUser extends User { + public function greet(User $target) { + echo "おい、{$target->name}!"; + } + + // 基底クラスにはない、shout() というメソッドを独自に定義した + public function shout(User $target) { + echo "うるせぇ、{$target->name}!"; + } +} + +$user4 = new RudeUser("Eve"); +$user4->greet($owner); +// => おい、オーナー! +$user4->shout($owner); +// => うるせぇ、オーナー! +``` + +### 敢えてアクセス制限をかける (アクセス修飾子) + +ここまで読み進めてきた人に取っては、ソースコードに含まれる `public` というキーワードが気になっているかもしれません。 +これは、どこからでもアクセスすることを許可するという意味の **アクセス修飾子 (access modifier)** と呼ばれるものです。 + +アクセス修飾子には、いくつかの種類がありますが、主に以下の3つが使われます。 + +| 種類 | 説明 | +| :-- | :-- | +| `public` | どこからでもアクセスできる | +| `protected` | クラスの中と、継承したクラスの中からアクセスできる | +| `private` | クラスの中からのみアクセスできる | + +具体的に見てみましょう。 + +あるユーザーに対応するクラスがあったとして、そのメソッドに成人かどうかを判定する `isAdult()` というメソッドがあったとします。 +また、`birthday()` を呼び出すことが、誕生日を迎えることを表すとします。 + +```php +name = $values["name"]; + $this->age = $values["age"]; + } + + public function birthday() { + $this->age += 1; + } + + public function isAdult() { + if ($this->age >= 20) { + echo "{$this->age}歳なので、成人です"; + } else { + echo "{$this->age}歳なので、未成年です"; + } + } +} + +$user = new User([ "name" => "Alice", "age" => 18 ]); + +$user->isAdult(); +// => 18歳なので、未成年です + +// 2回誕生日を迎える +$user->birthday(); +$user->birthday(); + +$user->isAdult(); +// => 20歳なので、成人です +``` + +
+ +今、すべてのアクセス修飾子が `public` になっています。 +このとき、どのような不都合があるでしょうか? + +```php collapse={6-22} add={"ageはpublicなので、外部から代入できる": 37-38} +name = $values["name"]; + $this->age = $values["age"]; + } + + public function birthday() { + $this->age += 1; + } + + public function isAdult() { + if ($this->age >= 20) { + echo "{$this->age}歳なので、成人です"; + } else { + echo "{$this->age}歳なので、未成年です"; + } + } +} + +$user = new User([ "name" => "Alice", "age" => 18 ]); + +$user->isAdult(); +// => 18歳なので、未成年です + +// 2回誕生日を迎える +$user->birthday(); +$user->birthday(); + +$user->isAdult(); +// => 20歳なので、成人です + + +$user->age = 10; // 年齢詐称 + +$user->isAdult(); +// => 10歳なので、未成年です +``` + +
+ +なんと年齢を詐称することができてしまいました。 +これができるということは、未成年にも関わらずにお酒を飲んだり、タバコを吸ったりすることを許してしまうことになります。 + +これをさせないために、`age` プロパティを `private` にしてみましょう。 + +```php collapse={6-22} "private" {"ageはprivateなので、外部から操作できない": 37-38} +name = $values["name"]; + $this->age = $values["age"]; + } + + public function birthday() { + $this->age += 1; + } + + public function isAdult() { + if ($this->age >= 20) { + echo "{$this->age}歳なので、成人です"; + } else { + echo "{$this->age}歳なので、未成年です"; + } + } +} + +$user = new User([ "name" => "Alice", "age" => 18 ]); + +$user->isAdult(); +// => 18歳なので、未成年です + +// 2回誕生日を迎える +$user->birthday(); +$user->birthday(); + +$user->isAdult(); +// => 20歳なので、成人です + + +$user->age = 10; // Error: Cannot access private property User::$age +``` + +
+ +これで、誕生日を迎える以外の方法で、年齢を変更することができなくなりました。 + +ところで、`User`というクラスから、派生したクラスが作られたとします。 +そこに、`greet()` というメソッドを定義して、ユーザーに挨拶させたいとします。 + +```php add={25-29} "private" +name = $values["name"]; + $this->age = $values["age"]; + } + + public function birthday() { + $this->age += 1; + } + + public function isAdult() { + if ($this->age >= 20) { + echo "{$this->age}歳なので、成人です"; + } else { + echo "{$this->age}歳なので、未成年です"; + } + } +} + +class ExtendedUser extends User { + public function greet() { + echo "こんにちは、{$this->name}さん!"; // Undefined property: User::$name + } +} +``` + +
+ +これはエラーになります。 +なぜなら、基底クラスである `User` クラスの `name` プロパティは、アクセス修飾子が `private` になってなっています。 +そのため、継承先のクラスではこのプロパティにアクセスすることができないからです。 + +これを解決するために、アクセス修飾子を `protected` にしてみましょう。 + +```php "protected" {31-33} {"protectedなので、関係のない場所からは操作できない": 45-46} +name = $values["name"]; + $this->age = $values["age"]; + } + + public function birthday() { + $this->age += 1; + } + + public function isAdult() { + if ($this->age >= 20) { + echo "{$this->age}歳なので、成人です"; + } else { + echo "{$this->age}歳なので、未成年です"; + } + } +} + +class ExtendedUser extends User { + public function greet() { + echo "こんにちは、{$this->name}さん!"; + } +} + +$user = new ExtendedUser([ "name" => "Alice", "age" => 18 ]); +$uset->greet(); +// => こんにちは、Aliceさん! + +$user->isAdult(); +// => 18歳なので、未成年です + +// 2回誕生日を迎える +$user->birthday(); +$user->birthday(); + +$user->isAdult(); +// => 20歳なので、成人です + + +$user->age = 10; // Error: Cannot access protected property User::$age +``` + +
+ +これで、継承先のクラスからは `name` プロパティにアクセスすることができるようになりました。 +`age`も同様に `protected` にしているので、継承先のクラスからはアクセスできますが、関係のない場所からはアクセスできないようになっています。 + +### staticな変数 + +クラスは、基本的にはインスタンスごとにデータを持ちます。 +一方で、クラス全体で共有されるデータを定義することもできます。 + +これを **staticな変数** 、日本語では **静的変数** と呼びます。 + +staticな変数は、クラス名を使ってアクセスすることができます。 +またクラス全体で共有しているため、インスタンスを通してアクセスしても同じ値が見えるようになっています。 + +```php + 初期値 (MyClass) +// => 初期値 (instance) + +MyClass::$static_variable = "MyClassから値を変更"; + +echo MyClass::$static_variable . " (MyClass)" . PHP_EOL; +echo $instance::$static_variable . " (instance)" . PHP_EOL; +// => MyClassから値を変更 (MyClass) +// => MyClassから値を変更 (instance) + +$instance::$static_variable = "instanceから値を変更"; + +echo MyClass::$static_variable . " (MyClass)" . PHP_EOL; +echo $instance::$static_variable . " (instance)" . PHP_EOL; +// => instanceから値を変更 (MyClass) +// => instanceから値を変更 (instance) +``` + +
+ +このstaticな変数は、継承したクラスからもアクセスすることができます。 +ただし継承先で同じ名前のstaticな変数を定義している場合、それ以降は別の変数として扱われるようになります。 + +```php + 初期値 (MyClass1) +// => 初期値 (MyClass2) +// => MyClass3の初期値 (MyClass3) +// => MyClass3の初期値 (MyClass4) + +MyClass1::$static_variable = "MyClass1から値を変更"; +echo MyClass1::$static_variable . " (MyClass1)" . PHP_EOL; +echo MyClass2::$static_variable . " (MyClass2)" . PHP_EOL; +echo MyClass3::$static_variable . " (MyClass3)" . PHP_EOL; +echo MyClass4::$static_variable . " (MyClass4)" . PHP_EOL; +// => MyClass1から値を変更 (MyClass1) +// => MyClass1から値を変更 (MyClass2) +// => MyClass3の初期値 (MyClass3) +// => MyClass3の初期値 (MyClass4) + +MyClass3::$static_variable = "MyClass3から値を変更"; +echo MyClass1::$static_variable . " (MyClass1)" . PHP_EOL; +echo MyClass2::$static_variable . " (MyClass2)" . PHP_EOL; +echo MyClass3::$static_variable . " (MyClass3)" . PHP_EOL; +echo MyClass4::$static_variable . " (MyClass4)" . PHP_EOL; +// => MyClass1から値を変更 (MyClass1) +// => MyClass1から値を変更 (MyClass2) +// => MyClass3から値を変更 (MyClass3) +// => MyClass3から値を変更 (MyClass4) +``` + +### staticなメソッド + +通常のメソッドは、インスタンスを通して呼び出す必要があります。 +これに対してstaticなメソッドはインスタンスを通さずに、クラス名を使って呼び出すことができます。 + +```php + 私はMyClassです! +``` + +
+ +staticなメソッドは一見通常の関数と同じ用に見えます。 +しかしクラスの中に定義されているため、そのクラスの [アクセス修飾子](#敢えてアクセス制限をかける-アクセス修飾子) を気にせず、自由にアクセスすることができます。 + +```php +data = $data; + + return $instance; + } + + public function display() { + var_dump($this->data); + } +} + +$instance = MyClass::fromData('Hello, World!'); +$instance->display(); +// => string(13) "Hello, World!" + + +function makeMyClass($data) { + $instance = new MyClass(); + + // public なプロパティではないため、外部からアクセスできない + $instance->data = $data; // Uncaught Error: Cannot access protected property MyClass::$data + + return $instance; +} + +$instance2 = makeMyClass('Hello, World!'); // エラー +``` + +
+ +通常のメソッドと同様に、staticなメソッドもオーバーライドすることができます。 + +```php + 私はBaseClassです! + +DerivedClass::introduce(); +// => 私はDerivedClassです! +``` + +### どのメソッドを呼び出すかを指定する (self, static, parent) + +当然ではありますが、staticなメソッドから別のstaticなメソッドを呼び出すこともできます。 + +```php + 私はBaseClassです! +// 私はBaseClassです! +``` + +
+ +今のコードでは、`introduceTwice()` メソッドは、`BaseClass::introduce()` を呼び出しています。 + +ところで、この`BaseClass`が継承されて、`DerivedClass`が作られたとします。 +そのときには自己紹介をする`introduce()` メソッドは、きっとオーバーライドされるでしょう。 + +```php collapse={4-11} add={14-18} add={"意図した動作ではない?":25-30} + 私はBaseClassです! +BaseClass::introduceTwice(); +// => 私はBaseClassです! +// 私はBaseClassです! + +DerivedClass::introduce(); +// => 私はDerivedClassです! +DerivedClass::introduceTwice(); +// => 私はBaseClassです! +// 私はBaseClassです! +``` + +
+ +この状態では、まだ問題が残っています。 + +`introduceTwice()` メソッドは`BaseClass::introduce()` を呼び出しているため、`DerivedClass` から呼び出したときも`BaseClass::introduce()` が呼び出されてしまいます。 + +これを、`DerivedClass::introduce()` を呼び出すようにするにはどうしたらいいでしょうか? +愚直に、`introduceTwice()` メソッドをオーバーライドして、`DerivedClass::introduce()` を呼び出すようにするしかないのでしょうか? + +```php collapse={3-13,25-29} add={19-22} {"これも1つの解決策":30-35} + 私はBaseClassです! +BaseClass::introduceTwice(); +// => 私はBaseClassです! +// 私はBaseClassです! + +DerivedClass::introduce(); +// => 私はDerivedClassです! +DerivedClass::introduceTwice(); +// => 私はDerivedClassです! +// 私はDerivedClassです! +``` + +
+ +今回の問題の根本は、継承されたクラスからメソッドを呼ぶときに、どの時点のメソッドを呼び出すかを指定する手段を知らない状態だったことにあります。 +これを解決するためのキーワードが、`self`、`static`、`parent` です。 + +```php "parent" "self" /static(?=::)/ add={8-12,14-17,30-33} + 私はBaseClassです! +BaseClass::introduceSelf(); +// => 私はBaseClassです! +BaseClass::introduceStatic(); +// => 私はBaseClassです! + +DerivedClass::introduce(); +// => 私はDerivedClassです! +DerivedClass::introduceSelf(); // オーバーライドされていないため、BaseClass::introduce() が呼び出される +// => 私はBaseClassです! +DerivedClass::introduceStatic(); +// => 私はDerivedClassです! + +DerivedClass::introduceParent(); +// => 私はBaseClassです! +``` + +
+ +つまり意図した`introduceTwicw()` メソッドを定義するためには、`self` ではなく `static` を使う必要があったということになります。 + +```php del={9-10} add={11-12} + 私はBaseClassです! +BaseClass::introduceTwice(); +// => 私はBaseClassです! +// 私はBaseClassです! + +DerivedClass::introduce(); +// => 私はDerivedClassです! +DerivedClass::introduceTwice(); +// => 私はDerivedClassです! +// 私はDerivedClassです! +``` + +staticなメソッドから別のstaticなメソッドを呼び出すときには、`self` と `static` のどちらを使うかで、呼び出されるメソッドが変わることがわかります。 + +## よく見るコード例 + +### 初期化のための静的メソッド + +初期値となるデータを渡すと、クラスのインスタンスを作るような静的メソッド + +```php {9-22} +name = $values["name"]; + $instance->email = $values["email"]; + $instance->gender = $values["gender"]; + $instance->message = $values["message"]; + + return $instance; + } +} + +// $_POSTの中身をもとに、FormFieldクラスのインスタンスを作る +$formField = FormField::fromValues($_POST); +``` + +### 別の型に変換するためのメソッド + +入力フォームの情報を、そのままユーザー情報に変換する処理 + +```php {3-24, 46-57} +name = $values["name"]; + $instance->email = $values["email"]; + $instance->gender = $values["gender"]; + $instance->message = $values["message"]; + + return $instance; + } + + public function toUser() { + $values = [ + "name" => $this->name, + "gender" => $this->gender, + "email" => $this->email, + ]; + + $user = new User(); + $user->fromValues($values); + + return $user; + } +} + +// $_POSTの中身をもとに、FormFieldクラスのインスタンスを作る +$formField = FormField::fromValues($_POST); + +// FormFieldクラスが持つ情報で、Userクラスのインスタンスを作る +$user = $formField->toUser(); +$user->save(); +```