-
Notifications
You must be signed in to change notification settings - Fork 3
2012 03 16 re mix the mixin based composition pattern
Published on March 16th, 2012 at 14:05
At rubicon, we implement various applications based on our business case processing product called ActaNova. ActaNova is a piece of software that implements electronic records management (and more) in a standard way. Those specific applications we implement on top of it extend the standard domain model, UI, database schema, and so on with application-specific behavior and structure.
This has quite a few interesting effects on the technical implementation. For example, we need a way to allow the UI to be extended (and modified) by modules added to the application. In this post, however, I’d like to concentrate on the idea of building a domain model that is extensible by modules within the application. And I’ll use a pattern based on mixins (as provided by re-mix), which we call the Mixin-Based Composition Pattern.
The pattern allows us to do two things:
- Define a domain class as an aggregation of functionality defined by different modules within the application, and
- extend the domain class from modules added to the application.
Consider a simple Person
class, as it could be defined by an electronic records management software. Note that I’m not talking about the actual current or future implementation of ActaNova here, I’m just inventing an example that lets me illustrate the pattern. Let’s say a Person
has the following features, implemented by various modules within the records management product:
- It is an entity in the domain model, so it has got an ID.
- It also has a first name, last name, middle names, and date of birth as standard properties.
- It is versioned, which means that changes made to its data don’t simply overwrite any previous values, but put them into a history object. Versioning is implemented in a generic fashion by the
Versioning
module. - It is also tenant-bound, which means that it belongs to a specific tenant within the application (and only in the context of that tenant can it be loaded, edited, and changed. Tenant handling is implemented in a generic fashion by the
Tenant
module.
A specific employee records management application is built around the standard records management software. It creates an Employee
domain class that derives from Person
. Different modules extend the standard Person
class as follows:
- The
Employee
domain class has a job description property in addition to the inheritedPerson
properties. - The
Human Resources
module adds the current salary of the employee. - The
Organizational Structure
module adds the current manager of the employee.
Here’s a picture of how you could model this domain by using mixins:
As you can see, the Person
class aggregates the versioning and tenant binding functionality defined by the respective modules by using generic mixins exposed from those modules. The Employee
class derives from the Person
class and is extended by mixins from within the Human Resources
and the Organizational Structure
modules. This is a combination of simulating multiple inheritance via mixins (reusing code by including mixins into your classes) and use-case slicing, also implemented via mixins (implementing different slices of your code separately, use case by use case). (And in case you’re wondering, the OnLoaded
and OnCommitting
methods in the picture are considered to be infrastructure methods exposed by all domain objects.)
Using re-mix, this all works really nicely, with one caveat: when you have an instance of Person
or Employee
and want to access those properties added by mixins, you need to cast the instance to IVersionedObject
, IPaidEntity
, and so on. Which is not very nice, which is why the Mixin-Based Composition Pattern adds the following conventions:
- A composed object (such as
Person
) must provide a This property of a typeTComposedInterface
. TheTComposedInterface
type must be an interface derived from all the interfaces introduced by used mixins. (In the example, that would be anICompletePerson
interface, inheriting fromIVersionedObject
andITenantBoundObject
.) TheTComposedInterface
can also provide properties defined by the core object itself. (I.e.,ICompletePerson
can also provide the ID, FirstName, LastName, MiddleNames, andDateOfBirth
properties.) - Mixins that extend a specific class must also provide a conversion method
As...()
as an extension method for instances of that target class. The conversion method returns the instance cast to the interface introduced by the mixin.
These conventions allow client code to access all members added by used mixins via the This
property and all members added by extending mixins via the As...()
extension methods. This makes it very convenient to work with a domain implemented via mixins. Here’s an example of how the Employee
class shown above would be used:
var employee = ObjectFactory.Create<Employee\>();
Console.WriteLine (employee.ID);
Console.WriteLine (employee.FirstName);
// Properties added by mixins included by the Person/Employee classes
Console.WriteLine (employee.This.TenantID);
Console.WriteLine (employee.This.VersionID);
// Properties added by mixins extending the Person/Employee classes
Console.WriteLine (employee.AsOrganizedEntity().Manager);
Console.WriteLine (employee.AsPaidEntity().Salary);
re-mix has a ComposedObject<TComposedInterface>
base class that provides the This
property, so implementing the Person
class could be as simple as this:
public interface ICompletePerson : IVersionedObject<Person>, ITenantBoundObject {}
[Uses (typeof (VersioningMixin<Person\>))]
[Uses (typeof (TenantBindingMixin))]
public class Person : ComposedObject<ICompletePerson>
{
public Person (int id)
{
ID = id;
MiddleNames = new List<string\>();
}
public int ID { get; private set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<string\> MiddleNames { get; private set; }
public DateTime DateOfBirth { get; set; }
// etc.
}
(I’ve given the IVersionedObject
interface a generic parameter, just to show that this is possible.)
But even if you can’t (or don’t want to) use the ComposedObject
base class, implementing the This
property by hand is very simple:
public class ...
: IHasCompleteInterface<TComposedInterface\>
{
// etc.
public TComposedInterface This
{
get { return (TComposedInterface) this; }
}
}
The IHasCompleteInterface
marker interface just causes re-mix to include the given interface on the domain object type (without the domain programmer having to implement its methods).
Here’s an example of one of those As...()
extension methods, as it would come with the HumanResources
module:
public static class EmployeeExtensions
{
public static IPaidEntity AsPaidEntity (this Employee employee)
{
return (IPaidEntity) employee;
}
}
No magic there, either, just some syntactic sugar to avoid users having to make those casts.
To summarize, by implementing the Mixin-Based Composition Pattern, you can:
- build your domain classes by means of aggregating functionality from different modules in the application by using mixins exposed from those modules,
- access members added by those mixins nicely via the This property,
- extend your domain model from external modules by having mixins in there that extend the domain classes, and
- access members added by those mixins nicely via the extension methods provided with the mixins.
This makes for very extensible, yet nicely factorized and simple to use domain models.
(One word about persistence and O/R mapping: Our own O/R mapper “re-store” recognizes mixed entities, and it persists properties added by the mixins into the database. It’s of course possible to implement the same behavior for other O/R mappers and persistence frameworks as well if they provide the necessary extensibility points. Whether to put the mixin data into the same database tables as the core domain data is another topic and depends on the specific situation.)