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.