使用转发装饰器实现模块化架构

tech2022-08-25  113

This article was peer reviewed by Younes Rafie and Christopher Pitt. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

这篇文章由Younes Rafie和Christopher Pitt进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!



As your web application becomes larger, you certainly start to think more about designing a flexible, modular architecture which is meant to allow for a high amount of extensibility. There are lots of ways to implement such architecture, and all of them circle around the fundamental principles: separation of concerns, self-sufficiency, composability of the parts of an app.

随着您的Web应用程序变得越来越大,您肯定会开始更多地考虑设计一种灵活的模块化体系结构,该体系结构旨在实现高度的可扩展性。 有很多方法可以实现这种体系结构,所有这些方法都围绕基本原理:关注点分离,自给自足,应用程序各部分的可组合性 。

There is one approach which is rarely seen in PHP software but can be effectively implemented — it involves using native inheritance to provide manageable patching of the software code; we call it the Forwarding Decorator.

有一种方法在PHP软件中很少见,但可以有效地实现–它涉及到使用本机继承来提供软件代码的可管理补丁; 我们称之为转发装饰器。

概念介绍 (Introduction to the Concept)

In this article, we are going to observe the implementation of the Forwarding Decorator approach and its pros/cons. You can see the working demo application at this GitHub repository. Also, we’ll compare this approach to other well-known ones such as hooks, and code patching.

在本文中,我们将观察转发转发器方法的实现及其优点/缺点。 您可以在此GitHub存储库中看到正在运行的演示应用程序。 另外,我们还将把这种方法与其他众所周知的方法(例如钩子和代码修补)进行比较。

The main idea is to treat each class as a service and modify that service by extending it and reversing the inheritance chain through code compilation. If we build the system around that idea, any module will be able to contain special classes (they will be marked somehow to separate from the usual classes), which can inherit from any other class and will be used anywhere instead of the original object.

主要思想是将每个类都视为服务,并通过扩展代码和通过代码编译反转继承链来修改该服务。 如果我们围绕该思想构建系统,则任何模块都将能够包含特殊类(它们将以某种方式标记为与普通类分开),这些特殊类可以从任何其他类继承而来,并将在任何地方使用,而不是原始对象。

That’s why it is called Forwarding decorators: they wrap around the original implementation and forward the modified variant to the forefront to be used instead.

这就是为什么将其称为“ 转发装饰器”:它们包装原始实现,并将修改后的变体转发到要使用的最前沿。

The advantages of such an approach are obvious:

这种方法的优势显而易见:

modules can extend almost any part of the system, any class, any method; you don’t have to plan extension points in advance.

模块几乎可以扩展系统的任何部分,任何类,任何方法; 您不必提前计划扩展点。 multiple modules can modify a single subsystem simultaneously.

多个模块可以同时修改单个子系统。 subsystems are loosely coupled and can be upgraded separately.

子系统是松散耦合的,可以单独升级。 extension system is based on the familiar inheritance approach.

扩展系统基于熟悉的继承方法。

you can control extensibility by making private methods and final classes.

您可以通过制作private方法和final类来控制可扩展性。

With great power comes great responsibility, so the drawbacks are:

强大的力量伴随着巨大的责任,因此缺点如下:

you would have to implement some sort of compiler system (more about that later)

您将必须实现某种编译器系统(稍后会详细介绍)

module developers have to comply with the public interface of the subsystems and not violate the Liskov substitution principle; otherwise other modules will break the system.

模块开发者必须遵守子系统的公共接口,不得违反Liskov替代原则 ; 否则其他模块将破坏系统。

you will have to be extremely cautious when modifying the public interface of the subsystems. The existing modules will certainly break and have to be adapted to the changes.

在修改子系统的公共接口时,您将非常谨慎。 现有的模块肯定会中断,必须适应这些更改。 extra compiler complicates the debugging process: you can no longer run XDebug on the original code, any code change should be followed by running the compiler (although that can be mitigated, even the XDebug problem)

额外的编译器使调试过程复杂化:您不能再对原始代码运行XDebug,应该在运行编译器后进行任何代码更改(尽管可以缓解,甚至可以缓解XDebug问题)

如何使用该系统? (How Can This System Be Used?)

The example would be like this:

该示例将如下所示:

class Foo { public function bar() { echo 'baz'; } } namespace Module1; /** * This is the modifier class and it is marked by DecoratorInterface */ class ModifiedFoo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } // ... somewhere in the app code $object = new Foo(); $object->bar(); // will echo 'baz modified'

How can that be possible?

那怎么可能呢?

Achieving this would involve some magic. We have to preprocess this code and compile some intermediate classes with the reversed inheritance graph, so the original class would extend the module decorator like this:

实现这一目标将涉及一些魔术。 我们必须对该代码进行预处理,并使用反向继承图编译一些中间类,因此原始类将像以下那样扩展模块装饰器:

// empty copy of the original class, which will be used to instantiate new objects class Foo extends \Module1\ModifiedFoo { // move the implementation from here to FooOriginal } namespace Module1; // Here we make a special class to extend the other class with the original code abstract class ModifiedFoo extends \FooOriginal implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } // new parent class with the original code, every inheritance chain would start from such file class FooOriginal { public function bar() { echo 'baz'; } }

The software has to implement the compiler to build the intermediate classes and the class autoloader, which will load them instead of the original ones.

该软件必须实现编译器以构建中间类和类自动加载器,该类将加载它们而不是原始的。

Essentially, the compiler would take a list of all classes of the system and for each individual non-decorator class find all children that implement DecoratorInterface. It will create a decorator graph, make sure it is acyclic, sort decorators according to the priority algorithm (more on that later), and build intermediate classes, where the inheritance chain would be reversed. The source code would be extracted to the new class which is now parent for the inheritance chain.

本质上,编译器将获取系统所有类的列表,并为每个单独的非装饰器类找到实现DecoratorInterface所有子代。 它将创建一个装饰器图,确保它是非循环的,并根据优先级算法对装饰器进行排序(稍后会详细介绍),并构建中间类,在该类中继承链将被反转。 源代码将被提取到新类,该新类现在是继承链的父级。

Sounds pretty complicated, yeah?

听起来很复杂,是吗?

Complicated it is indeed, unfortunately, but such a system allows you to combine the modules in a flexible way, and the modules will be able to modify literally any part of the system.

不幸的是,确实确实很复杂,但是这样的系统允许您灵活地组合模块,并且这些模块将能够在字面上修改系统的任何部分。

如果有多个模块可以修改单个类怎么办? (What If There Are Multiple Modules To Modify a Single Class?)

In cases when multiple decorator classes should be in effect, we can place them in the resulting inheritance chain according to their priority. The priority can be configured through annotations. I highly recommend using Doctrine Annotations or some config files. Take a look at this example:

如果应该使用多个装饰器类,则可以根据它们的优先级将它们放置在生成的继承链中。 可以通过注释配置优先级。 我强烈建议使用“教义注释”或一些配置文件。 看一下这个例子:

class Foo { public function bar() { echo 'baz'; } } namespace Module1; class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' modified'; } } namespace Module2; /** * @Decorator\After("Module1") */ class Foo extends \Foo implements \DecoratorInterface { public function bar() { parent::bar(); echo ' twice'; } } // ... somewhere in the app code $object = new Foo(); $object->bar(); // will echo 'baz modified twice'

Here, Decorator\After annotation can be used to place another module decorator further up the inheritance chain. The compiler would parse the files, look for annotations, and build the intermediate classes to have this chain of inheritance:

在这里, Decorator\After注释可用于将另一个模块装饰器放置在继承链的更远处。 编译器将解析文件,查找注释并构建中间类以具有此继承链:

Also, you could implement Decorator\Before (to place decorator class higher), Decorator\Depend (to enable decorator class only if another module is present or non present). Such a subset of the annotations would be pretty much complete to make any required combination of the modules and classes.

另外,您可以实现Decorator\Before (将装饰器类放在更高的位置), Decorator\Depend (仅在存在或不存在另一个模块的情况下启用装饰器类)。 这样的注释子集几乎可以完全构成模块和类的任何必需组合。

但是钩子或修补代码又如何呢? 这是否更好? (But What About Hooks Or Patching The Code? Is This Better?)

Just like Decorators, each of the approaches has its advantages and disadvantages.

就像装饰器一样,每种方法都有其优点和缺点。

For example, hooks (based some sort of Observer pattern) are widely used in WordPress and many other applications. Their benefits are having a clearly defined extension API and a transparent way of registering an observer. At the same time they have a problem of the limited number of extension points and indeterminate time of the execution (difficult to depend on the result of the other hooks).

例如,钩子(基于某种观察者模式)在WordPress和许多其他应用程序中被广泛使用。 它们的好处是拥有明确定义的扩展API和透明的注册观察者的方式。 同时,它们存在扩展点数量有限和执行时间不确定的问题(难以依赖其他钩子的结果)。

Patching the code is trivial to get started but it is often considered to be very dangerous, as it can lead to unparseable code and it is often very hard to merge several patches of a file or undo the changes. Patching the system may require its own DSL to control the modifications. Complex modifications would require deep knowledge of the system.

对代码进行修补很容易上手,但通常被认为非常危险,因为它可能导致无法解析的代码,并且通常很难合并文件的多个补丁或撤消更改。 修补系统可能需要其自己的DSL来控制修改。 复杂的修改需要对系统有深入的了解。

结论 (Conclusion)

The Forwarding Decorators pattern is at least an interesting approach that can be used to tackle the problem of achieving a modular, extensible architecture of PHP software while using familiar language constructs like inheritance or execution scope to control extensibility.

Forwarding Decorators模式至少是一种有趣的方法,可用于解决实现PHP软件的模块化,可扩展架构的问题,同时使用诸如继承或执行范围之类的熟悉语言构造来控制可扩展性。

Some applications have already incorporated the described concept, notably OXID eShop uses something very similar. Reading their dev docs is kinda fun, these folks do have a sense of humor! Another platform, X-Cart 5 eСommerce software, uses the concept exactly in the form described above – its code was taken as a basis for the article. X-Cart 5 has a marketplace for the 3rd party extensions that modify the behavior of the system yet do not break the upgradeability of the core.

一些应用程序已经结合了所描述的概念,特别是OXID eShop使用了非常相似的东西。 阅读他们的开发文档很有趣,这些人确实有幽默感! 另一个平台X-Cart 5电子商务软件完全按照上述形式使用该概念-该代码用作本文的基础。 X-Cart 5具有第三方扩展的市场,这些扩展可以修改系统的行为,但又不会破坏核心的可升级性。

Such a concept can be difficult to implement and there are some issues with the application’s debugging, but they aren’t impossible to overcome if you spend some time fine-tuning the compiler. In the next article, we will cover how to build an optimal compiler and autoloader and use PHP Stream filters to enable step by step debugging via XDebug on the original code. Stay tuned!

这样的概念可能难以实现,并且应用程序的调试存在一些问题,但是如果您花一些时间对编译器进行微调,这些问题并非不可能克服。 在下一篇文章中,我们将介绍如何构建最佳的编译器和自动加载器,以及如何使用PHP流过滤器通过XDebug对原始代码进行逐步调试。 敬请关注!

If you have any other tips and tricks you’d like to share – please let us know in the comments section below. Also, any questions are welcome.

如果您还有其他要分享的提示和技巧,请在下面的评论部分中告诉我们。 另外,任何问题都欢迎。

翻译自: https://www.sitepoint.com/achieving-modular-architecture-with-forwarding-decorators/

最新回复(0)