Skip to content
On this page

基于装饰器的日志写入器


基于装饰器的日志写入器

装饰器模式:

不基于子类实现的方式,更灵活的给对象动态增加额外的功能。

丰富对象功能

在开闭原则中,明确了在为系统增添功能的过程中,应当避免修改已有的对象来实现。 这是因为对于已有的对象,系统中可能已经存在很多实用到它的对象,如果我们盲目修改,会导致这些对象的工作都会受到影响。 由于这些对象本身处于我们修改的范围之外,对其修改不但增加了我们的工作量,还会因为难以统计它们而造成遗漏。

即使我们能够修改所有的这些相关对象,那又会冒出新的问题,就是这些修改又会影响到引用这些对象的其他对象。 任何一个对已有对象的修改,都可能造成牵一发动全身的系统性变动。 所以直接修改对象来实现新的功能或修改原有的功能,是尽可能不要做的,因为这简直就是在制造一个系统的黑洞。

程序黑洞

那么在遵循开闭原则的情况下,我们可以通过两种维度为对象增加新的功能:

  • 垂直扩展:利用对象的继承机制,获得父类的所有方法和特性,进而通过子类中增添行为来实现扩展需要。
  • 水平扩展:将对象包裹到另一个对象中,通过在这个新对象中增添行为来实现扩展需要。

对比两种实现扩展的方法,其实各有优劣。 通过继承来实现新功能,由于子类可以接触更多父类的内容,所以能够改变的行为相对更多。 而由于大多数语言是单继承形式的,所以通过继承来扩展行为的方式又缺乏一定的灵活性。

通过对象包裹的形式来实现扩展,可以避免通过继承来实现的过程中子类定义过多等问题。 这种水平扩展的实现思想,就是装饰器模式。 而包裹原有对象的新对象,就是装饰器对象。

装饰器模式浅析

装饰器模式是一种能够很好指导对象功能扩展的设计模式。 装饰器模式的初衷,就是在对象使用者不需要改变使用方法的情况下,改变或扩展对象的行为。

在装饰器改变对象行为的过程中,我们应该保持装饰器与原有对象拥有一致的接口。 这样能够保证装饰器能够直接替代原有对象进行工作,而不需要使用者再进行适配。 这种做法保证了装饰器的嵌入和移除更加方便。

用我们装修的过程来类比装饰器模式是再合适不过的了。

装饰

我们可以通过搭配不同风格的地板、布艺来改变我们房屋的展示效果,这就是对原有对象改进的过程。 而床依然是躺在睡觉的,窗帘依然是拉开遮阳的,使用对象的方式没有进行改变。 这就是类比装修过程的装饰器模式。

在装饰器模式下,主要有四种角色:

  • Component : 主体对象抽象
  • ConcreteComponent : 主体对象实现类
  • Decorator : 装饰器抽象
  • ConcreteDecorator : 装饰器实现

我们通常让装饰器实现原有对象的接口,这样就能帮助我们保证装饰器与原有对象保持接口一致。 同时,这也方便我们进行面向接口的编程。

装饰器模式 UML 图

由于没有继承关系的束缚,通过装饰器来扩展对象的功能更加方便。 当然,由于硬性的束缚少,那么在使用装饰器思想进行设计时,我们就更新需要把握好对象之间的关系,避免跑偏。 总而言之,在使用装饰器模式的过程中,永远记住装饰器永远只是饰品,它只是用来丰富对象行为、功能的,切不要喧宾夺主了。

扩展 Monolog 功能

在 Laravel 的日志模块中,装饰器模式得到了很好的体现。

Laravel 并不是自己实现了日志的功能,而是借助于 Monolog 这个功能丰富的日志库。

在引入 Monolog 实现日志写入的过程中,Laravel 还希望在写入日志的同时触发一个日志写入事件。 这就是一个典型的为日志操作对象增加触发事件功能的过程,也是装饰器模式很好的用武之地。

Laravel 日志写入器 UML 图

Laravel 通过 Illuminate\Log\Writer 来为 Monolog 的 Monolog\Logger 对象增加功能。 两个对象都实现了 Psr\Log\LoggerInterface 接口来保证对日志写入接口调用的一致性。

由于接口是一致的,我们写入日志的过程中,两个对象可以互相切换,这也就确保了装饰器对象的无缝对接。

Illuminate\Log\Writer 中,我们可以找到被包裹的 Monolog 日志对象:

namespace Illuminate\Log;

class Writer implements LogContract, PsrLoggerInterface
{
    /**
     * Monolog 日志对象
     */
    protected $monolog;

    /**
     * 事件分发器
     */
    protected $dispatcher;

    /**
     * 记录日志
     */
    public function log($level, $message, array $context = [])
    {
        $this->writeLog($level, $message, $context);
    }

    /**
     * 写入日志
     */
    protected function writeLog($level, $message, $context)
    {
        // 写入日之前触发日志事件
        $this->fireLogEvent($level, $message = $this->formatMessage($message), $context);

        $this->monolog->{$level}($message, $context);
    }
}

在包裹了 Monolog 对象之后,就可以在日志写入时增加触发事件了。

小结

通过装饰器模式来实现对象功能的扩展,相比较于对象继承来说,更加的灵活。 同时,由于装饰器与对象是水平结构的,避免了扩展时逐渐增加层次导致系统难以维护的问题。 装饰器和原有对象的关联、耦合要比继承少很多,所以很容易实现与原有对象的热插拔。