内容简介:这个问题来源于在Seaslog开发组的一次讨论。SeasLog是一个声称遵循PSR-3规范的PHP日志工具,它是一个PHP扩展(采用C编写)。问题的核心在于能否能否在实际上改变了接口的定义之后还说自己遵循该接口。PSR-3提供了9个方法,这里仅以一个为例来说明。
这个问题来源于在Seaslog开发组的一次讨论。
SeasLog是一个声称遵循PSR-3规范的 PHP 日志工具,它是一个PHP扩展(采用C编写)。问题的核心在于能否能否在实际上改变了接口的定义之后还说自己遵循该接口。
PSR-3提供了9个方法,这里仅以一个为例来说明。
<?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的实现方式,即(简化版本)
<?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
中的内容快速定位到相同类型的日志,一方面可以省去了占位符,可读性也没问题。
分歧有两点:
- 接口并没有指定前两个参数的类型
- 接口没有也不应该指定这个方法该如何实现
下面分别剖析该这两个问题。
是否需要指定参数类型
对于第一个参数 $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需要以占位符
{$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那样直接改变接口定义的参数类型强行实现重载。
以上所述就是小编给大家介绍的《关于接口的实现和扩展的思考》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 在 Laravel 项目中集成 Swagger 扩展包为 Laravel API 生成接口文档并对接口进行测试
- 面试官:Spring 框架内置了哪些可扩展接口,咱们一个一个聊
- [ PHP 内核与扩展开发系列] 类与面向对象:接口实现与类的继承
- Bee V1.5.0 发布,增强分页功能和命名转换提供接口扩展
- 在 Laravel 中集成 API 文档生成器扩展包为 Dingo API 接口生成文档
- 使用 Dingo API 扩展包快速构建 Laravel RESTful API(二) —— 编写第一个 API 接口
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。