Recently I was clued in to a thing called Component-based architecture by one of my good friends. I'd never heard of it before, but I find some aspects of it very appealing.
I never thought that the strict inheritance model would work for a large system. It's hard to define multi-faceted things using a strict tree of inheritance (rather than, say, a DAG). In a nutshell, the component-based architecture dictates that classes themselves have no properties except for a collection of specialized pieces of functionality. The idea is that you can create new classes by dropping in different combinations of functionality, which offers better flexibility.
Ruby and mixins
As with many good ideas, multiple people come up with them independently. I actually use the spirit of this technique in certain places in the MUD. However, I take heavy advantage of the language features of Ruby as the mechanism for making the components work. There's a commonly used language feature of Ruby called mixins, wherein basically an interface is allowed to add instance data and methods to a class.
Here's a quick example:
# mixins are modules, not classes
module Reviewable
def reviewScore()
return @reviewScore
end
def reviewScore=(val)
@reviewScore = val
end
# in Ruby you can use this shorthand for the getter and setter:
# attr_accessor :reviewScore
end
class VideoGame
include Reviewable
end
game1 = VideoGame.new()
game1.reviewScore = 5
class Shoes
include Reviewable
end
vans = Shoes.new()
vans.reviewScore = 9
So the Reviewable
mixin adds the reviewScore
instance methods and data to a class--any class. Video games and shoes obviously have nothing to do with each other, but you could concievably review either, and the mixin lets you add that functionality very easily.
The component-based architecture model is a little different. In it, the class would have some kind of "reviewable" component, and whatever main loop needs to process the "reviewable" data would call some abstracted method on the object to fetch it. Maybe something like this?
class Reviewable
def getData()
return @reviewScore
end
def updateData(val)
@reviewScore = val
end
end
class VideoGame
def initialize()
self.components = [Reviewable.new()]
end
attr_accessor :components
end
def mainLoop()
entities.each() { |ent|
ent.components.each() { |comp|
# consume the data from each entity's components
print comp.getData()
}
}
end
"Real-world" examples
I mentioned I use the mixin model in my MUD. I'll give some examples. But there's a difference, which I'll touch on at the end. First, the examples.
Probably the best example is the Visible
mixin, which provides a thing with the necessary data and attributes to be seen.
module Visible
# initialization and such omitted for brevity
attr_accessor :bareShortDescription, :bareLongDescription, :hugeDescription
end
More interestingly, the Visible
module also provides all of the code needed to automatically load these values from XML data files (and save them back). This mixin is used for Mob
s and Dealie
s (items like things you'd pick up and carry) alike without modifications needed, despite how dissimilar they are.
I have modules called InputFilter
and OutputFilter
that can be applied to any class to advertise that they know how to process text typed by the user or printed to the user's screen, respectively.
Actually, Ruby uses mixins for this kind of thing pretty broadly. The Comparable
and Enumerable
mixins advertise that particular objects implement functionality needed to compare or enumerate themselves, respectively, plus they add a bunch of additional methods implemented for you.
The difference... is all inside.
I claim that this use of mixins is similar to the component-based abstraction. In both cases, the objects store some state that is sequestered wholly inside some other class or functionality. In both cases, other objects would interact with this object through some abstraction that hides the nature of the containing class--either finding the component within the object to access or just accessing the mixin's functions directly.
Which technique you use doesn't seem too important. The thing that matters most is discipline and convention. If you want to reap the benefits of these techniques, you need to follow through with consistency better than I've done. I have a lot of places where state and functionality is given to a class rather than a mixin of a class.
For example, a Mob
has a lifeState
member, which tells if it's alive. But what if I want an animate sword that can be "killed?" I should put this functionality into, say, a Living
module. Or a really good example: there are containers, which can contain other dealies, and there are mobs, which can also hold objects in their inventory, but they're handled differently. Instead, I could make a Container
mixin and treat them the same.
There's a lot of work I could do to basically be more discliplined. I can't really promise I'll get to it, but I may in places it makes sense.