👻

コンストラクタってどういうとき使うんですか?

2024/02/07に公開

コンストラクタの役割

ある日、後輩さんとペアプロをしたときのこと。
突然、後輩さんがこう言いました。
「あのー、コンストラクタってそういえば、どういうとき使うんですか?」

newするときに呼ばれるというのは分かるのだけれど、使うシチュエーションがよく分からないとのことでした。

う~~~~~~~~~~~~~~~~~ん!?
なるほど? 難しいことをお訊きになる・・・

こういう「そもそも」みたいな質問って一番難しい。
何気ない一言に、私はとても悩みました。
言われてみれば、なんとなく使っていた気がします。
コンストラクタって、どういう役割なんでしょう。
でも私は先輩だからちゃんと答えてあげたいのです。

しばらく悩んだあと、私はこう答えました。
インスタンスが完全な形であることを保証するためのもの……だと思う……たぶん」

できそこなインスタンス

概念に名前をつけると、共通認識になります。
私はよく、仕事で登場した新しい概念に、名前をつけるのが好きです。

さて、ある日、こんなコードを書きました。

class Customer {
    public string $name;
    public string $address1;
    public string $address2;
    public string $phone;

    public function save(): void
    {
        //INSERT文を発行する
    }
}

$customer = new Customer();
$customer->name = "まきしま";
$customer->address1 = "東京都千代田区";
$customer->address2 = "永田町1-1-1";
$customer->phone = "09012345678";
$customer->save();

こういうコード、よくあります。
ORMを使って、データベースに保存するようなコードなんかは、こんな感じになることが多いかと思います。

よく見るコードではあるのですが、よく考えると、「本当にこれでいいのかな?」と疑問に思ったことがありました。

$order = new Order();
var_dump($order);

最初の1行目の状態でvar_dumpすると、下記のように、各プロパティはuninitializedになります。

object(Customer)#1 (0) {
  ["name"]=>
  uninitialized(string)
  ["address1"]=>
  uninitialized(string)
  ["address2"]=>
  uninitialized(string)
  ["phone"]=>
  uninitialized(string)
}

つまり、この時点では不完全、「できそこない」なインスタンスになるわけです。
この状態のインスタンスをできそこなインスタンスと呼ぶことにしました。

できそこなインスタンス何が問題なのか

できそこなインスタンスは、Customerクラスのインスタンスとして、不完全です。
なぜなら、name, address1, address2, phoneのすべてがしっかり揃った状態で、初めてCustomerクラスのインスタンスになるわけです。

できそこなインスタンスの状態では、たとえばsave()メソッドを呼んでも、DB側の制約によっては保存できないこともあります。

できそこなインスタンスになっている状態があると、save()メソッドを呼ぶために、「すべてのプロパティに値が入っていること」という前提条件が必要となってしまいます。

これだと安心してメソッドがコールできません。

コンストラクタ登場

できそこなインスタンスができないようにするためには、不完全な状態でnewできないようにすればいいわけです。
そうなると、次のように、コンストラクタが登場します。

class Customer {
    public string $name;
    public string $address1;
    public string $address2;
    public string $phone;

    public function __construct(
        string $name,
        string $address1,
        string $address2,
        string $phone,
    ){
        $this->name = $name;
        $this->address1 = $address1;
        $this->address2 = $address2;
        $this->phone = $phone;
    }

    public function save(): void
    {
        //INSERT文を発行する
    }
}

$customer = new Customer(
    "まきしま",
    "東京都千代田区",
    "永田町1-1-1",
    "09012345678"
);
$customer->save();

$customerが、Customerクラスのインスタンスとして、成立することがこれで保証できました。

パラメーターがいっぱいあるとき

「ふむふむなるほど、わかりました。 『コンストラクタはインスタンスの状態が不完全でないことを保証するためにある』ってことですね・・・ところで、、、」

私が書いてるクラス、めっちゃプロパティが多いんですけど、どうすればいいと思いますか?

後輩さんが書いているクラスは、お買い物サイトで商品を購入されたときのデータを管理するためのクラスでした。

特にProductクラスがめちゃめちゃ長い状態でした。

class Order {
    public Customer $customer;

    /**
     * @var OrderDetail[]
     */
    public array $order_details;
}

class Customer {
    public string $name;
    public string $address1;
    public string $address2;
    public string $phone;
}

class OrderDetail {
    public Product $product;

    /**
     * 購入時の価格
     */
    public int $price;

    /**
     * 購入個数
     */
    public int $count;
}

class Product {
    public string $product_id;
    public string $product_name;
    public string $product_text;
    public string $color;
    public string $size;
    //などなど、いっぱい!!
}

一旦、Productクラスにだけ着目してみます。
さっきの理屈でいうと、

class Product {
    public function __construct(
        string $product_id,
        string $product_name,
        string $product_text = "",
        string $color,
        string $size
    )
}


new Product(
    't-shirt-white-l',
    'Tシャツ',
    '',
    '白',
    'L', //・・・・・・・・・
);

となるわけですが、いかんせん対応が分かりづらい。
あと、$product_textは、オプション項目で、今までは一番最後に書いてあったのに、後からcolorとsizeが追加になってしまって、オプション項目が間に埋もれてしまったそうです。

こういうときは、PHP8から導入された、名前付き引数を使うと整理されます。

new Product(
    product_id: 't-shirt-white-l',
    product_name: 'Tシャツ',
    color: '白',
    size: 'L',
    //・・・・・・・・・
);

これを全体に適用したら完成です。

$order = new Order(
    customer: new Customer(
        name: 'まきしま',
        address1: '東京都千代田区',
        address2: '永田町1-1-1',
        phone: '09012345678',
    ),
    order_details: [
        new OrderDetail(
            product: new Product(
                product_id: 't-shirt-white-l',
                product_name: 'Tシャツ',
                color: '白',
                size: 'L',
                //・・・・・・・・・
            ),
            price: 800,
            count: 3
        ),
        new OrderDetail(
            product: new Product(
                product_id: 't-shirt-red-m',
                product_name: 'Tシャツ',
                color: '赤',
                size: 'M',
                //・・・・・・・・・
            ),
            price: 500,
            count: 6
        )
    ]
);
$order->save();

こうすると、JSONみたいで、結構いい感じに収まっているように思います。
なかなか一発で全部の情報を揃えるのも、もしかしたら難しいところがあるかもしれませんし、やってみたらうまくいかないことも多いかもしれません。

私もこの辺りもうちょっと研究してみて、良いやり方を模索していきたいです。

それにしても、人に何かを教えるというのは、何か質問を受けるたびに私の方がよっぽど勉強になっています。
(教える、というより、最適解を一緒に考える、と言ったほうが正しいかもしれません)

ここまで読んでくださってありがとうございます。
よかったらぜひ、感想を教えて下さいね!

Discussion