kikito / middleclass

Object-orientation for Lua
https://github.com/kikito/middleclass
MIT License
1.77k stars 190 forks source link

Added the ability to include mixins into instances. #11

Closed mebens closed 13 years ago

mebens commented 13 years ago

Today I was in need of being able to include mixins into instances, and not just classes. For example, I made a mixin to cut down my inheritance, but because I want only certain instances of a class to have the abilities of this mixin, (at present) I would have to create a new class anyhow. So I've changed it so that instances can include mixins.

A source of inspiration is Ruby with its class << x syntax. For example:

i = MyClass.new

class << i
  def another_method
    -- blah
  end

  -- in here we can add to just the instance 'i'
end

One thing I'm wondering about is, should included be called for instances and classes alike? Should there be a separate method for instances? What's your thoughts?

kikito commented 13 years ago

You can differentiate between instances and classes by doing instanceOf(Object) for instances and subclassOf(Object) for classes, so in the theory, you can have a mixin that detects whether it is being included by an instance or by a class, inside the included callback with a couple ifs.

However, I don't see any advantages in including a mixin in an instance.

In ruby it makes sense because the object model is way more complex than in middleclass. Classes themselves are classes - you need a way to add stuff to instances, because underneath the surface, that's what happens when you define a regular method. Or something along those lines.

Say that the mixin that you want is called "Mixin" and the "class without mixins" is called MyClass.

If you need to add stuff to some instances right after creating them, then in reality you have two classes:

Mixin = { ... }
MyClass = class('MyClass')
MyClassWithMixin = class('MyClassWithMixin', MyClass):include(Mixin)

local normalInstance = MyClass:new()
local instanceWithMixin = MyClassWithMixin:new()

On the other hand, if you need the instances to have certain methods only on certain occasions, then that blends very well with Stateful:

Mixin = { ... }
MyClass = class('MyClass'):include(Stateful)
local MixedIn = MyClass:addState('MixedIn') -- MixedIn is a state of MyClass
MixedIn:include(Mixin) -- MixedIn is a regular class. It can include mix-ins!

local instance = MyClass:new()
instance:gotoState('MixedIn') -- now instance has the methods of Mixin
instance:gotoState(nil) -- instance doesn't have the Mixin methods any more

You can even use enterState and exitState as "included" and "desincluded" callbacks for the instance if you feel like it.

I think that for now I will not merge this commit. It makes the interface less obvious, in exchange for a functionality for which there are alternatives IMHO.

But I'll keep it in mind if I ever want to implement a "HighClass" object model, with all the ruby rings and bells. In that case it will be completely necessary.

Thanks for submitting it anyway, I appreciate your contributions.

mebens commented 13 years ago

But I thought inheritance should be avoided in preference for composition. In the first alternative you presented that's just adding to the inheritance chain (which, as I've found, can cause problems). I think of it like this:

michael = Person:new()
michael:include(SuperChargedBrain)
michael:include(BadHealth)

john = Person:new()
john:include(RelaxedManner)

This is much easier than created the classes PersonWithSuperChargedBrainAndBadHealth, PersonWithARelaxedManner, PersonWithSuperChargedBrainAndARelaxedManner, and so on. Sure, we could use Sateful, but this is not good because the class requires that a state be added for each mixin. I think it's much more intuitive to allow instances to have mixins.

I'm sure they're ways to do this, like Stateful, but I'm thinking largely of intuition here. I was thinking to myself "Oh man, I wish I could move the functions of a mixin into one of these instances", and then I immediately thought "Yeah, include for instances, that'd be cool." I think it's the most elagent way of achieving it, plus it doesn't change anything in the current API.

kikito commented 13 years ago

But.. that is no composition. Composition is this:

michael = Person:new()
michael.brain = SuperChargedBrain:new()
michael.health = BadHealth:new()

john = Person:new()
john.manner = RelaxedManner:new()

By cramping the Brain, Health and Manner functions inside Person, I think you would violate the Single Responsibility Principle (by the way, give a look at the SOLID principles, they are cool stuff).

mebens commented 13 years ago

Oh, I must not understand composition, lol. So you're right. But that aside, I still find instance:include intuitive, and doesn't change anything in the current API. But, if you don't, then that's fine. :)