Puzzling OO design problem

Dirk Thierbach dthierbach at usenet.arcornews.de
Sat Apr 9 04:21:28 EDT 2005


George Sakkis <gsakkis at rutgers.edu> wrote:

> 1. There is a (single inheritance) hierarchy of domain classes, say
> A<-B<-..<-Z (arrows point to the parent in the inheritance tree).
> 2. This hierarchy evolved over time to different versions for each
> class. So for example, version's 1 hierarchy would be A_v1 <-B_v1
> <-..<-Z_v1.
> 3. At runtime, only one version is selected by a factory function.

> Up to this point, the inheritance graph would be the following:
> 
> A <- A_V1 ... <- A_Vn
> ^     ^           ^
> |     |           |
> B <- B_V1 ... <- B_Vn
> .     .           .
> .     .           .
> .     .           .
> ^     ^           ^
> |     |           |
> Z <- Z_V1 ... <- Z_Vn

Interesting problem.

> This could be implemented either with multiple inheritance (e.g.
> B_V1(B,A_V1)) 

To help myself thinking about that, let's make a somewhat complicated
example, somewhere in the middle of the graph, with the possibility of
introducing a hole at B3. I also shorten A_Vn to An etc. Consider the
subgraph (with nonstandard annotations of method definition after the bar
(to save space) as explained below):

    A2 | f   <-   A3 | f
    ^             ^    
    |             |    
    B2       <-   B3   
    ^             ^    
    |             |    
    C2 | g   <-   C3 | h

Assume a method g that is present in C2 but not changed in C3. Now g
calls a method f, which is inherited unchanged in C2 from A2 (and not
changed in B2, B3 or C3, either), but changed in A3. Finally, let h
call g in C3. As here the "inheritance columns" have "priority", one
would expect then g to call f in A3, and not in A2, for example.

So what you need is that every method, even if not originating from
the "real" class, is looked up first in the column above the "real"
class, then in the column left to that, and so on.

Ok. Multiple inheritance can often select priority for conflicting
methods. If you can specify yhat tou want "column priority" for
each class, you're fine.

> or using the bridge design pattern |Z| times, one per each row.

When I read your description above, I also thought immediately
"bridge pattern", but then I tried to write down details,
and got stuck. How would you do it?

> Now the problem is that there are 'holes' in this
> inheritance lattice: Not all versions introduced new variations of
> all types; [...]

> My first thought was to create all the missing classes dynamically,
> but it's somewhat obscure and it may not be that simple. Is there a
> more elegant solution, either a general design pattern or some
> clever python metaprogramming hack ?

In the most general case, you need to access time the whole "upper
left" subgraph at class creation, collect all methods defined in this
subgraph with "column priority", and overwrite or add to that any
methods defined in the newly defined class.

I don't know enough about the intricacies of Python's class creation
to make a concrete suggestion, but I'd think that would be possible
with the help of __metaclass__. You would need some sort of
repository for the complete inheritance. One way to do that would
be to create the chain A ... Z first, letting A inherit from some
special class with __metaclass__ set, and then store an array
of all versions somewhere inside the class namespace.

You'd also need some special syntax to create a new version of a class
(say, again inheriting from some special class with __metaclass__
set).  You could set the version inside the class definition, and then
let the __metaclass__ routine disable the normal inheritance
mechanism, and add missing methods as appropriate.

This could for example look like

class A(Versioning):
  ...

class B(A):
  ...

class C(B):
  def h ...
  ...

class A2(NewVersion,A):
  __version__ = 2
  def f(): ...

class B2(NewVersion,B):
  __version__ = 2

class C2(NewVersion,C): 
  __version__ = 2
  def g(): ... f() ...

class A3(NewVersion,A):
  __version__ = 3
  def f(): ...

class C3(NewVersion,C): 
  __version__ = 3
  def h(): ... g() ...

with a hole at B3, as in the example. C3 will get g from C2 and f from
A3, and hence the call chain will work correctly. Also, C3 will have no
base classes (or maybe only the __metaclass__ ones), the inherited
class A, B, C are just used by the class creation process to find
out where to look for the inheritance matrix.

Others who know more about the class creation mechanism will no doubt
improve this suggestion, point out my errors, and tell you how to
implement it :-) Note that you're basically completely changing the
normal inheritance mechanism, and the class objects will be larger,
because they'll have to copy all the necessary methods.

I cannot think of any pattern that would give similar flexibility.

- Dirk

  



More information about the Python-list mailing list