關於接口的實現和擴展的思考

這個問題來源於在Seaslog開發組的一次討論。php

SeasLog是一個聲稱遵循PSR-3規範的PHP日誌工具,它是一個PHP擴展(採用C編寫)。問題的核心在於可否可否在實際上改變了接口的定義以後還說本身遵循該接口。html

PSR-3提供了9個方法,這裏僅以一個爲例來講明。java

<?php
interface LoggerInterface { 
	public function log($level, $message, array $context = array()); 
} 
複製代碼

很是簡單的接口定義,同時也給出了一個參考實現,這裏簡單描述一下。程序員

<?php

include __DIR__ . '/LoggerInterface.php';

class Logger implements LoggerInterface {
    public function log($level, $message, array $context = array()) {
        error_log($this->interpolate($message, $context), 3, '/tmp/a.log');
    }   
    
    private function interpolate($message, array $context = array()) {
        // build a replacement array with braces around the context keys
        $replace = array();
        foreach ($context as $key => $val) {
            // check that the value can be casted to string
            if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
                $replace['{' . $key . '}'] = $val;
            }
        }
    
        // interpolate replacement values into the message and return
        return strtr($message, $replace);
    }
}
複製代碼

從上面的代碼反推,使用該日誌類的過程應該是這樣的編程

include_once __DIR__ . '/Logger.php';

$logger = new Logger();
$logger->log('debug', 'I am a {job}', ['job' => 'programmer']);

// 輸出
// I am a programmer
複製代碼

也就是說,若是想讓第三個參數$context生效,必須在第二個參數中加入佔位符,顯然這不是一個友好的方式。但其實初衷很容易理解,就是要讓$message成爲一行完整的sentence。可是在實際使用中,咱們其實更傾向於Monolog的實現方式,即(簡化版本)json

<?php

include __DIR__ . '/LoggerInterface.php';

class Monolog implements LoggerInterface {
    public function log($level, $message, array $context = array()) {
        error_log($this->interpolate($message, $context) . "\n", 3, './a.log');
    }

    private function interpolate($message, array $context = array()) {
       return $message . '|' . (string)$context;
    }
}
複製代碼

這樣的話,用起來就是這樣的:數組

<?php

include_once __DIR__ . '/Monolog.php';

$logger = new Monolog();
$logger->log('debug', 'job description', ['job' => 'programmer']);

// 輸出
// job description|{"job":"programmer"}
複製代碼

這樣作,一方面能夠經過$message中的內容快速定位到相同類型的日誌,一方面能夠省去了佔位符,可讀性也沒問題。框架

分歧有兩點:工具

  1. 接口並無指定前兩個參數的類型
  2. 接口沒有也不該該指定這個方法該如何實現

下面分別剖析該這兩個問題。ui

是否須要指定參數類型

對於第一個參數$level,沒有任何問題,由於它表明的是日誌的嚴重等級,PSR-3爲其定義了8個等級,這裏再也不贅述。

第二個參數$message,是否能夠是數組?接口並無指定,這就給了接口實現者一些可發揮的空間。

好比我喜歡讓$message是數組,這樣我就不須要再思考本來接口設計者認爲的$message的做用。最終的實現可能會是這樣:

<?php

include __DIR__ . '/LoggerInterface.php';

class SeasLog implements LoggerInterface {
    public function log($level, $message, array $context = array()) {
        if (is_array($message)) {
            error_log(json_encode($message) . "\n", 3, './a.log');
        } else {
            error_log($this->interpolate($message, $context) . "\n", 3, './a.log');
        }
    }

    private function interpolate($message, array $context = array()) {
        // build a replacement array with braces around the context keys
        $replace = array();
        foreach ($context as $key => $val) {
            // check that the value can be casted to string
            if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
                $replace['{' . $key . '}'] = $val;
            }
        }

        // interpolate replacement values into the message and return
        return strtr($message, $replace);
    }
}
複製代碼

這樣就變相支持了上面提到的兩種調用方式。但這樣混亂的支持真的好嗎?

因此問題就在於,是否遵循PSR-3規範就意味着要遵循它的參數形式?

這個問題又能夠分爲兩點:

  1. 第一個參數和接口參考實現中同樣是字符串
  2. 第二個參數是數組,它的key須要以佔位符{$key}的形式出如今$message

若是這兩點都是確定的那麼其實PSR-3本質上綁定了一種實現。是否能夠說上面的Monolog類就不遵循PSR-3呢?

是否要綁定具體實現

從我對面向接口編程思想的理解來講,使用接口就是爲了可替換,好比今天我使用PeasLog類不爽想換成SeasLog,那若是兩個具體實現雖然都implements了同一個接口,但就像上面提到的,兩個類接收的參數其實並不一樣,也就沒法作到直接替換。

而真實的Monolog那樣的實現無疑是很是靈活的,實際上它定義了一個Processor的概念用於解決這個問題。默認狀況下它的作法就是return $message . '|' . (string)$context;,但咱們能夠經過自定義Processor來改變它的默認行爲。好比它內置的PsrLogMessageProcessor就是爲了兼容PSR-3而實現的,同時也能夠隨意實現自定義Processor來知足個性化需求。

在思考這個問題的過程當中我查閱了一些資料,其中深刻理解abstract class和interface中的理解和Monolog的實現一模一樣,或許能說明這是那些偉大的程序員們的共識吧。

套用原文中關於Door類實現的討論,這裏討論PlaceholderLogger,首先它是一個(is a)Logger,也須要實現接口定義的功能記錄日誌,但如何記錄並非問題的核心,能夠放在另外一個接口中定義。和文中討論問題不一樣的是,Monolog中並無所謂的Abstract class,但我認爲這並不影響結論。

Monolog裏的Processor很有些像Slim框架中的Middleware,用於預處理輸入,將結果繼續交給下游處理,但其實下游根本感受不到它的存在。

結論

本文並無任何結論。只是思考一下接口和實現到底應該是什麼樣的關係。只能說我本身喜歡Monolog這樣,提供默認實現,又暴露接口讓用戶能夠自定義方法控制其實現方式的作法,而不喜歡SeasLog那樣直接改變接口定義的參數類型強行實現重載。

相關文章
相關標籤/搜索