I have been experimenting and implementing a Dependency Injection Container in python. Now I am using it to build a really small CMS named ‘element’. The only data unit in this CMS is a Node. A node has a type, an id and a dictionary which contains data from the source ~ and that is it!
This model allows to perform really nicely however there is a major drawback: a node does not contain any fonctions or specific logic depending on the node type. This can be view as a good point as data should not be mixed with any logic. However on real life projects, we need to be a bit more pragmatic. Let’s take a simple example: a media gallery node. Obviously, a gallery is linked to medias.
Now to retrieve media nodes, there are different options:
- build the list when the gallery node is loaded, however this can be expensive if the callee does not need them to the current workflow.
- create a helper function to retrieve the medias, however the helper function will not be accessible from the node. So the helper function must be passed anywhere we need it.
- [put here your option …]
Cannot we merge all solutions into one ?
The pseudo code to retrieve the list will always be the same depends on the solutions:
<?php
fonction get_medias(node) {
medias = ['media1', 'media2'];
return medias;
}
Now the real question is how you can access to this workflow ?
- we have services which can be used to perform stateless logic
- we have data object wich can hold state and some methods
But as we have just saw with the get_medias
function, the main subject is the node. If you think about others helpers functions, the node will always be the first argument as it is the main subject.
Can we use services as methods ? Services or methods will always do the same work: return data or alter a state.
With PHP, we can have something like this for the Node object:
<?php
class Node
{
static $methods = array()
protected $id;
protected $type;
protected $data;
public function __call($method, $args) {
if (!isset(self::$methods[$this->type][$method])) {
throw new \RuntimeException(sprintf('Undefined method : %s on node type %s', $method, $this->type)
}
return call_user_func_array(self::$methods[$this->type][$method], $args)
}
}
The __call
method catch non existant methods, and the code check if the method for the current node type exists then the callable is started otherwise an exception is thrown.
So let’s check the callable code:
<?php
class MediaHelper
{
public function getMedias(node)
{
return array([/* array of medias */]);
}
}
Now, let’s see how we can plug them together:
<?php
// bootstraping
$container = new Container();
$container->add('media.helper', new MediaHelper);
Node::$methods['media.gallery'] = array(
'getMedias' = array($container->get('media.helper'), 'getMedias')
);
It will be possible to use the getMedias
method like this:
<?php
// usage
$node = new Node('id', 'media.gallery');
$medias = $node->getMedias();
$medias = $container->get('media.helper')->getMedias($node)
The main advantage of this code is that you have one logic shared accross a service and a data model. The data model only hold data but delegate actions to services. Thoses services can also be used in another context or be inject as dependency to others services.
What PHP code is doing here, it wasn’t suppose to be about a post about python …. Right! The code is about a concept, the current php code can be ported into python very easily.
But I keep the best part for python. The language has an unique feature which can make services as methods
very easily and natural: Metaclass. A metaclass defines how a class is being defined and a metaclass can be defined on the fly. So instead of having a static array of: type => methods, we can have true class with true methods however methods can be references to services.
The implementation for the Element CMS still need to be done so this will be another story. In the mean time, stackoverflow contains a very interested post on metaclass: http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python
As a side note, the “services as methods” strategy also open a question about the need of interface for data object vs method call depends on the context where data is used.