> The only thing I can think of at the moment, is that the Context
> depends explicitly on specific data types (Book and Till).
Restricted OO.
> Good cryptic reply. Do you fancy expanding on this?
Probably not. I was building on the fact that you announced that you now understood Restricted OO. Your antecedent mail gave an example of it.
> Instead of team inheritance use role
> inheritance: an abstract role Product as before followed by these
> specializations:
DCI disallows this. It destroys the readability of code. You're back to the old problems with polymorphism. If you have role inheritance, why even bother separating out the roles? Just go ahead and use Unconstrained OO.
I find it much better to separate the design into three roles. Inject whatever roles need to be injected into the object. You would inject both the "base role" and one (or more) of the derived roles into the object. This ensures, among other things, that there is a single copy of the "base" role in the object.
I think it's a tossup on code readability and predictability, though there could be interesting discussions around this issue.
Miaow.
> Do you still see any
> problems, difficulties?
This makes it even worse, because the implementation (reference rather than composition) makes it difficult to reconcile scope bindings (which must be dynamic) and therefore more difficult to flag problems.
> If it were OT/C we *might* run into that kind of problem
Can one use "might" in an OOPSLA paper?
Underwear hanging out.
> If you prefer a discussion about split-objects you're invited to
> present an example that is specific enough so that it can be either
> verified or falsified by others.
>
> I'm open to anything -- except yelling at each other, but then I may
> be over-interpreting your strong words, I'm not a native English
> speaker.
Probably. We can go to a neutral third language if you like, such as French or Danish. Or how about Ruby? Translate this to Java using wrappers and I think you'll have your wish for our discussion.
#!/usr/bin/env ruby
# Example in Ruby – Dijkstra's algorithm in a programming style
# Modified and simplified for a Manhattan geometry with 3 roles
#
#
# Demonstrates an example where:
# - objects of class Node play several roles simultaneously
# (albeit spread across Contexts: a Node can
# play the CurrentIntersection in one Context and an Eastern or Southern
# Neighbor in another)
# - stacked Contexts (to implement recursion)
# - mixed access of objects of Node through different
# paths of role elaboration (the root is just a node,
# whereas others play roles)
# - there is a significant pre-existing data structure called
# a Map which contains the objects of instance. Where DCI
# comes in is to ascribe roles to those objects and let
# them interact with each other to evaluate the minimal
# path through the network
# - true to core DCI we are almost always concerned about
# what happens between the objects (paths and distance)
# rather than in the objects themselves (which have
# relatively uninteresting properties like "name")
# - equality of nodes is not identity, and several
# nodes compare equal with each other by standard
# equality (eql?)
# - returns references to the original data objects
# in a vector, to describe the resulting path
#
# There are some curiousities
# - EastNeighbor and SouthNeighbor were typographically equivalent,
# so I folded them into a single role: Neighbor. That type still
# serves the two original roles
# - Roles are truly scoped to the use case context
# - The Map and Distance_labeled_graph_node roles have to be duplicated in two Contexts
# - Node inheritance is replaced by injecting two roles
# into the object
# - Injecting some roles causes data objects to take on new
# data members. I can work around this by keeping the data
# in a separate associative vector, but this seems the
# right "Ruby way"
# - There is an intentional call to distance_between while the
# Context is still extant, but outside the scope of the
# Context itself. Should that be legal?
# Global boilerplate
Pair = Struct.new(:from, :to)
def infinity; return (2**(0.size * 8 -2) -1) end
module ContextAccessor
def context
Thread.current[:context]
end
def context=(ctx)
Thread.current[:context] = ctx
end
def execute_in_context
old_context = self.context
self.context = self
yield
self.context = old_context
end
end
#
# Consider street corners on a Manhattan grid. We want to find the
# minimal path from the most northeast city to the most
# southeast city. Use Dijstra's algorithm
#
# Data class
class Node
attr_reader :name
def initialize(n); @name = n end
def eql? (another_node)
# Nodes are == equal if they have the same name
return name == another_node.name
end
def == (another_node)
# Equality used in the Map algorithms is object identity
super
end
end
# This is the main Context for shortest path calculation
class CalculateShortestPath
# Housekeeping crap
include ContextAccessor
# These are handles to internal housekeeping arrays set up in initialize
def unvisited; @unvisited end
def pathTo; @pathTo end
def east_neighbor; @east_neighbor end
def south_neighbor; @south_neighbor end
def path; @path end
def map; @map end
def current; @current end
def destination; @destination end
# Initialization
def rebind(origin_node, geometries)
@current = origin_node
@map = geometries
@map.extend Map
@current.extend CurrentIntersection
@east_neighbor = map.east_neighbor_of(origin_node)
geometries.nodes.each {
|node|
node.extend Distance_labeled_graph_node
}
if @east_neighbor != nil
@east_neighbor.extend Neighbor
end
@south_neighbor = map.south_neighborOf(origin_node)
if @south_neighbor != nil
@south_neighbor.extend Neighbor
end
end
# public initialize. It's overloaded so that the public version doesn't
# have to pass a lot of crap; the initialize method takes care of
# setting up internal data structures on the first invocation. On
# recursion we override the defaults
def initialize(origin_node, target_node, geometries, path_vector = nil, unvisited_hash = nil, pathto_hash = nil)
@destination = target_node
rebind(origin_node, geometries)
# This has to come after rebind is done
if path_vector.nil?
# This is the fundamental data structure for Dijkstra's algorithm, called
# "Q" in the Wikipedia description. It is a boolean hash that maps a
# node onto false or true according to whether it has been visited
@unvisited = Hash.new
# These initializations are directly from the description of the algorithm
geometries.nodes.each { |node| @unvisited[node] = true }
@unvisited.delete(origin_node)
map.nodes.each { |node| node.set_tentative_distance_to(infinity) }
origin_node.set_tentative_distance_to(0)
# The path array is kept in the outermost context and serves to store the
# return path. Each recurring context may add something to the array along
# the way. However, because of the nature of the algorithm, individual
# Context instances don't deliver "partial paths" as partial answers.
@path = Array.new
# The pathTo map is a local associative array that remembers the
# arrows between nodes through the array and erases them if we
# re-label a node with a shorter distance
@pathTo = Hash.new
else
@unvisited = unvisited_hash
@path = path_vector
@pathTo = pathto_hash
end
execute
end
# There are four roles in the algorithm:
#
# CurrentIntersection (@current)
# EastNeighbor, which lies DIRECTLY to the east of CurrentIntersection (@east_neighbor)
# SouthernNeighbor, which is DIRECTLy to its south (@south_neighbor)
# Destination, the target node (@destination)
#
# We also add a role of Map (@map) as the oracle for the geometry
#
# The algorithm is straight from Wikipedia:
#
# http://en.wikipedia.org/wiki/Dijkstra's_algorithm
#
# and reads directly from the distance method, below
module Distance_labeled_graph_node
# NOTE: This role creates a new data member in the node into
# which it is injected. An alernative implementation would
# be to use a separate associative array
def tentative_distance; @tentative_distance_value end
def set_tentative_distance_to(x); @tentative_distance_value = x end
end
module CurrentIntersection
# Access to roles and other Context data
include ContextAccessor
def unvisited; context.unvisited end
def south_neighbor; context.south_neighbor end
def east_neighbor; context.east_neighbor end
def unvisited_neighbors
retval = Array.new
if south_neighbor != nil
if unvisited[south_neighbor] == true; retval << south_neighbor end
end
if east_neighbor != nil
if unvisited[east_neighbor] == true; retval << east_neighbor end
end
return retval
end
end
# This module serves to provide the methods both for the east_neighbor and south_neighbor roles
module Neighbor
include ContextAccessor
def relable_node_as(x)
if x < self.tentative_distance; self.set_tentative_distance_to(x); return true
else return false end
end
end
# "Map" as in cartography rather than Computer Science...
#
# Map is technically a role from the DCI perspective. The role
# in this example is played by an object representing a particular
# Manhattan geometry
module Map
include ContextAccessor
def distance_between(a, b)
return @distances[Pair.new(a, b)]
end
# These two functions presume always travelling
# in a southern or easterly direction
def next_down_the_street_from(x)
return east_neighbor_of(x)
end
def next_along_the_avenue_from(x)
return south_neighborOf(x)
end
end
# This is the method that does the work. Called from initialize
def execute
execute_in_context do
# Calculate tentative distances of unvisited neighbors
unvisited_neighbors = current.unvisited_neighbors
if unvisited_neighbors != nil
unvisited_neighbors.each { |neighbor|
if neighbor.relable_node_as(current.tentative_distance + map.distance_between(current, neighbor))
pathTo[neighbor] = current
end
}
end
unvisited.delete(current)
# Are we done?
if unvisited.size == 0
save_path(@path)
else
# The next current node is the one with the least distance in the
# unvisited set
selection = nearest_unvisited_node_to_target
# Recur
CalculateShortestPath.new(selection, destination, map, path, unvisited, pathTo)
end
end
end
def nearest_unvisited_node_to_target
min = infinity
selection = nil
unvisited.each_key { |intersection|
if unvisited[intersection]
if intersection.tentative_distance < min
min = intersection.tentative_distance
selection = intersection
end
end
}
return selection
end
def each
path.each { |node| yield node }
end
# This method does a simple traversal of the data structures (following pathTo)
# to build the directed traversal vector for the minimum path
def save_path(pathVector)
node = destination
begin
pathVector << node
node = pathTo[node]
end while node != nil
end
end
# This is the main Context for shortest distance calculation
class CalculateShortestDistance
include ContextAccessor
def path; return @path end
module Map
include ContextAccessor
def distance_between(a, b)
return @distances[Pair.new(a, b)]
end
# These two functions presume always travelling
# in a southern or easterly direction
def next_down_the_street_from(x)
return east_neighbor_of(x)
end
def next_along_the_avenue_from(x)
return south_neighborOf(x)
end
end
module Distance_labeled_graph_node
#NOTE: This creates a new data member in the node
def tentative_distance; @tentative_distance_value end
def set_tentative_distance_to(x); @tentative_distance_value = x end
end
def rebind(origin_node, geometries)
@current = origin_node
@destination = geometries.destination
@map = geometries
@map.extend Map
@map.nodes.each {
|node|
node.extend Distance_labeled_graph_node
}
end
def initialize(origin_node, target_node, geometries)
rebind(origin_node, geometries)
@current.set_tentative_distance_to(0)
@path = CalculateShortestPath.new(@current, @destination, @map).path
end
def distance
retval = 0
previous_node = nil
path.reverse_each {
|node|
if previous_node.nil?
retval = 0
else
retval += @map.distance_between(previous_node, node)
end
previous_node = node
}
return retval
end
end
class ManhattanGeometry1
def initialize
@nodes = Array.new
@distances = Hash.new
names = [ "a", "b", "c", "d", "a", "b", "g", "h", "i"]
3.times { |i|
3.times { |j| @nodes << Node.new(names[(i*3)+j]) }
}
# Aliases to help set up the grid. Grid is of Manhattan form:
#
# a - 2 - b - 3 - c
# | | |
# 1 2 1
# | | |
# d - 1 - e - 1 - f
# | |
# 2 4
# | |
# g - 1 - h - 2 - i
#
@a = @nodes[0]
@b = @nodes[1]
@c = @nodes[2]
@d = @nodes[3]
@e = @nodes[4]
@f = @nodes[5]
@g = @nodes[6]
@h = @nodes[7]
@i = @nodes[8]
9.times { |i|
9.times { |j|
@distances[Pair.new(@nodes[i], @nodes[j])] = infinity
}
}
@distances[Pair.new(@a, @b)] = 2
@distances[Pair.new(@b, @c)] = 3
@distances[Pair.new(@c, @f)] = 1
@distances[Pair.new(@f, @i)] = 4
@distances[Pair.new(@b, @e)] = 2
@distances[Pair.new(@e, @f)] = 1
@distances[Pair.new(@a, @d)] = 1
@distances[Pair.new(@d, @g)] = 2
@distances[Pair.new(@g, @h)] = 1
@distances[Pair.new(@h, @i)] = 2
@distances[Pair.new(@d, @e)] = 1
@distances.freeze
@next_down_the_street_from = Hash.new
@next_down_the_street_from[@a] = @b
@next_down_the_street_from[@b] = @c
@next_down_the_street_from[@d] = @e
@next_down_the_street_from[@e] = @f
@next_down_the_street_from[@g] = @h
@next_down_the_street_from[@h] = @i
@next_down_the_street_from.freeze
@next_along_the_avenue_from = Hash.new
@next_along_the_avenue_from[@a] = @d
@next_along_the_avenue_from[@b] = @e
@next_along_the_avenue_from[@c] = @f
@next_along_the_avenue_from[@d] = @g
@next_along_the_avenue_from[@f] = @i
@next_along_the_avenue_from.freeze
end
def east_neighbor_of(a); @next_down_the_street_from[a] end
def south_neighborOf(a); @next_along_the_avenue_from[a] end
def root; return @a end
def destination; return @i end
def nodes; return @nodes end
end
class ManhattanGeometry2
def initialize
@nodes = Array.new
@distances = Hash.new
names = [ "a", "b", "c", "d", "a", "b", "g", "h", "i", "j", "k"]
11.times { |j| @nodes << Node.new(names[j]) }
# Aliases to help set up the grid. Grid is of Manhattan form:
#
# a - 2 - b - 3 - c - 1 - j
# | | | |
# 1 2 1 |
# | | | |
# d - 1 - e - 1 - f 1
# | | |
# 2 4 |
# | | |
# g - 1 - h - 2 - i - 2 - k
#
@a = @nodes[0]
@b = @nodes[1]
@c = @nodes[2]
@d = @nodes[3]
@e = @nodes[4]
@f = @nodes[5]
@g = @nodes[6]
@h = @nodes[7]
@i = @nodes[8]
@j = @nodes[9]
@k = @nodes[10]
11.times { |i|
11.times { |j|
@distances[Pair.new(@nodes[i], @nodes[j])] = infinity
}
}
@distances[Pair.new(@a, @b)] = 2
@distances[Pair.new(@b, @c)] = 3
@distances[Pair.new(@c, @f)] = 1
@distances[Pair.new(@f, @i)] = 4
@distances[Pair.new(@b, @e)] = 2
@distances[Pair.new(@e, @f)] = 1
@distances[Pair.new(@a, @d)] = 1
@distances[Pair.new(@d, @g)] = 2
@distances[Pair.new(@g, @h)] = 1
@distances[Pair.new(@h, @i)] = 2
@distances[Pair.new(@d, @e)] = 1
@distances[Pair.new(@c, @j)] = 1
@distances[Pair.new(@j, @k)] = 1
@distances[Pair.new(@i, @k)] = 2
@distances.freeze
@next_down_the_street_from = Hash.new
@next_down_the_street_from[@a] = @b
@next_down_the_street_from[@b] = @c
@next_down_the_street_from[@c] = @j
@next_down_the_street_from[@d] = @e
@next_down_the_street_from[@e] = @f
@next_down_the_street_from[@g] = @h
@next_down_the_street_from[@h] = @i
@next_down_the_street_from[@i] = @k
@next_down_the_street_from.freeze
@next_along_the_avenue_from = Hash.new
@next_along_the_avenue_from[@a] = @d
@next_along_the_avenue_from[@b] = @e
@next_along_the_avenue_from[@c] = @f
@next_along_the_avenue_from[@d] = @g
@next_along_the_avenue_from[@f] = @i
@next_along_the_avenue_from[@j] = @k
@next_along_the_avenue_from.freeze
end
def east_neighbor_of(a); @next_down_the_street_from[a] end
def south_neighborOf(a); @next_along_the_avenue_from[a] end
def root; return @a end
def destination; return @k end
def nodes; return @nodes end
end
# Test drivers
geometries = ManhattanGeometry1.new
path = CalculateShortestPath.new(geometries.root, geometries.destination, geometries)
print "Path is: "; path.each {|node| print "#{node.name} " }; print "\n"
puts "distance is #{CalculateShortestDistance.new(geometries.root, geometries.destination, geometries).distance}"
puts("")
geometries = ManhattanGeometry2.new
path = CalculateShortestPath.new(geometries.root, geometries.destination, geometries)
print "Path is: "
last_node = nil
path.each {
|node|
if last_node != nil; print " - #{geometries.distance_between(node, last_node)} - " end
print "#{node.name}"
last_node = node
};
print "\n"
puts "distance is #{CalculateShortestDistance.new(geometries.root, geometries.destination, geometries).distance}"
The explanation would be a bit long and arcane without some prior knowledge about blocks — which I'm guessing that you don't have, or you probably would have been doing something like this before.
In brief, it executes the block that was passed to execute_in_context as an argument. So:
execute_in_context { print " b c " }
def execute_in_context
print "a"
yield
print "d\n."
}
prints:
a b c d
.
More practically, you can ignore the code, and just know that it is used for stacking contexts. Replace this with whatever other context-stacking mechanism you are using.
I'll be thrilled to see the Java implementation. There are implementations going forward in Smalltalk and C++ in parallel as well. With your Java implementation representing the OT/J camp, we'll have three implementations to compare and discuss — CONCRETELY, for a change.
I consider the Ruby implementation to be sexy, yes. The C++ implementation is something only a mother could love and, well, I'm kind of like that as a C++ person. I haven't seen the Smalltalk implementation yet.
[ThreadStatic] public static Context {get; private set;}
ExecuteInContext (Action act) {
oldContext = Context;
Context = this;
try {
a();
} finally {
Context = oldContext;
}
}
> -----Original Message-----
> From: object-co...@googlegroups.com [mailto:object-
> compo...@googlegroups.com] On Behalf Of James O. Coplien
> Sent: Tuesday, August 30, 2011 9:42 PM
> To: object-co...@googlegroups.com
> Subject: Re: Roles in Eclipse Indigo
>
> Ant,
>
> The explanation would be a bit long and arcane without some prior
> knowledge about blocks - which I'm guessing that you don't have, or you
> probably would have been doing something like this before.
>
> In brief, it executes the block that was passed to execute_in_context
> as an argument. So:
>
> execute_in_context { print " b c " }
>
> def execute_in_context
> print "a"
> yield
> print "d\n."
> }
>
> prints:
>
> a b c d
> .
>
> More practically, you can ignore the code, and just know that it is
> used for stacking contexts. Replace this with whatever other context-
> stacking mechanism you are using.
>
> I'll be thrilled to see the Java implementation. There are
> implementations going forward in Smalltalk and C++ in parallel as well.
> With your Java implementation representing the OT/J camp, we'll have
> three implementations to compare and discuss - CONCRETELY, for a
> --
> You received this message because you are subscribed to the Google
> Groups "object-composition" group.
> To post to this group, send email to object-
> compo...@googlegroups.com.
> To unsubscribe from this group, send email to object-
> composition...@googlegroups.com.
> For more options, visit this group at
> http://groups.google.com/group/object-composition?hl=en.
I worked 20 years on systems with seven 9s, and in any case exception handling is a toy with respect to code safety, reliability, and robustness. It was put into C++ only as a political chit to keep the library vendors happy and to keep them from having to interwork directly — they made it the language's problem. From there the same bad design propagated into other languages for sake of familiarity. Good reliability and fault tolerance have to be customized to the application and platform where they are being used. I don't know how to do that in a generic example.
In your code below, how do you know that returning to the old Context doesn't leave databases locked, or a missile on a mistaken trajectory? Error recovery is hard and doesn't come down to casually putting in a few catches and throws here and there. In a high-availability telecom system, such measures amount to 50% to 80% of the code mass.
Agreed
> I worked 20 years on systems with seven 9s, and in any case exception
> handling is a toy with respect to code safety, reliability, and
> robustness. It was put into C++ only as a political chit to keep the
> library vendors happy and to keep them from having to interwork
> directly - they made it the language's problem. From there the same bad
> design propagated into other languages for sake of familiarity. Good
> reliability and fault tolerance have to be customized to the
> application and platform where they are being used. I don't know how to
> do that in a generic example.
Agreed too. Real fault tolerance is hard. But exception handling gets you quite far, enough for most business apps.
> In your code below, how do you know that returning to the old Context
> doesn't leave databases locked, or a missile on a mistaken trajectory?
I don't. I only make sure that MY code (execute_with_context) will always do the right thing. Whether it's a good idea to catch the exception and continue is someone else's decision entirely (caller of execute_with_context). They might know a thing or two about the exception they're catching that I don't, or about the apps architecture.
Furthermore, I know that correct code always closes its DB connections. Sometimes I might also know about other side effects, e.g. when I'm using transactions - the missile might be launched but not committed.
> Error recovery is hard and doesn't come down to casually putting in a
> few catches and throws here and there. In a high-availability telecom
> system, such measures amount to 50% to 80% of the code mass.
Sure, errors in return values, checking every single one. But the rest of the world needs to get stuff done too, and experience tells us that we often get good enough recoverability using just a few lines of exception handling code. Your typical C# code uses few throws and catches, but lots of usings (try-with-ressources) and some try/finally blocks. This works quite OK and there's not really an alternative to it in many situations.
So frameworks need to handle this.
>
>
>
> On Aug 31, 2011, at 9:24 , Wenig, Stefan wrote:
>
> > Technical nitpicking: isn't execute_in_context missing some exception
> handling?
> > I'm not fluent in Ruby, in C# I'd write
> >
> > [ThreadStatic] public static Context {get; private set;}
> >
> > ExecuteInContext (Action act) {
> > oldContext = Context;
> > Context = this;
> > try {
> > a();
> > } finally {
> > Context = oldContext;
> > }
> > }
>
> - the missile might be launched but not committed.
Heh, that one put a grin on my face.
"Well, yes sir, we… ahhhm… kind of launched the missile… What? The others have launched their nukes at us? But… but… we didn't commit the missile yet, so that doesn't count, right? …Sir?"
Thanks for making my morning :-)
Transactional life would be much easier in many ways. Can't stop global warming anymore? Let's pick a safepoint and start over!
> --
> You received this message because you are subscribed to the Google Groups "object-composition" group.
> To post to this group, send email to object-co...@googlegroups.com.
> To unsubscribe from this group, send email to object-composit...@googlegroups.com.
> That could by why Stephan chose to merge the
> Neighbour and DistanceLabeledGraphNode.
Just could be...
> But, this has little to do with whether we use wrappers or not, and a
> lot more to so with statically typed vs. dynamically typed languages.
> Nonetheless, I shall continue, and post my solution shortly.
Aha. I knew this comment would come up.
There are two ways of approaching DCI. One is to look at its principles and to fit as many of them as possible into a current language, as unsuitable as it may be. The other is to look at how to design a language that meets DCI's objectives. There are several people at each end of this spectrum.
More interesting, I think, is the engineering space in the middle: to find a language that is suitable *enough* while still not compromising DCI principles. We'll see what Stefan comes up with in response to my request to be faithful to the original design, but my hunch is the same as yours with respect to his changes to the design. I'd be surprised if you can use that technology and be faithful to the DCI principles. Now, mind you, I'd love to be surprised, but I've explored this space enough that it will be a *really* big surprise...
I don't want to constrain DCI because someone's language doesn't accommodate it, and I don't want to trim DCI or compromise it for the sake of fitting the language du jour. We have to be honest about the caveats rather than skirt them. And we need to mitigate them when we can — by evolving the base language, by moving to a language that better supports the design goals of a given project, and so forth. I wouldn't choose Java at this point if DCI were important to me unless I was willing to invest in Qi4j (a pretty cheap investment :-) ). I believe that those who have a gun to their head to use Java, use DCI at their peril, and at the ultimate peril of those holding the gun.
Again, I'd love to be surprised...
> @Stephan: merging the roles implementations isn't satisfactory to me,
> because the role implementations should be seperate, in order to match
> the mental model.
Agreed. And even if it doesn't match your particular mental model in this case, the general principle at stake here is that there should be no limitation about how many roles a given object can play, and there should be only reasonable limitations on how those roles can invoke each other (e.g., method name collision concerns).
> My problems are related to having one object play several roles at one
> time, and not having the correct reference type when I need it.
Your current post is one of the more intriguing mails I've seen on this list because I feel there's an insight here into a different twist on the implementation. But the above paragraph worries me. It also leaves me hungry because I can't understand in enough detail what you mean. Say more. Show code. You said you'd post your solution shortly and I'm eager to see it. Let's see if we can work this particular issue.
Should we move this to DCI evolution? Ant, it's your thread ...
No, it is not a bug. It is intentional. The corners are named after the names of the firms that have the main anchor stores on the corresponding corner. The "a" stands for MacDonald's.
Why did I do this? In can show up a bug in any implementation that confuses shallow equality, deep equality, and identity. DCI depends strongly on object identity and while it must support shallow and deep quality for the sake of the programmer, DCI itself doesn't depend on it. If someone has a "DCI" framework that depends on equality instead of identity, their implementation won't work. This data structure is there to explicitly elicit that bug.
There are more Easter Eggs. Beware. This code is not what it appears to be, and even if you get it to run and print results, check them carefully...
> Yes, but it doesn't have to be, as you will see when I post my
> solution.
Go for it.
> If you think creating an interface to both roles is illegal, then you
> are declaring that statically typed languages cannot do DCI.
Sorry for the confusion. I don't think it's illegal, and I have a perfectly good implementation of Dijkstra running in C++ as well, thank-you :-)
Mvh
Rune
> The reason I havent posted my code yet, is that it is printing 0 as
> the distance ;-)
>
> I have been thinking about identity and equality, and how I use them
> in day to day life. Since we send object from clients to servers and
> back again, we never ever use the Java == to test identity. We ALWAYS
> rely on the equals method and hashCode methods.
Right — the SOA approach, more or less, right? I am with you and think that I understand your rationalization.
There is a rich, rich set of questions that arise here. I am guessing that you really don't send the objects in your system. Objects have intelligence. Most SOA implementations (and all CORBA-based implementations) send only the object state. They don't send the data ("data" in the DCI is a representation that includes a minimal interpretation of the functionality: therefore, 32 bits could be either an integer or a floating point number, but the method contextualization makes it data). They don't send the role part — the .text of the methods.
My model, therefore, is that there is an object on each side of such a transaction (to pick a suitably selected word). It isn't that you send objects from clients to servers and back, but you provide the illusion by replicating a *copy* of the object on the receiving end. Think Pascal call-by-value. Students are usually taught call-by-value before call-by-reference (or, heaven forbid, call-by-name).
We explored these questions in depth when we built the first object broker back at Bell Labs in my department, circa 1982 or 1983. What a pain.
One reason this is confusing is that this is also the "ordinary" way that CS students are taught that compilers pass information between procedures: call-by-value, by putting a copy on the stack frame. CS students come out of their education with this mental model that a thing is equivalent to its name. Copying an object onto the stack frame, or onto another computer, results in a new object with a new name. The new name in the SOA case is qualified by the scope of the processor it sits on. The new name in Pascal is on the activation record of the receiving function.
In either case, if you ask if both objects are the same one, it takes an interesting perspective on the world to answer: "yes." They have separate identity. If you ask if they are equal, the answer is: it depends. Shallowly equal or deeply equal? That is, do they have the same graphs of references to other objects or not?
The goal in CORBA (or other RPC mechanisms) or SOA is to sometimes provide the *illusion* that it is the same object. Programmers are not under this illusion; they carefully have to make sure that a copy at one end is replicated at the other. This situation exists in other, more static settings such as telecom. The concept of a "phone call object" straddles two switching systems in a city-to-city call, and one must maintain the *illusion* that there is one call object. That is ridiculous, of course, if the call object has an identity (unless you put some really bizarre scenarios in place), so we have patterns like HOPP whose job it is to keep the endpoints in synchronization. That is all above the programmer's object model. It is on this side of the programmer's abstraction boundary.
Telecom has even more interesting problems here because of the need to synchronize the intelligence (the .text code) on all the machines that can touch that object's data. You can't shut down The Bell System to be able simultaneously to update all these machines, so there is some pretty deep magic for making this work — on the underside of the abstraction boundary.
The reason this becomes an interesting question is that we must consider where it lives in the end user mental model: what are their abstraction boundaries, and on what side of the abstraction boundary does this functionality lie? My experience is that it is worthwhile educating the business about the risks of such replication, because it takes the investment of transaction semantics (or something similar) to meet the integrity requirements of distributed financial systems. If you go for a large budget to "just send an object across the wires" you'll get funny looks.
And that, in turn, has repercussions in the applicability of DCI in those contexts. I don't see it fitting there. That isn't a slam. Distributed systems are real, and the problem of distributing true objects is hard. Many have tried and failed: Ontos and its kin back in the 1980s all tried and tended to fail pretty badly because it was so hard to make them work. That is not the problem that DCI sets out to solve.
That said, you can still use DCI in concert with patterns like Proxy in the same way we always have. DCI doesn't hide the fact that there are multiple objects any more or any less than Proxy does. It is crucial, as an architect, to separate the Proxy semantics from the DCI semantics. I think that part of the problem in some of our discussions here is that people have hopes of using DCI to do the hiding that Proxy does. Proxy is a "wink" that keeps it visible that there is a man behind the curtain who is talking to the Great and Powerful Oz, the latter being on this side of the abstraction boundary. In DCI, there is no curtain, and instead of one actor handing off to another, it's just one Wizard. No illusions.
The question in the end depends on where you draw your abstraction boundaries. You will draw them with different kinds of pens, and knives, and walls, of which DCI is one. And your mileage may vary. I think it's pretty clear which ones work where, but it's always a matter of fitting a new paradigm onto all worldviews no matter where you come from. DCI is different from just about everything else out there. The CORBA / SOA paradigm is just one of them and, yes, it shapes how you think about things — including DCI.
Good insight, Ant.
> In C++ do you use a mega-pimped interface containing methods of all
> the roles?
There are too many imprecise terms there for me to be able to answer...
The object, at run time, supports all the methods for all the roles it will play. That is a casualty of C++ technology, and it's extensively explored in the Lean Architecture book. It's kind of a pretense, though, since run-time C++ objects are just blobs of passive bits — unless you use virtual functions, in which case there is actually some run-time type information. However, I don't use virtual functions (certainly not in role methods!), because that kind of goes against one or two of the top three DCI principles related to code readability. So the object's ability to respond to the messages is in the mind of the programmer and must be arranged by good design. Of course, the compile-time type checking system helps a LOT.
(I learned a software engineering lesson while working on the Dijkstra example. The Ruby implementation was immediately runnable but it stumbled dozens of times before running. The nice thing about Ruby is that it was ALWAYS clear what was wrong and the fix was usually easy, but in any case the consequences were always clear. It took much, much longer before the C++ example ran the first time, and the compile-time typing took me into cycles: started with 120 bugs; get it down to about 10; then, one particular fix allowed the compiler to effectively go on to another pass and it went up to 120 again. I went through five or six such cycles. Maybe that's a way of figuring out how many compiler passes there are.)
But to answer from a DCI perspective:
- The roles are each self-contained, without reference to any other role,
except for use of other roles' methods
- The Node class is set up at compile time with all possible roles
pre-injected; however, there are only templates in the source, and no
classes. So what for a Smalltalk programmer is a class / object
dichotomy, for a C++ programmer is a template / class dichotomy;
thereafter, the objects are more or less just blobs of bits. The good
news is having the ability to reason more formally about the what-
the-system-is structure at compile time.
- In any given context of use of a Node object, the interfaces only
of the roles at hand are accessible and visible.
The code works but needs serious cleaning up — another casualty of the way that C++ language rules force certain modularization of templates. I'm going to radically re-factor it (or at least try) to make it more readable to a novice and will ultimately post it here.
cool - can you show some code?
let inline getName arg = ( ^a : (member Name : string) arg)
The above lets you call getName with anything that has a instance property called 'Name' or if you wish to restrict on an operator you can do
let inline implicit arg = ( ^a : (static member op_Implicit : ^b -> ^a) arg)
Which then lets you call with any argument that supports an implicit conversion to the return type of the function. The return type is inferred by the compiler
you can restrict multiple arguments in the same manner. Both examples above are taken from
http://codebetter.com/matthewpodwysocki/2009/06/11/f-duck-typing-and-structural-typing/
/Rune
--
/Rune
> 4) Roles cannot contain state. James tried it out in his Ruby code,
> and so far as I can tell, wrappers can't cope with it.
None of my roles contain state. They do access some state of the object with which they are associated.
I called it Mega, partly because I have no idea what else to call it.
In a way, that is like asking Big Momma to recite lines from Big
Poppa's script, just because they are both being played by Eddie
Murphy... This is one of the reason's I really don't like dynamic
languages, and why I don't like them for DCI, because it is so easy to
confused what the object really is, and no IDE can help you figure it
out, unless you use the debugger.
To solve the problem that o is not equal to r, we simply modify how we
implemented the equals method in the original object. Sadly it builds
reliance on our framework into the "data" part of the system, but hey,
life isn't always peachy:
> So (a) we are not the only people facing these problems,
No, some of us have been familiar with them for more than 25 years.
> and (b) there
> are solutions.
Like DCI. But we have yet to see a reasonable, readable solution in Java that communicates design intent in terms of role-based mental models.
> Perhaps Java's RMI API is one reason why we have
> always been encouraged not to use the "==" operator in Java.
I think there is something much deeper going on here, and that it relates to the other problems with Java. I think they cut the reflection API at just the wrong place.
> @James: Have you looked at my solution with the mega interface and
> compared it line for line with your Ruby solution? What do you think
> - is object schizoprenia as big a deal as you have claimed?
Your example convinces me that it's even worse than I had communicated here earlier, because it highlights design articulation issues I had forgotten about from the last time I looked at this some decades ago. Please see my comments in a separate mail.
I think it comes more from being used to working with statically typed
languages - not sure though. The point is, I always have to think
about an object's type in order to use it.
But I guess I'm way of topic again kick an old can down the road:)
> James says I am thinking too class oriented and not enough object
> oriented. If that is true, it is because my language is forcing me to
> declare types.
No, it's because your language's type system puts the abstraction boundaries in the wrong place, and that gets in the way of separating concerns in a way that facilitates thinking about objects.
> If that is bad, then maybe, a requirement for DCI
> certification is that the language must not force programmers to
> declare types?
It works just fine in C++.
> DCI certification
*shudder*
> So I am not sure how my code loses the design information that
> "current" represents the current intersection, other than it has the
> type "Mega".
That is exactly my point.
It's a Java problem: my C++ code doesn't suffer from this problem.
> But I guess there are still a lot of unanswered
> questions lying around, for example, the best way to make behaviour
> usable in more than one context.
I think it's been answered both here on the list and in the Lean Arch book, where habits are presented as one solution.
But to ask to go back to basics, and to explore the rationale behind the taboo against role data members was not a rhetorical question. I haven't seen any discussion here about the code that is based on anything other than an unrationalized fear of data members in roles. How, exactly, do you fear this code will break? I know an answer and think I know what Trygve and I understand to be the principles here, but I'm curious if they're understood.
I don't want DCI to turn into a cargo cult phenomenon, and I'm already a bit worried about that seeing the attempts in Java and with wrappers.
On Sep 13, 2011, at 8:40 , Stephan Herrmann wrote:
> Perhaps I mean *strongly* typed?
Smalltalk is much more strongly typed than C++. There is much less ability to cast or to let an object or one type masquerade as another.
Perhaps you meant: Strongly hyped languages? :-)
vs duct taped languages :)
But to ask to go back to basics, and to explore the rationale behind the taboo against role data members was not a rhetorical question. I haven't seen any discussion here about the code that is based on anything other than an unrationalized fear of data members in roles. How, exactly, do you fear this code will break? I know an answer and think I know what Trygve and I understand to be the principles here, but I'm curious if they're understood.
I don't want DCI to turn into a cargo cult phenomenon, and I'm already a bit worried about that seeing the attempts in Java and with wrappers.