Modular Application Architecture - Inheritance
When developing software, sometimes we need to allow our application to have plug-ins or modules developed by third parties. In this post we will see how "inheritance" can help us to build a plugin system for our application.
This is the fourth post from a series of posts that will describe strategies to build modular and extensible applications. In this post we will start looking how to use "inheritance" to create a plugin based application.
Inheritance is a characteristic offered by many class/object oriented languages and allows you to "inherit" some of the functionalities from a "parent" object or class. The object that inherits those functionalities can eventually alter some of them but for the rest behave as the inherited class.
Being able to change the behaviour of the inherited class but "look" as the original
comes useful when building plugin systems that allows to change existing functionalities of the core.
Combined with a good plugin registration system (configurable or based on discovery) is possible to change almost every
aspect of an application without touching the application core code.
In the picture here above there is a "base page" that has some blocks defined (header, side, content and footer) and there is a "new page" that inherits earthing from the parent page and "overwrites" the content page.
Possible use cases for this strategy are "themes" for the layout of a website. By overriding the right portion of the original class is possible to change, add, remove functionalities and/or look-and-feel.
Most of the frameworks from the past 10 years were built on the idea of "extending" some parts of them
and replacing/implementing some other implementations. Many developers quickly started to
override any part of the framework (just because the programming
language was allowing them to do it). But the ability of being able to change everything comes with a price.
Being able to alter fully the behaviour of the application often was resulting that the "core" application could be broken
really easily by some "bad" plugin.
Basic example
Implementing an "inheritance" based plugin architecture requires the following "things" to be in place:
- A "base" class that offers some functionalities.
- a "configurable" way to get an instance of that class ( using just its name, a factory class or using dependency injection).
- A way to "run" that class.
- A way to change the class to run.
Let's make an example by looking at a simple example where plugins can change the homepage of a website:
This could be our "base" class, the default homepage.
<?php
class HomePage
{
public function display()
{
return 'This is my homepage!';
}
}
This could be our "application core".
<?php
// this is the class able to "run" our base class
class Website
{
private static $pageName = 'HomePage';
public static function init($pageName)
{
self::$pageName = $pageName;
}
public static function run()
{
$pageToRun = self::$pageName;
$page = new $pageToRun();
echo $page->display();
}
}
// run
Website::run();
Website::run
is responsible to "run" our application while the method Website::init
is the method that allows
to register the "page plugin".
Now let's suppose we want to write a modified version of the homepage but adding the nome of the owner of the page. We can do something as:
<?php
// this is our "base" class
class NewHomePage extends HomePage
{
public function display()
{
$original = parent::display();
$new = 'My name is tom. '. $original;
return $new;
}
}
// change the "configuration"
Website::init('NewHomePage');
As you can see from this very simple example, the NewHomePage
class has full control over the display
method.
Can decide to alter its result or even to not invoke the parent method at all.
The parent class (HomePage
) can be arbitrary complex and have tens (or hundreds) of methods, is up to the developer to
decide which one to override.
Here the "plugin registration" is done by configuration, but can also be done by "discovery". The method Website::init
could lookup for the class name directly from the database, from a predefined list of directories or any other place.
As it will be discussed more in detail at the end of this article, inheritance is a relatively dangerous feature and
can quickly go out of control. Having long hierarchies and fat-classes with many many methods used by few child classes,
is a perfect recipe for failure.
Inheritance has its use cases too, but the hierarchy should be short and almost all the "parent" methods should be used
by child classes.
A more concrete example.
The previous example can be too much abstract and that pattern can be used to develop software in general. A better example could be done using Twig to implement a configurable/theme-able layout system for web pages. Twig offers an inheritance system very similar except that the "parents" can be decided at runtime.
Let's suppose the following Twig template page (named layout.html.twig
) that implements a common website layout
with "header", "side", "content" and "footer":
{# layout.html.twig #}
{% block html %}
<html>
<title>Welcome to my website</title>
{% block body %}
<body>
{% block header %}
<div class="header">
My homepage
</div>
{% endblock %}
{% block content_wrapper %}
<div class="content-wrapper">
{% block sidebar %}
<div class="sidebar">
Menu items here
</div>
{% endblock %}
{% block content %}
{% endblock %}
</div>
{% endblock %}
{% block footer %}
<div class="footer">
My copyright
</div>
{% endblock %}
</body>
{% endblock %)
</html>
{% endblock %)
<!-- html equivalent -->
<html>
<title>Welcome to my website</title>
<body>
<div class="header">
My homepage
</div>
<div class="content-wrapper">
<div class="sidebar">
Menu items here
</div>
<!--
content section here
<div class="content">
Lorem ipsum sploram
</div>
-->
</div>
<div class="footer">
My copyright
</div>
</body>
</html>
This can be the general layout for our website, a more detailed page (the homepage.html.twig
) can as example extend the
template and "implement" the homepage.
{# homepage.html.twig #}
{% extends layoutName %}
{% block content %}
<h1>Welcome to my homepage</h1>
{% endblock %}
Here there are two important things to notice:
- First, the
{% block content %}
allows us to overwrite the default (empty) implementation offered by thelayout.html.twig
template. The resulting template is simple and focused on the homepage (as it should be).homepage.html.twig
can override any block defined in the parent template, changing virtually any part. - Second and more important, the "parent" template name is not hardcoded in the template.
Is dynamic and defined at runtime ( by the
layoutName
variable) and can be changed (by a registered plugin as example).
A code necessary to run our page can be:
<?php
// this variable holds the layout template name
$defaultLayout = 'layout.html.twig';
$twig = new Twig_Environment(new Twig_Loader_Filesystem('/path/to/templates'));
$twig->display('homepage.html.twig', array('layoutName' => $defaultLayout));
Let's suppose now we want to allow a plugin to change the layout but keeping the homepage content
(as example removing the sidebar).
We need only to allow the plugin to change the $defaultLayout
value and provide its own layout implementation.
Our new template can be just:
{# plugins/pluginName/layout.html.twig #}
{% block content_wrapper %}
<div class="content-wrapper">
<h1>my plugin layout</h1>
{{ block('content') }}
</div>
{% endblock %}
plugins/pluginName/layout.html.twig
is overriding the content_wrapper
block and just calling the content
block (not calling also the sidebar
block as in the original layout).
Obviously the plugin needs to be registered (via discovery or configuration) and being able
to change the $defaultLayout
value. As example:
<?php
// should be 'plugins/pluginName/layout.html.twig' as registered by our plugin
$defaultLayout = PluginRegistry::getLayout();
// this is identical as before
$twig = new Twig_Environment(new Twig_Loader_Filesystem('/path/to/templates'));
$twig->display('homepage.html.twig', array('layoutName' => $defaultLayout));
This strategy allows to use inheritance to change completely the layout without the supervision of the "application core".
Other strategies
In my experience I saw other implementations that were also allowing similar things in pure PHP code, by generating at runtime (or at deploy) "custom" PHP classes, renaming the original one and creating more complex inheritance schemes that are not available in PHP. Some examples were allowing to extend from two (or more) classes at the same time and other "esoteric" tricks.
Nowadays PHP traits can be used to achieve similar functionalities in a much more clean and efficient way.
Conclusions
Personally I'm not a big fan of inheritance. When it comes to PHP applications I try to use as much as possible composition over inheritance (of course inheritance has its use cases too). On the other side, the "inheritance" model offered by twig is tailored for layouts and works really well.
Advantages:
- Allows plugins to alter almost any part of the application
- Does not require explicit "entrypoints"
- For some use cases works really well
Disadvantages:
- Allowing to change "anything" increases the risk that a plugin breaks the application
- Difficult to document what a plugin can do
- Difficult to control what a plugin can do
- Requires deeper knowledge of the "application core" compared to other plugin strategies.
- The plugin is tightly coupled with the application structure
- Changes to the application may require bigger changes to the plugin compared to other plugin strategies.
Inheritance can go out of control very quickly compared to that other strategies, but when dealing with some particular requirements (as the necessity to allow changes to any part of the application or not limiting the extensibility to specific extension points pre-defined by the application core), the inheritance-based strategies are a good solution.
Hope you enjoyed this article and if you have some feedback, do not hesitate to leave a comment. In the next article will discuss strategies that are based on conventions and have similar characteristics to what covered in this article.