2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NEAdvent Calendar 2023

Day 10

なぜあの日の私は「メソッドは常に静的に定義した方が良い」と思ってしまったのか

Last updated at Posted at 2023-12-09

OOP(オブジェクト指向)言語の実践経験者各位におかれましてはとんでもないタイトルに映るかと思います。
実際自分自身でもゾッとします。主語が大きめなのもゾッとしますね。大丈夫かな。

でも、恥ずかしい話、以前は本気で思ってました。

いついかなるとき1 でも、 OOPに於いては動的(dynamic)なメソッドより静的(static)なメソッドのほうが様々な面で優れているって。

似た思考の方は、そのプロダクトの危険信号が思考に反映されているかもしれません。

誤った思考をたどるサンプルコード2

staticメソッドを使いたくなってしまったクラスの例
<?php
class Test
{
    const MESSAGE_TYPE_SUCCESS = '正常';
    const MESSAGE_TYPE_ERROR   = 'エラー';
    public $property = null;

    public function setProperty($property)
    {
        $this->property = $property;
    }

    public function message($type = self::MESSAGE_TYPE_SUCCESS) // 実装してみたけど安定しないなぁ…プロパティを信用して大丈夫かなぁ…
    {
        return '[' . $type . ']プロパティの値は[' . $this->publicProperty . ']です。' . "\n";
    }
}

// ----- 以降、このクラスを扱おうとするスコープ -----

$test = new Test();
echo $test->message(); // [正常]プロパティの値は[]です。

// 1. setterメソッドでset
$test->setProperty('test_1');
echo $test->message(); // [正常]プロパティの値は[test_1]です。

// 2. publicなら直接set
$test->property ='test_2';
echo $test->message(); // [正常]プロパティの値は[test_2]です。

$this->message() の返り値は、インスタンスの状態(=プロパティの値)に応じて変化します。

ポイントは

  1. setterメソッド、あるいはpublicプロパティそれ自体によって、そのインスタンスのプロパティがインスタンス化以後に変更され得る
  2. そのプロパティが必要とする処理に関連している

といったところですね3。これを見た私、それに依って message() メソッドを以下のように修正してしまいました。

静的化してしまったmessage()
<?php
class Test
{
    // :
    public static function message($property, $type = self::MESSAGE_TYPE_SUCCESS) // これならインスタンスの状態に関わらず安定してうごくぞ\(^o^)/
    {
        return '[' . $type . ']プロパティの値は[' . $property . ']です。' . "\n";
    }
}

// 確実に意図した文字列で動いてくれる\(^o^)/
echo Test::message('test_3'); // [正常]プロパティの値は[test_3]です。

こうして出来上がったstaticメソッド、 静的なので他のクラスからでも呼べます 。プロパティに依存しないし スコープもpublicのままに しておきましょう。
急に必要になったとき、様々なクラスから再利用できていい感じですね。

staticメソッドの再利用
<?php
class Test2
{
    echo Test::message('いつでもどこでもほしい処理が呼べるね'); // [正常]プロパティの値は[いつでもどこでもほしい処理が呼べるね]です。
    echo Test::message('これはエラー', Test::MESSAGE_TYPE_ERROR); // [エラー]プロパティの値は[これはエラー]です。
}

よかったよかった。

…そんな訳はありません。

誤った思考の流れ

  1. 修正・追加実装しようとする処理や機能に関連するオブジェクトが存在しており、関連して(必要な情報を保持していたり出力構造を持って)いる
  2. そのオブジェクトの状態(=プロパティ)が信頼できないため、そのオブジェクトの既存処理およびプロパティを活用することが難しい、あるいはそもそも使えないケースがある
  3. 「オブジェクトの状態に依存せず、与えた引数や不変の定数に依ってのみ行われる処理は安定している」と考え、staticメソッドを配備してしまう
  4. 配備したstaticメソッドは不変で安定性が高い(と考える)ため、 いつでも何度でも呼ばれても問題ない(と考える)。このため、どのスコープからでも使えるようにしばしばpublicで配備してしまう
  5. 以降、ほかクラスでstaticメソッドをcallして値を得て行使するのが常態化する

なぜこう考えてしまったか

オブジェクトの状態(=プロパティ)が信頼できない

ほぼ結論なんですがとにかくこれがそもそもの原因です。

こういったクラスはしばしば、

  • 不変(immutable)を意識した実装がなされておらず、プロパティが何か、あるいは定義・未定義なのかすら信用できない(生焼けオブジェクトとしばしば呼称される状態)
    • プロパティのアクセス修飾子が適切でない。publicは論外
    • 破壊的setterメソッドが定義されている 等が起きている
  • (手続き型のように)クラスが関係する機能の処理置き場と化している

といった実態をもちます。これらの状態を是正するには以下が考えられます。

  1. インスタンス化した段階でインスタンスを完全な状態にする(完全コンストラクタ)
  2. 生成以後のインスタンスを不変にする

この例において、1.は「対象とするプロパティがnullでないこと=プロパティが想定する形式で正しく定義されていること」と言い換えてみると、それは「インスタンスを生成した(=コンストラクタ処理を終えた)段階で全てのプロパティに想定の値を配備する」ことで対処できそうです。
2.については、1.を満たしつつsetterメソッドを配備しないこと、プロパティのアクセス修飾子を適切に設定することで対処可能です。

不変を維持し、状態が信頼できるインスタンスを生成できるクラス
<?php
declare(strict_types=1); // 厳密な型付け

class Test
{
    const MESSAGE_TYPE_SUCCESS = '正常';
    const MESSAGE_TYPE_ERROR   = 'エラー';
    private ?string $property = null; // 1.のための型宣言,2.のためのprivate化

    public function __construct(string $privateProperty) // 1.のため、全てのメソッドの引数・返り値を型宣言
    {
        // 1.のため全てのプロパティをコンストラクタで配備する
        $this->property = $property;
    }

    /**
     * :
     * 2.のため、後天的にプロパティを変更してしまうようなsetterを配備しない
     * :
     */

    public function message(string $type = self::MESSAGE_TYPE_SUCCESS): string
    {
        return '[' . $type . ']プロパティの値は[' . $this->property . ']です。' . "\n";
    }
}

// ----- 以降、このクラスを扱おうとするスコープ -----

$test = new Test('property');
echo $test->message(); // [正常]プロパティの値は[property]です。
echo $test->message(Test::MESSAGE_TYPE_SUCCESS); // [正常]プロパティの値は[property]です。

また、どうしてもsetterに相当する処理を配備しつつ不変を維持する場合、自身のインスタンスを返させることで、そのインスタンス自体の不変を維持することもできます。

不変を維持するsetterを配備する例
<?php
declare(strict_types=1);

class Test
{
    const MESSAGE_TYPE_SUCCESS = '正常';
    const MESSAGE_TYPE_ERROR   = 'エラー';
    private ?string $property1 = null;
    private ?string $property2 = null;

    public function __construct(string $property1, string $property2)
    {
        $this->property1 = $property1;
        $this->property2 = $property2;
    }

    /**
     * プロパティを変更せずインスタンス自身を返させることで不変を維持するsetter
     */
    public function setProperty1(string $property1): Test
    {
        return new self($property1, $this->property2);
    }
    
    public function setProperty2(string $property2): Test
    {
        return new self($this->property1, $property2);
    }

    public function property1Message(string $type = self::MESSAGE_TYPE_SUCCESS): string
    {
        return '[' . $type . ']プロパティの値は[' . $this->property1 . ']です。' . "\n";
    }
    
    public function property2Message(string $type = self::MESSAGE_TYPE_SUCCESS): string
    {
        return '[' . $type . ']プロパティの値は[' . $this->property2 . ']です。' . "\n";
    }
}

// ----- 以降、このクラスを扱おうとするスコープ -----

$test = new Test('property1', 'property2');
echo $test->property1Message(); // [正常系]プロパティの値は[property1]です。
echo $test->property2Message(); // [正常系]プロパティの値は[property2]です。

$tunedTest = $test->setProperty1('tunedProperty1'); // プロパティ1に値をセットするようなリクエストをするが、$testを変更しない
echo $test->property1Message();      // [正常系]プロパティの値は[property1]です。
echo $test->property2Message();      // [正常系]プロパティの値は[property2]です。
echo $tunedTest->property1Message(); // [正常系]プロパティの値は[tunedProperty1]です。
echo $tunedTest->property2Message(); // [正常系]プロパティの値は[property2]です。

なぜそこまでstatic(特にpublic static)メソッドを避けるのか

public staticメソッドを使うと何が悪いのでしょう?

密結合低凝集を引き起こすためです。

ことビジネスロジックでstaticメソッドを配備し、また広いスコープでそれを運用するのは、 どこからでも呼べてしまい広いスコープに影響する(密結合) 状態になっており、またしばしば、それで得られた結果を呼び出し元が使ってしまうことで 関心事として集まるべきクラスに集まっていない(低凝集) 状態になりえます。

public staticメソッドを推奨し運用を推し進めることは、これらそれぞれの要素を悪化させ、関わる実装、ひいてはプロダクト自体の保守性はどんどん悪くなります。

「尋ねるな、命じよ」の言、そしてカプセル化

今回の例のようなstaticメソッドは(特にスコープがコードの技術レイヤにとどまる場合に限れば)クラスの状態を意図的に扱わない都合上、クエリ4の性質であることが殆どであり、ほぼ返り値を返します。

本来関心ごとを持つ必要のない呼び出しスコープが、関心ごとを集約しているはずのクラスのメソッドから返り値を受け取った結果、処理に使ったり、後続の処理系に渡したりしてしまう。

これは 尋ねるな、命じよ(Tell, Don't Ask) の言に反しており、「値をどう扱うべきなのか」という知識が呼び出し側に要求されることで、本来必要ないはずのロジックの実装を呼び出し側に引き起こしてしまい、低凝集・密結合を加速させます。

この考え方に基づくと、状態を変更してしまうsetterメソッドだけでなく、 プロパティを返すgetterメソッド5も、知識を漏出する性質をもつため、乱用は推奨されないと考えることができます。

またpublicな定数も、ほかクラスから参考されることによって知識の漏出を引き起こすといえます。先の例では(ちょっと無理やりですが)message()系メソッドの$type引数の扱いがそうですね。

これらに基づいて、先のクラスを改善してみます。

「正常」「エラー」をそのインスタンスの状態=プロパティの一環とし、またこのプロパティもコンストラクタ完全を意識し、尋ねず、命じる形式を試みます。6

知識の漏れ出しを防いだクラス
<?php
declare(strict_types=1);

class Test
{
    private const MESSAGE_TYPE_SUCCESS = '正常'; // 定数をprivate化
    private const MESSAGE_TYPE_ERROR   = 'エラー'; // 定数をprivate化
    private bool $success = true; // メッセージのタイプを表現できるプロパティを追加
    private string $property;

    public function __construct(string $property)
    {
        // 無効な文字列の場合は「エラー」扱いにする
        if (strlen($property) <= 0) {
            $this->success = false;
        }
        $this->property = $property;
    }

    /**
     * メッセージの出力
     * @return void
     */
    public function echoMessage(): void
    {
        echo $this->buildMessage();
    }
    
    /**
     * メッセージを返す
     * @return string 出力するメッセージ
     */
    private function buildMessage(): string
    {
        if ($this->success) {
            return $this->buildSuccessMessage();
        }
        return $this->buildErrorMessage();
    }
    
    /**
     * 正常メッセージを返す
     * @return string 正常時メッセージ
     */
    private function buildSuccessMessage(): string
    {
        return $this->messagePrefix() . 'プロパティの値は[' . $this->property . ']です。' . "\n";
    }
    
    /**
     * エラーメッセージを返す
     * @return string エラー時メッセージ
     */
    private function buildErrorMessage(): string
    {
        return $this->messagePrefix() . 'プロパティが不正です。' . "\n";
    }
    
    /**
     * メッセージ接頭辞を返す
     * @return string メッセージの接頭辞
     */
    private function messagePrefix(): string
    {
        return '[' . $this->typeKeyword() . ']';   
    }
    
    /**
     * メッセージのタイプを表現する文字列を返す
     * @return string メッセージのタイプを表現するキーワード
     */
    private function typeKeyword(): string
    {
        return $this->success ? self::MESSAGE_TYPE_SUCCESS : self::MESSAGE_TYPE_ERROR ;
    }
}

// ----- 以降、このクラスを扱おうとするスコープ -----

// 想定する「正常」ケース
$test = new Test('property');
$test->echoMessage(); // [正常]プロパティの値は[property]です。

// 想定する「エラー」ケース
$test = new Test(''); // [エラー]プロパティが不正です。
$test->echoMessage(); 

こうして改善されたクラスでは、

  • インスタンスの状態が途中で変更され得ない
    • もし異なるメッセージを出力したい場合はインスタンスを生成し直すほかない
  • このクラスを扱おうとするスコープにおいて処理実態を意識したり、引数でコントロールしようとする必要がない
    • インスタンスを生成→echoMessage()するだけで必要な結果は完全に得られる
    • 要求される知識は「入力する文字列の仕様」程度で、処理途中〜処理以降の知識を必要以上に要求しない
  • 大部分が(例の場合は全てが)動的メソッドで構成される

といった特徴があります。

こうして、当初よりも 動的(dynamic)メソッドにより低凝集・密結合を防ぎつつ、呼び出し側に知識を要求しないクラスに改善できました。

いわゆるカプセル化がなされた、ともいえるかと思います。

static(public static)メソッドを使うべきシーン

当然ながら、staticメソッドそれ自体が悪ではないですし、確実に必要になるシーンがありますので、徹底して排除する必要があるわけではありません。

以下などに相当する場合にだけ使う、という程度に留まるでしょうか。

  • 各種ビジネスロジックに関わらない基盤処理を扱うような処理系。アーキテクチャ最外殻の具体性の高いクラス(=横断的関心事)
    • 永続化データの運用クラス(PHPの場合はPDOクラスに依るコネクションの生成・管理)
    • メール発信処理 など
  • 意図した形式のインスタンスを返すメソッド(=ファクトリメソッド)
  • 標準関数に肉薄するような、汎用的でかつ極めて具体性の高い処理
    • 既存の標準関数を強化するような汎用処理 など

おわり

こういった生っぽい思考と、それを解決する原則・法則を拙くでも辿って紐解くことでこそ、自分以外のどなたかに少しでも刺さるようなことがあればなーと思います。(おてやわらかに)

参考したもの

  1. タイトルの思考はここが大前提になっているためご注意ください。終盤に述べている通り、staticメソッドは適切に運用するタイミング・処理系は確実に存在します

  2. 個人的な都合上、PHP 8.1.9で可能な記法で表記します。また、こちらも個人的な思想として、getterに相当するメソッドにget接頭辞は付けません。

  3. このサンプルコードではごくシンプルなのでこういった判断にはなり得ないと思いますが、実際には「対象に取るインスタンスの寿命が長く運用箇所が広い(特にpublicプロパティを有している場合に致命的)」「クエリとコマンドの役割が混合しているメソッドが存在しており、どのメソッドに依ってプロパティが変わるのかすら判別が難しい」「プロパティの値に厳密性を要求されており、その処理を差し込もうとしたときに適正な状態なのか判別できずリスクがある」といった理由を抱えていることが多いです

  4. この記事で言うコマンド、クエリはいわゆるコマンドクエリ分離原則(CQS)でいうそれです

  5. getterが要所で必要になる場合は少なくないと思います。あくまで問題なのは乱用しすぎることで、「getterは駆逐すべき」というものではないと考えています

  6. 紛らわしいですが、「エラー」「正常」といういずれも正常系のステータスを扱う、とご理解下さい。

  7. この記事の思考・思想はこの本の5.1に記載されていることとほぼ同じだったりします

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?