关于接口的实现和扩展的思考

栏目: PHP · 发布时间: 5年前

内容简介:这个问题来源于在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 中的内容快速定位到相同类型的日志,一方面可以省去了占位符,可读性也没问题。

分歧有两点:

  1. 接口并没有指定前两个参数的类型
  2. 接口没有也不应该指定这个方法该如何实现

下面分别剖析该这两个问题。

是否需要指定参数类型

对于第一个参数 $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那样直接改变接口定义的参数类型强行实现重载。


以上所述就是小编给大家介绍的《关于接口的实现和扩展的思考》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

淘宝技术这十年

淘宝技术这十年

子柳 / 电子工业出版社 / 2013-5 / 45.00元

《淘宝技术这十年》内容简介:任何网站的发展都不是一蹴而就的。它在发展过程中会遇到各种各样的问题和业务带来的压力。正是这些问题和压力推动着技术的进步和发展,而技术的发展反过来又会促进业务的更大提升。如今淘宝网的流量排名已是全球前15名、国内前3名,其系统服务器也从一台发展到万台以上。 《淘宝技术这十年》从工程师的角度讲述淘宝这个超大规模互联网系统的成长历程,及其所有主动和被动的技术变革的前因后......一起来看看 《淘宝技术这十年》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具