Worker Domain design pattern
Back when I introduced:
https://github.com/x00/Gdn_PScaffold
I encourages through it a design pattern, which in hindsight is pretty terrible. It can be broadly illustrated by this
The intention was to organise code. But it is flawed for the following reasons:
Using the inheritance chain break code into pieces, but there is was no implicit way of identifying what something comes from within those method, because it is all lumped together in the same pluign object and the methods are shared.
Much of the functionality that is commonly put in plugin classes has nothing to do with the pluggable API of the framework, so doesn’t actually need to be in there.
Although this pattern organised code, by moving it, it didn't solve this issue of not really having anything to do with Gdn_Pluign or the above point.
By virtue of coupling the work to the plugin instance through inheritance, this limits what other design patterns you can use, making it inflexible.
The order of the chain matters, when it really shouldn't be so strict, this can both cause practicability problems as well as not making sense depending on what it is you are trying to model.
Instead, I have come up with another design pattern, which I will be adding to Gdn_PScaffold (you would have to compile with --legacy
if you wish to use the old pattern).
The pattern I have come up with is called the Worker Domain design pattern.
Here is an example plugin which implements it:
https://github.com/x00/WorkerDomain
It is not a be and end all, it might be to someone’s taste. Even I'm not going to implement on every plugin, especially small plugin which are pretty stable as it.
Follows are excerpt from the README.md
grep is your friend.
Comments
About
This plugin is an example of the Worker Domain design pattern. It is a working example and I have been generous with annotating the code with documentation.
This pattern was developed with pluggable APIs in mind. Particularly for the Vanilla/Garden framework, which this example uses.
Place in the plugin directory an enable it the normal way.
You can access the "Hello World" by going to /helloworld and you can change the settings at /settings/workerdomain
However more interesting is the plugin code itself.
grep is your friend.
Intro
A common way of extending code besides overwriting, is to make use of event hooks and other special functions/methods that will be called by the framework's pluggable API.
I use 'Hooks' broadly here to denote anything that inserts, overrides (overloads), or adds new functionality through documented means. So it can include an MVC framework a convention to create new controller methods dynamically for or 'magic'
__call
methods.This example was specifically designed with pluggable MVC frameworks in mind but may be adapted for others.
There are two common styles of pluggable APIs, preregistered hooks, where functions and object methods are explicitly registered to an event like Wordpress, or named convention based implicit methods which is more in the the style of Vanilla/Garden.
Either way you often see a class (or file containing functions) specifically to be the interface with all these hookable events, which is the convention in Garden plugins or theme and application hooks files and what I have in mind for this example.
I didn’t design this for Applications as a whole, but specifically for the increasingly popular way of extending, and adapting functionality within frameworks, and their applications.
grep is your friend.
Description
Without further ado, here is a psuedo-UML example of implementation of the pattern
The purpose is both separation of concerns and a way of organising and making clear implicit code. What you have is the standard plugin class at the bottom left in pink
MyPlugin
, is what is directly employed by the framework, and its methods will be called by the framework.Quite often this file doesn't just serve the purpose of interfacing with the pluggable API, it contain all kinds of other logic, which can make it quite cluttered, especially if your plugin is more than a dozen lines.
The idea in this pattern is to keep this file lean, and basically only contain interfacing methods, if at all possible.
Now within plugin there can be all sort of different tasks, and these categories of tasks can be intermingled. The idea of the pattern is to place task operations under
Workers
, which can collaborate together.Now Workers are grouped in a single collection, but their classes are not part of the plugins inheritance, they are are merely employed to do tasks.
This means that each worker is capable of using its own design pattern an is not constrained by a particular implementation, or inheritance, as it is not part of the plugin object, it is simply loosely held by reference in a collection.
Workers need a way of collaborating with each other, and also a way of interacting with the plugin instance.
These two happen to be related, because the collection is held by the plugin instance. So each Worker has a back-reference to the plugin, set dynamically use a public property
->Plgn
.To reference the plugin it would be as simple as
$this->Plgn
. However it is more long-winded to reference another worker,$this->Plgn->Workers[WorkerName]
. However you won't have worry about this as you will see.But wait a minute...how do the workers even get employed, and put into the collection in the first place? Well there is a Utility method called LinkWorker that does it.
However this is not just called repeatedly in the plugin class, because this isn't really to do with the framework's pluggable interface, but a design pattern for the plugin as a whole.
Instead you have Domain classes (shown on the left in yellow and cyan), which use inheritance of the plugin class as a mechanism of the pattern. These classes are supposed to be lean, and have a very specific purpose.
So instead of your plugin file extending
GND_Plugin
directly in this case there is an inheritance chain of Domain classes in-betweenGND_Plugin
and the Plugin class. These files will have to be explicitly loaded as Garden auto-loader's will not do this, but I suggest using conventions outlined here. As Domains classes are supposed to be lean they do not pollute with unnecessary functionality, but simply using inheritance as mechanism to provide a Domain method to link the worker.Domains like the the name implies, provide a simple identifier for the Workers and Plugin class to reference Workers. This is simply a named Method which is the same as the worker reference string in the Worker collection.
Each Domain is responsible for linking a single Worker at a time. Note this is one instance. It doesn't say that you couldn't reuse the same class for different Worker. The linking of the worker could be conditional, for example a different version of the framework, could Link a different worker or the same class with custom parameters send via the LinkWorker.
This possibility of custom deployment is one of the reason why you have separate Domain classes, the other reason is simply to make it plain in the code, which is part of the reason for using a design pattern in the first place. it would be easy to magically deploy the workers but the wouldn't be as clear, not to mention it would carry an overhead.
A Domain classes are usually very lean they can go in the same file as a Worker, which is also for clarity, and simplifies loading.
A Domain neither owns nor manages Workers once deployed. its job is to link the Workers, and return the worker so they can collaborate together.
The Workers are known by their short name with is usually a suffix of their class name. The convention for Worker class names is
PluginNameWorkerName
and the Domain classPluginNameWorkerNameDomain
e.g. withMyPlugin
plugin theMyPluginAPI
would be for the WorkerAPI
the Domain would typically be calledMyPluginAPIDomainspuedo
, so in order for the plugin class to use a worker such asAPI
it would be as simple as$this->API()
, and in order for a Worker to useAPI
it would be as simple as$this->Plgn->API()
.However as LinkWorker lets you put whatever Name and Class identifier as you like, you can do as you please, and this can be useful if you have a more complex scenario. After these two parameters, it also can take an arbitrary number of parameters, which is passed to the constructor of the Worker on initialisation, however if you are using many of them of too often, you are probably doing it wrong.
In the case of
->API()
, it is referencing->Workers['API']
internally. However if it is not initialised, LinkWorker will automatically do that for you, which is the advantage of using the Domain method of access. If a Worker is not used it is not initialised, and it is initialised automatically on first use. If you do not use the domain method of acces, this will not happen. It is lazy loading if you like.As you remember
$this->Plgn
is the public back-reference to the plugin instance from a Worker (you don't have to explicitly declarepublic $Plgn;
, but it shouldn't hurt). This means if you wish to use native plugin methods such asGetView
, a worker could do$this->Plgn->GetView()
. As it happens this example also includes a bunch of useful Utility functions for plugin development so I recommend$this->Plgn->Utility()->ThemeView()
instead, which usesGetView
internally, however this is not part of the design pattern just an implementation using the pattern with some code candy.Hooks can have parameters such as
$Sender
which can easily be passed to Worker methods, in order to do the work. Nothing unconventional about this.As you can see in the diagram the
UtilityDomain
is a little less lean because it contains the Worker collection and also theLinkWorker
method used to like all the workers. This is because it both serves as a Domain for Utility Worker, but also holds all the workers and the method to link them. It doesn't have to be done this way but it is clear enough IMO.Now you can see in this example you have the Workers
Utility
,API
,Settings
andUI
shown on the right and the corresponding domain classes shown on the left. This is simply a suggestion, you can have whatever Workers you like. You can add more Workers, so long as the chain is extended for the Domains. I will talk more about the specific implementation and how it relates to the Vanilla/Garden in a linked discussion.The actual order of the Domain chain is less important, because workers are decoupled from the plugin, and as they are auto-initialised they are available to each other from the outset. However the order might be useful for visualising the extension.
You could say Workers have a loose hierarchy, because some may never be directly employed by the plugin's hooks. However it is more accurate to say the Workers are collaborative, and delegated to specific sorts of tasks, and the hierarchy, so far as it exists, is fairly flat. Though obviously by design
API
is more low level thanUI
in this example.grep is your friend.
I'll come back to explain specific code candy in this implementation, which is not require by this pattern but could be useful for plugin development, and is implemented in the worker domain way.
grep is your friend.
It's a very interesting topic, thank you for sharing. More explanations are always welcome.
There was an error rendering this rich post.