NAME
Class::Mixer - Arrange class hierarchy based on dependency rules
SYNOPSIS
package Base;
use Class::Mixer;
sub x { 'Base::x' }
package Main;
use Class::Mixer before => 'Base';
sub x { 'Main::x' }
my $obj = Main->new();
print "@Main::ISA\n"; # prints "Base Class::Mixer"
package Mixin;
use Class::Mixer before => 'Base', after => 'Main';
sub x { 'Mixin::x' }
package NewMain;
use Class::Mixer isa => 'Main', requires => 'Mixin';
sub x { 'NewMain::x' }
my $obj = NewMain->new();
print "@NewMain::ISA\n"; # prints "Main Mixin Base Class::Mixer"
DESCRIPTION
This module is designed to solve a problem which occurs when using inheritance to mixin behaviors into a class hierarchy. The dependencies between a number of mixin modules may be complex. When different components wrap the same behavior, they often need to be in a specific order in the call chain, making it tricky to get the base classes to inherit in the right order.
Then if you have a class Main which gets the inheritance right, and you want to add a class Mixin which needs to go in the middle of the inheritance, you cannot simply do package NewMain; use base qw(Main Mixin);
because Mixin will be put at the end of the inheritance chain.
Also, if you have a class Foo::Better which enhances the Foo behavior, the same problem occurs trying to mixin Foo::Better. And it is even worse if some classes have done use base 'Foo';
to try to enforce the correct hierarchy.
This module solves these problems by implementing a dependency-based hierarchy. You declare the relations between the classes, and an order of inheritance which will support those relations is determined automatically.
For example, if you have a Logging component and an Authentication component, the Logging needs to be called first, because if Authentication fails, it will never be called at all. In the Logging class, one can declare the Mixer rule before=>'Authentication', optional=>'Authentication'
, so that if Authentication is in the class hierarchy, Logging will be placed before it, but it will not complain if it is not there. Alternatively, one could place the rule after=>'Logging'
in the Authentication class to achieve the same result.
Logging could also be an abstract base class, and any class which declares isa=>'Logging'
will be kept together with Logging in the inheritance chain. This allows rules to refer to behavior classes without needing to know exactly which behavior class will actually be used.
In my own usage, in a structured wiki project, less essential classes usually declare their rules in relation to more essential classes, such as Storage and IO. So for example, the Security class declares before=>'Storage'
because it must be invoked before records are stored or retrieved, and also requires=>'Session'
because it needs a session but does not share any behavior methods with it. Session declares after=>'IO'
because it needs IO to process cookies and arguments before it can work. The Index and Revision classes both declare before=>'Storage'
but they do not care which of them runs first.
Class::Mixer combines functions from base and Class::C3 to do its job. It will require
the given classes (unless optional) similar to use base
. And it attempts to force c3 semantices, so you should do $self->method::next
instead of SUPER
for inheritance.
Inheritance rules
When you design your classes, instead of doing use base
or our @ISA
, do something like the following:
package Example;
use Class::Mixer before => 'BaseClass', 'OtherClass',
after => 'SoAndSoClass',
isa => 'PreviousExample',
requires => 'OtherBehavior',
optional => 'OtherClass';
The Class::Mixer class provides a basic new() method, which will call init(), which your classes can override.
The actual inheritance is computed for a class the first time new() is called.
The inheritance rules are described here:
before
The 'before' rule means this package must occur before some other class in the method dispatch order, which means that if they both define the same method, this class will be invoked before the other. In the inheritance hierarchy, this class should be a descendant of the other.
This is exactly the same as what you would get if you did use base
instead. Which you can, and Class::Mixer will notice and use it.
If no rule type is given (e.g. use Class::Mixer 'BaseClass';
) then the before rule is assumed.
after
The 'after' rule means this package must occur after some other class in the method dispatch order. The other class will usually be a descendant of this one. This is best if used rarely, but it is nice when it is necessary.
It is essentially the same as doing before => 'this', optional => 'this'
in the other class.
isa
The 'isa' rule establishes a very strong isa relationship between classes. All the classes are related by @ISA
, of course, but this 'isa' means that the particular behaviors implements by the classes are the same, and that this one enhances the other.
The classes are kept together as close as possible in the computed hierarchy, and all rules which were applied to the other class will be applied to this class as well.
requires
The 'requires' rule says that this class needs the behavior from some other class, but the inheritance order is not important. Usually this is because the two classes do not share any methods.
optional
The 'optional' rule doesn't affect the class hierarchy. It simply makes the class not complain if the other class is not there.
This is useful when we want to say, "I do not really need this other class, but IF someone else does, I should be before (or after) the other class."
Why not Traits?
Traits are complementary to inheritance, and do not address situations where one module needs to extend another by extending/wrapping/inheriting the same method. Traits are more concerned with properly adding new methods to a class, while Class::Mixer tries to solve some problems with complex mixin-style inheritance trees.
For example, I have a write
method, and several optional behaviors which happen when write
is called, such as an Index behavior and an VersionControl behavior. These will both override the write
method and call the parent method before or after they do what they need to do. But this violates the flattening property of traits. The write
method in Index and VersionControl are not conflicting; they need to inherit, perhaps in a specific order, so that both with be called.
It may be that what I am actually describing is event-based, because write is the event, and the behaviors are various things which need to happen when that event occurs. None of the event systems I have looked at have contraints to allow ordering the behaviors relative to each other, so even if I setup an event model, I would still need the solution which Class::Mixer provides.
Comparison with Class::C3::Componentised
Class::C3::Componentised is used in DBIx::Class similarly to Class::Mixer to implement the same sorts of mixin behaviors. The difference is exemplified by this quote from DBIx::Class::Component,
"The order in which is you load the components may be very important, depending on the component. If you are not sure, then read the docs for the components you are using and see if they mention anything about the order in which you should load them."
With Class::Mixer, the user never has to deal with the complexities of component order. Component ordering requirements are both documented and *enforced* by the inheritance rules in each component. So when building his application class, he can list the desired component modules in any order, and let Class::Mixer put them in a correctly functioning order.
On the other hand, Class::Mixer has no way to override the rules in the subclasses, so if the user decided he really wanted Authentication to happen before Logging, he would have to change the rules in the subclasses in order to make that happen.
BUGS and TODO
Probably.
"optional" isn't fully implemented yet.
TODO?: implement a 'nota' rule, to prevent someone from putting this class in the same hierarchy as another.
SEE ALSO
The snide comments in mixin probably apply to this module as well.
AUTHOR
John Williams, <smailliw@gmail.com>
Thanks to Matt S Trout for help clarifying the documentation.
COPYRIGHT AND LICENCE
Copyright (c) 2009 by John Williams
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.