Hybrid DCI: DCI in Ruby using role wrappers AND injection

77 views
Skip to first unread message

rebo

unread,
Jan 11, 2013, 12:20:39 PM1/11/13
to object-co...@googlegroups.com
I think I finally have a reasonable working solution for DCI in ruby, and shock-horror it uses wrappers!

Cue everybody telling me wrappers are not DCI !! Well, I hope you will bear with me...

A quick overview of existing issues with DCI in ruby:

1)  #extend blows the method cache, uses injection only, and cannot be undone.  Quite far removed from DCI where players can only act as roles in a context. Injection means you cannot reliably call one role method over another when multiple roles are used.
2) Wrappers like SimpleDelgator have self schizophrenia issues. Wrapped objects can easily leak out of the context.

What i've done is combined both approaches to solve every issue above.

There is one caveat, and maybe this is too much to accept, but here it is anyway:

"When calling a role method, you need to pass the argument (:call) to the role identifier".

I.e. this is what a role method call looks like, for the "greeter" role.

    greeter(:call).say_hi

and this is what a role method looks like (pretty normal) :

  role :greeter do
    def say_hi
      #self here always refers to the role-player, there are no self-schizoprenia issues at all
      puts "Hello there! says #{self.name}"
    end
  end

Maybe this compromise is too much but I've tried to minimise any impact on the mental model of the user or context author.  

N.b (:call) will also call instance methods, but of-course prioritise the appropriate role method.

Here are some example files showing typical usage:


I've not put the source up yet because i'd like to hear your views on the (:call) compromise i've made.  I think it is about as minor as you can hope for in ruby to be honest.

Anyway let me know what you think.








rune funch

unread,
Jan 11, 2013, 12:26:59 PM1/11/13
to object-co...@googlegroups.com
Have you tried it out on dijkstra (Manhattan)? There's already a ruby implementation on fullOO.info so should take long I'd guess
--
You received this message because you are subscribed to the Google Groups "object-composition" group.
To view this discussion on the web visit https://groups.google.com/d/msg/object-composition/-/PWDhYK0mN5sJ.
To post to this group, send email to object-co...@googlegroups.com.
To unsubscribe from this group, send email to object-composit...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/object-composition?hl=en.

rebo

unread,
Jan 11, 2013, 12:29:14 PM1/11/13
to object-co...@googlegroups.com
I will definitely do that, I don't for-see any problems with manhattan DCI.  Have to go out soon, but will do it tonight as soon as I return.
To unsubscribe from this group, send email to object-composition+unsub...@googlegroups.com.

James O Coplien

unread,
Jan 12, 2013, 1:43:43 PM1/12/13
to object-co...@googlegroups.com

On Jan 11, 2013, at 7:29 , rebo wrote:

I will definitely do that, I don't for-see any problems with manhattan DCI.

I do. I await your results :-)

rebo

unread,
Jan 13, 2013, 12:00:52 PM1/13/13
to object-co...@googlegroups.com
I've implemented the Dijkstra example on the fullOO.info  site at : http://fulloo.info/Examples/RubyExamples/Dijkstra/dijkstra.rb getting identical results.  I've tried to match the implementation as faithfully as possible.

Path is: i h g d a 
distance is 6

Path is: k - 1 - j - 1 - c - 3 - b - 2 - a
distance is 7

In my opinion there are some significant issues with the ruby example on the fulloo site.  This mostly results out of the use of injection and #extend.  In the interests of matching the behaviour of the code on site I have duplicated this behaviour.  However I feel some of it runs contrary to what is now accepted DCI.

Here are my detailed notes:

1)  The first issue is that the example on the website uses the same role behaviour for multiple objects at the same time. I.e. anonymous roles.  This means I had to add this feature  to HybridDCI.  

On line 183 ....
|node| node.extend Distance_labeled_graph_node

This assigns the same role behaviour "Distance_labeled_graph_node" to many role-players.  This is done so that each node can call methods such as #set_tentative_distance_to etc.

To mimic this I need a way to call these role methods. This is simple when using injection and #extend because these methods are called first,  however this is not true to DCI principles because those role methods will always be available on the object inside and outside of the context.  

In previous posts by Rune, he says that a role method may be called by going through the role explicitly i..e RoleA.foo, or by self.foo within the context of a role's role methods.   As roles are not named  the only way to trigger a role method (other than injection) is via a method on the role_player itself.   Therefore to implement this I had to add the method #as_role to role players, this mimics injection and enables a role_method to be called even though the call does not go explicitly though a role.

2)  On lines 391 - 394, nodes inside an  "unvisited_neighbours" array are iterated over.  These nodes may either be a south_neighbor role or a east_neighbor role.   Regardless which role, the method #relable_node_as is sent to to these objects.  

This is a role method that is sent to an object without explicit reference to the role.

unvisited_neighbors.each {
     |neighbor|
     net_distance = current.tentative_distance + map.distance_between(current, neighbor)
        if neighbor.relable_node_as(net_distance) == :distance_was_udated
pathTo[neighbor] = current

The problem with this is that it is only by chance that this algorithm works.   It works because the #relable_node_as method contains the same code for both the south role and the east role.    If #relable_node_as was different for the south and east roles, the wrong method may be picked for two reasons.
    
(a) #extend does not re-order included modules, so #extend Moo; #extend Foo; #extend Moo, would never call Moo's role methods over Foo in the event of a clash, even though Moo was the last extended module.  This is because Moo was already extended first and repeated extensions do not have any effect.  So extend SouthNeighbor; extend EastNeighbor; extend SouthNeighbor. will never call SouthNeighbor's #relable_node_as method.  We are just fortunate these methods contain the same logic.

(b) Given we are not accessing the role method explicitly though a role, how do we know which role & role method to use?   It just so happens that these role_players have only one role assigned.  Therefore we can choose that role's method.  This is a by-product of injection only.  If those objects had multiple roles, again, which one to choose?

In order to copy the behaviour of the code on site, I choose to duplicate injection behaviour.  However my preferred solution would be to always have to explicitly call a role method via it's role.

If we want to allow role methods to be called outside of an explicit reference to a role, then we need to decide what order to check the existing roles in the case of multiple roles.

3) On lines 273, inside the #relable_node_as method, on the EastNeighbor role.  The method self.tentative_distance is called.  Now tentative_distance is not a role method of EastNeighbor, nor is it an instance method.  It is a role method of another role entirely.  It is a by-product of injection that the correct role-method gets called.

In the context of DCI how can we determine which role 's role method to call.  Rune suggests in another post that this type of call should only call an instance method, and I agree.  It is very confusing to try to figure out what role method is going to be called as there is no explicit reference to the role.  That said,  in order to mimic the code on site, I do a search on all existing roles applied to an object and select the last one that responds to that role method.  This mimics injection.   

I will remove this feature though, it's only present in order to match the fullinfo example.

4)  In the code on site, roles and role methods are referenced outside of a context.  On line 700 geometries.distance_between is called, outside of any context.

The only reason this works is because earlier within a context (on line 476) geometries was extended within this method.  This is role behaviour leakage.  The way I solved this was by moving the calculation back into a context, and calling that context explicitly to prevent role method leakage.

======

Thats generally it, other than that everything else was a fairly direct copy of the code on site.

The algorithm code is here:  https://gist.github.com/4525021

Im not ready to release the source to this HybridDCI just yet, as I want to resolve the issues (1 -3 ) above and improve performance first.  
Reply all
Reply to author
Forward
0 new messages