Software engineering from east direction

六本木一丁目で働くソフトウェアエンジニアのブログ

PSR-3とCakePHPから見るNull Object Pattern

TL;DR

背景

CakePHP 4.0 の変更点を追っている際に、次のような記述があった。

Cake\Database\Connection::setLogger() no longer accepts null to disable logging. Instead pass an instance of Psr\Log\NullLogger to disable logging.

https://book.cakephp.org/4/en/appendices/4-0-migration-guide.html

これまで、nullを渡してロギングを無効にする方法をとっていたが、Psr\Log\NullLoggerを渡して無効化するように案内されていた。

NullXxxと見ると、NullObjectパターンが想起されたので、Psr\Log\NullLoggerを入り口として、PSR-3Null Object Pattternの説明を試みる。

PSR

そもそも、PSRとは、PHP Standards Recommendations1の略で、PHP-FIG2というコミュニティ団体による、コミュニティにおいて(ある程度)影響が大きい標準化勧告である。PHP開発に馴染みのある方であれば、普段良く使用しているフレームワークやライブラリが、この勧告によって提示されているInterfaceをサポートしていたりする。AcceptされたものからDraft段階のものを含めてNo.19まで提案されており、冒頭に上げたCakePHPもPSR-15やPSR-16などをサポートしているフレームワークの一つです。

PSR-3 Logger Interface

その中の、No.3であるPSR-33は、すでにAcceptされているPSRで、Logger Interfaceを規定する。つまり、ロガーライブラリの標準となる共通インターフェースを定めようというものになる。

www.php-fig.org

The main goal is to allow libraries to receive a Psr\Log\LoggerInterface object and write logs to it in a simple and universal way.

基本的なインターフェースとして、RFC54254で定義された8つのレベルにログを書き込むためのメソッドを公開する。そして、9つめのメソッドとして、ログレベルを第一引数として受け入れるlogを公開する。

<?php
namespace Psr\Log;

interface LoggerInterface
{
    public function emergency($message, array $context = array());
    public function alert($message, array $context = array());
    public function critical($message, array $context = array());
    public function error($message, array $context = array());
    public function warning($message, array $context = array());
    public function notice($message, array $context = array());
    public function info($message, array $context = array());
    public function debug($message, array $context = array());
    /**
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array());
}

https://github.com/php-fig/log/blob/4aa23b5e211a712047847857e5a3c330a263feea/Psr/Log/LoggerInterface.php#L3

logメソッドのPHPDoc5を見ると明らかですが、ログレベルがRFC5425[^4]で規定されたレベル以外の場合は、\Psr\Log\InvalidArgumentExceptionを返すことが指定されている。

Psr\Log\NullLogger

この、PSR-3[^3]のLogger Interfaceの「1.4 Helper classes and interfaces[^5]」にて、Psr\Log\NullLoggerがInterfaceと一緒に提供されている点について説明されている。

The Psr\Log\NullLogger is provided together with the interface. It MAY be used by users of the interface to provide a fall-back “black hole” implementation if no logger is given to them. However, conditional logging may be a better approach if context data creation is expensive.

フォールバックの実装を提供するために使用することができるblack hole実装となっています。

<?php

namespace Psr\Log;

/**
 * This Logger can be used to avoid conditional log calls.
 *
 * Logging should always be optional, and if no logger is provided to your
 * library creating a NullLogger instance to have something to throw logs at
 * is a good way to avoid littering your code with `if ($this->logger) { }`
 * blocks.
 */
class NullLogger extends AbstractLogger
{
    /**
     * Logs with an arbitrary level.
     *
     * @param mixed  $level
     * @param string $message
     * @param array  $context
     *
     * @return void
     *
     * @throws \Psr\Log\InvalidArgumentException
     */
    public function log($level, $message, array $context = array())
    {
        // noop
    }
}

github.com

継承しているPsr\Log\AbstractLoggerにて、8つのログレベルの公開メソッドが実装されているため、LoggerInterfaceを満たすものとなっています。

フレームワークにおける使用例

このPsr\Log\NullLoggerの使用例をCakePHPの内部実装で見てみます。PSR-3の\Psr\Log\LoggerInterfaceへの依存へ変更しているPRから、見ていきます。

-    public function setLogger(?QueryLogger $logger)
+    public function setLogger(LoggerInterface $logger)

github.com

CakePHP3.x以前では、ロギングを無効にするために、Cake\Database\Connection::setLogger()nullを渡す方法をとっていました。

        $connection = ConnectionManager::get('test');
        $connection->setLogger(null); // 無効

この場合、以降ロガーを呼び出す際に、if ($this->_logger === null)といった条件分岐などを使用してチェックするようなコードを用意することになります。

それに対して、NullLoggerを利用する場合は、そのようなチェックをせずとも、同一のインターフェースを利用することができます。

        $connection = ConnectionManager::get('test');
        $connection->setLogger(new NullLogger()); // 無効

このようなメリットを享受できるコード設計パターンは、Null Object Patternというパターンの特徴として見ることができます。

Null Object Pattern

Null Object Pattern6とは、オブジェクト指向言語おけるパターンで、参照される値がないか、定義されたニュートラル(null)動作を持つオブジェクトを指します。「プログラムデザインのためのパターン言語―Pattern Languages of Program Design選集7」や、「リファクタリング 既存のコードを安全に改善する(第2版)8」にて、本として初めて紹介されたパターンとなります。(※ 「リファクタリング 既存のコードを安全に改善する(第2版)[^8]」では、Special Case Patternとも表現されています。)

これは、GoF (Gang of Four)の23個のデザイン・パターンではありませんが、現代のプログラミングの現場において広く知られる「デザイン・パターン」といえます。

先程のCakePHPの例では、次のようなクラス関係を表現しています。

f:id:khigashigashi:20200102191704p:plain

デザイン・パターンは、オブジェクト指向ソフトウェアを設計する際の経験を記録、カタログ化したものです。繰り返し現れる構造をパターンとしてまとめたものです。「問題」・「解決」・「コンテクスト」の大きく3つの要素をそのパターンから見出すことができます。

まとめ

PSR-3の標準的なインターフェースとともに提供されているクラスから、NullObjectというパターンを紹介しました。


  1. PHP Standards Recommendations (https://www.php-fig.org/psr/)

  2. PHP-FIG (https://www.php-fig.org/)

  3. PSR-3 (https://www.php-fig.org/psr/psr-3/)

  4. RFC5425 (https://tools.ietf.org/html/rfc5424)

  5. phpDocumentor (https://www.phpdoc.org/)

  6. Null Object Pattern (https://en.m.wikipedia.org/wiki/Null_object_pattern)

  7. プログラムデザインのためのパターン言語―Pattern Languages of Program Design選集 (https://www.amazon.co.jp/dp/4797314397)

  8. リファクタリング 既存のコードを安全に改善する(第2版) (https://www.amazon.co.jp/dp/B0827R4BDW)