Mixins Plus - cpeosphoros/30log-plus GitHub Wiki
30log-plus provides stronger support for Mixins than the standard 30log library. The goal is avoid code duplication between classes and mixins as much as possible.
Features included with 30log-plus are:
Chained Methods
Chained methods provides a way to implement a simple case of the chain-of-responsibility design pattern with 30log-plus.
If present in one or more mixins included with a class, chained methods will be executed one after the other, in the order they were included. If the chained class also implements the same method, it will be executed last, after all the mixins'.
They are declared in the mixin table as a list with the methods' names, using
the key chained.
local function concat(v1, v2)
return (v1 or "")..(v2 or "")
end
class = require "30log-plus"
aclass = class()
aclass.Inter = function(self)
self.a = concat(self.a, "XX")
return true
end
mixchain1 = {
Inter = function(self)
self.a = concat(self.a, "C1")
self.b = concat(self.b, "C1")
return true
end,
chained = {"Inter"}
}
mixchain2 = {
Inter = function(self)
self.a = concat(self.a, "C2")
self.b = concat(self.b, "C2")
self.c = concat(self.c, "C2")
return true
end,
chained = {"Inter"}
}
inst = aclass:extend():with(mixchain1):with(mixchain2)()
inst:Inter()
print(inst.a, inst.b, inst.c)
--> "C1C2XX" "C1C2" "C2"
If a mixin's chained method returns false or nil, the chain will be interrupted
at that point. So, supposing mixchain1.Inter() returned false in the previous
example, the result would be:
--> "C1" "C1" nil
Intercepting Methods
Intercepting methods provides an easy way to implement the decorator design pattern with 30log-plus.
If present in a mixin included with a class, a pair of intercepting methods will be executed before and after, respectively, the intercepted method, with later intercepts enclosing earlier ones.
They are declared in the mixin table as a list with the intercepted methods'
names, using the key intercept and a pair of BeforeX() and AfterX()
methods, where 'X' is the name of the method being intercepted.
mixinter1 = {
BeforeInter = function(self)
self.a = concat(self.a, "B1")
return true
end,
AfterInter = function(self)
self.a = concat(self.a, "A1")
return true
end,
intercept = {"Inter"}
}
mixinter2 = {
BeforeInter = function(self)
self.a = concat(self.a, "B2")
return true
end,
AfterInter = function(self)
self.a = concat(self.a, "A2")
return true
end,
intercept = {"Inter"}
}
inst = aclass:extend():with(mixinter1):with(mixinter2)()
inst:Inter()
print(inst.a)
--> "B2B1XXA1A2"
The intercepting methods will be excuted even if the decorated class doesn't implement the intercepted method, which would give this result, with the examples above:
--> "B2B1A1A2"
A intercepting mixin doesn't need to implement both decorators. Suppose, in the
previous examples, mixinter1 didn't implement BeforeInter() and mixinter2
didn't implement AfterInter(). The result would be:
--> "B2XXA1"
As with chained methods, if any method in the sequence, including the decorated class's, returns false or nil, the chain will be interrupted at that point.
Using them together
Chained and intercepting methods may be used together, both within the same mixin or different ones.
Using them in the same mixin looks like this:
mixhybrid1 = {
BeforeInter = function(self)
self.a = concat(self.a, "C3")
return true
end,
BeforeInter = function(self)
self.a = concat(self.a, "B3")
return true
end,
AfterInter = function(self)
self.a = concat(self.a, "A3")
return true
end,
intercept = {"Inter"}
}
inst = aclass:extend():with(mixhybrid1)()
inst:Inter()
print(inst.a)
--> "B3C3XXA3"
And mixing all of them up, supposing no false or nil return, gives this
result:
inst = aclass:extend()
:with(mixhybrid1, mixchain1, mixinter1, mixchain2, mixinter2)()
print(inst.a)
--> "B2B1B3C3C1C2XXA3A1A2"
Inclusion of different kinds of non-hybrid mixins is entirely commutative. Thus, all the following examples are equivalent:
cclass = aclass:extend()
:with(mixhybrid1, mixchain1, mixinter1, mixchain2, mixinter2)
-------
cclass = aclass:extend()
:with(mixhybrid1, mixinter1, mixinter2, mixchain1, mixchain2)
-------
cclass = aclass:extend()
:with(mixhybrid1, mixchain1, mixchain2, mixinter1, mixinter2)
However, as stated in their respective sessions, inclusion of the same kind of mixins, or of hybrid ones with the correspondent kinds, is not commutative, as their ordering matters.
In resume, when mixing:
- Chaining is always calculated before intercepting;
- Intercepters will decorate the whole chained sequence;
- Those operations will always happen in the same order the mixins of each kind were included.
Working with Subclasses
Subclasses inherit their superclasses' mixins, which are subject to the same ordering logic as if they were all included at the same time. Thus, all the following examples are equivalent:
cclass = aclass:extend()
:with(mixhybrid1, mixchain1, mixinter1, mixchain2, mixinter2)
-------
bclass = aclass:extend():with(mixhybrid1, mixchain1)
cclass = bclass:extend():with(mixinter1, mixchain2, mixinter2)
-------
bclass = aclass:extend():with(mixhybrid1, mixinter1, mixinter2)
cclass = aclass:extend():with(mixchain1, mixchain2)
-------
bclass = aclass:extend():with(mixhybrid1, mixchain1, mixchain2)
cclass = aclass:extend():with(mixinter1, mixinter2)
Setup methods
As with classes, mixins may implement a :setup(...) method which will be
called on instantiation, but with self referring to the class object, not the
instance being created, before :init(...) is called on the new instance.
The parameters of :setup(...) will be the same passed to :new(...).
Mixins' setup methods will be ran in the order they were included, after the class's and its super classes' setup methods, if any.