这个问题来源于在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
中的内容快速定位到相同类型的日志,一方面能够省去了占位符,可读性也没问题。框架
分歧有两点:工具
下面分别剖析该这两个问题。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规范就意味着要遵循它的参数形式?
这个问题又能够分为两点:
{$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那样直接改变接口定义的参数类型强行实现重载。