I'll try an example, taken from my current project which has both good and bad examples
Much work remaining to be done. However, one good example in it is the way the container related components work together to reduce the overall number of specialized classes. I have a need for at least three types of containers working with the same (or very similar) family of objects. Each type manages its contents in a slightly yet importantly different way.
The base class for the container half of the components involved is the Container class, its interface is:
Code: Select all
local Container = class(Component)
function Container:init(owner, container_name)
function Container:addObject(object)
function Container:insertFirst(object)
function Container:removeObject(object)
function Container:clearAll()
function Container:asList()
function Container:serialize()
function Container:deserialize(sdata, factory)
function Container:onDeserialize(object)
Note that I'm making no attempt to be a 'full-featured' class here, just defining and implementing what I need for current requirements. The container class manages a simple list. Don't worry about the Component class yet its mainly there for syntactic sugar in attaching components to 'real' objects. Looking at the init function you can see how it attaches, when I use a raw Container component to add list management to an object I just do:
Code: Select all
local someObj...
Container(someObj, 'name_of_the_list_member_variable_to_create_and_manage')
I can add multiple Container instances to a single class. This comes in handy when dealing with the two derived classes: Inventory and EquippableGear used with my Avatar class (mobile actor or entity).
The Inventory class is intended to be used with objects that have an Item component. In this case my Inventory class has a restriction of only 26 items maximum. So it redefines the addObject method to enforce that limit. The EquippableGear class is more involved. For one thing it uses an associative array instead of a list as the storage, yet since every piece of Equipment (a component class derived from Item) can be in an Inventory or can be equipped, it still makes sense to derive it from Component. For one thing it allows reuse of the serialize method but it helps in other ways as well.
Code: Select all
local EquippedGear = class(Container)
function EquippedGear:init(owner)
function EquippedGear:_slot_check(slot)
function EquippedGear:addObject(gear)
function EquippedGear:removeObject(gear)
function EquippedGear:swapHands()
function EquippedGear:asList()
function EquippedGear:onDeserialize(object)
The last two pieces of this component family are the components that attach to and enhance individual items, helping them to interface with the containers. The Item component class, derives from Component, once again primarily for syntactic sugar and a bit of DRY-ness. Its interface looks like this:
Code: Select all
local Item = class(Component)
function Item:init(owner, name, use_func)
function Item:setContainer(container)
function Item:drop()
function Item:use()
-- an example of a use_func:
function Item:heal()
When Item's use function is called it performs whatever operation the use_func(tion) implements. If the use_func is set to Item.heal then when the item is use'd the Avatar owning the collection the Item is in will be healed by some amount. A use_func can be used in other ways, for Equipment items, it removes them from one container and places them in another. Equipment class looks like:
Code: Select all
local Equipment = class(Item)
Equipment.VALID_SLOTS = {
two_handed = true,
either_hand = true,
primary_hand = true,
secondary_hand = true,
armor = true
}
function Equipment:init(owner, name, slot)
function Equipment:_toggle_equipped()
I left the VALID_SLOTS table in the above just to better show how the items are associated. In the case of a piece of Equipment the use_func doesn't have to be specified as it is set by the initializer to the _toggle_equipped method. There are similar component approaches to providing additional behaviors to Avatars, including such things as AI, combat capabilities, even temporary attachables which modify the attached object for the duration are quite possible.
Each of the above component classes has a small and well defined public interface and is easily unit testable. The number of possible name conflicts is reduced by a factor of five or so, and the implementation code is very DRY (Don't Repeat Yourself).
For a bad example, see my underground.model class in Snapshot - it has way too many methods and roles still even though I've refactored some out. In fact, if it weren't for some longer term project goals, the model class in Snapshot would be a prime example of something that might well should instead be a module.
Hope this helps,
Omnivore