Achieving Modular Architecture with Forwarding Decorators

Share this article

Achieving Modular Architecture with Forwarding Decorators

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!


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.

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.

Picture for attention

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.

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.

Comparison of the original and compiled classes

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.

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.
  • 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)

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.

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:

Compiled classes diagram with annotation

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.

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).

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.

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.

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.

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!

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.

Frequently Asked Questions about Achieving Modular Architecture with Forwarding Decorators

What is the main advantage of using modular architecture in software development?

The primary advantage of using modular architecture in software development is its flexibility and scalability. It allows developers to break down complex systems into smaller, manageable modules. Each module can be developed, tested, and updated independently, reducing the complexity and risk associated with modifying an entire system. This approach also promotes code reusability, as modules can be reused across different projects, saving time and resources.

How does forwarding decorators contribute to modular architecture?

Forwarding decorators play a crucial role in achieving modular architecture. They act as wrappers around objects, adding or modifying behavior without changing the object’s code. This allows for greater flexibility and adaptability in software design, as functionality can be added or removed easily without affecting the underlying system. Forwarding decorators thus contribute to the modularity and extensibility of the architecture.

What is the difference between modular architecture and monolithic architecture?

In a monolithic architecture, the application is built as a single, indivisible unit. Any changes or updates to the system require modifying the entire codebase, which can be complex and risky. On the other hand, modular architecture breaks down the system into separate modules, each responsible for a specific functionality. This allows for easier updates, better maintainability, and increased scalability.

How does modular architecture enhance team collaboration?

Modular architecture enhances team collaboration by allowing different teams to work on separate modules simultaneously. This not only speeds up the development process but also reduces the chances of conflicts as each team works independently on their respective modules.

What are the challenges in implementing modular architecture?

While modular architecture offers numerous benefits, it also comes with its own set of challenges. These include the need for careful planning and design to ensure that modules interact seamlessly, the potential for increased complexity due to the need to manage multiple modules, and the requirement for rigorous testing to ensure that changes in one module do not adversely affect others.

How does modular architecture contribute to software quality?

Modular architecture contributes to software quality by promoting code reusability, reducing complexity, and facilitating easier testing and maintenance. By breaking down the system into manageable modules, developers can focus on improving the quality of each module, leading to overall better software quality.

Can modular architecture be used in other fields apart from software development?

Yes, modular architecture is not limited to software development. It is also widely used in fields like construction and manufacturing, where it allows for flexibility, efficiency, and cost savings by enabling components to be produced in large volumes and assembled in different configurations.

How does modular architecture support agile development practices?

Modular architecture supports agile development practices by enabling rapid and incremental development. Teams can work on different modules simultaneously, allowing for faster delivery of functional software. It also supports continuous integration and delivery, as modules can be tested and deployed independently.

What is the role of interfaces in modular architecture?

Interfaces play a crucial role in modular architecture. They define the way modules interact with each other, ensuring that they can work together seamlessly. By defining clear interfaces, developers can ensure that modules can be easily swapped or updated without affecting the rest of the system.

How does modular architecture support software evolution?

Modular architecture supports software evolution by allowing for easy updates and modifications. As business requirements change, modules can be added, removed, or updated independently, allowing the software to evolve and adapt over time without the need for major overhauls.

Eugene DementjevEugene Dementjev
View Author

Eugene Dementjev is a core developer and technical writer of the X-Cart team with more than 5 years experience of software development. Apart from PHP, Ruby and Javascript where Eugene is proficient, he takes active interest in machine learning, likes good UX and Japanese autos.

BrunoScompiledecoratorDecoratorsdesign patterndesign patternsmodularoddballOOPHPPHPxcart
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week