Re: OO Design induces an existential crisis



There are some interesting tidbits in here. With your permission, I copied
some of your comments from our parallel thread to this one where you
introduced a similar point. I didn't want to respond twice, and your
comments are well formed. Seemed a shame not to discuss them. I hope you
don't mind.

We've got about three conversations going. One on the principles (with copy
over from another thread), one on the validity of specific examples used in
articles and how applicable they are to business programming, and one on the
use of a conceptual basis for creating interface boundaries. Let's focus
this thread on the first. We can focus another thread on the third if you
want.

The second discussion (bad examples)... well... let's just say that it does
me no good to attempt to justify the poor selection of examples used in the
writings of others. If you'd like to focus a thread on something useful
from that, let me know.

>> I ask you to find the single OO principle that you disagree with. "if OO
>> purists would abandon _this_ one, then their code would be more
>> realistic/understandable/useful/agile... whatever..."
>
> They generally need to be considered case-by-case. I have found some
> issues with "Open-Closed" principle in a nearby message, mainly
> regarding it being unevenly applied.

So, if we pick on the Open-Closed principle, would that be enough to break
the "badness" of OO? Would you be happy with keeping the notions of
Substitutability and Single Responsibility and all the rest?

Think about it before you go down that road. Is that the only "bad" one?
I'm not hearing that in your critiques.

The comments from the "nearby message" (our parallel discussion on
consistency) is copied below since I didn't respond there.

> And, some of the are vague, such as Open-Closed Principle. The example
> shown is typical case-statement bashing. One can show that case
> statements like that can make it easier to add new behavior/operations
> without having to change existing modules (or at least less modules
> than poly). And like I always fuss about, "sub-types" are a fragile
> modeling technique in my domain. Sub-type based examples need even more
> overhauling if sub-types "degenerate".

I find interesting the notion of "case-statement bashing". I will, on
occasion, use case statements in my factory methods. I try to keep them to
factory methods, for my sake (explained later), but they are there,
nonetheless.

The open closed principle says that a class shall be closed for modification
but open for extension. The core idea is that the class models a specific
'thing' so well, that you can rely on it to play that role reliably for all
purposes where that 'thing' is needed. You don't need to change it.
However, if you need a 'thing2' (forgive me, Dr Seuss :-), you would derive
a 'thing2' from 'thing' in such a way that thing2 benefits for it... and
add new behavior or properties to that.

That said, this is a description of 'how' to use inheritance, not when. It
is, IMHO, often misused. Frequently, I've seen ill-advised designs that
produce large trees of inherited variations... many generations deep, where
the problem was really in the original 'thing' being modeled. It was really
two (or three or ten) 'things' and so variations tend to build on each other
in a combinational explosion.

This produces steep heirarchies, which are (usually) a poor use of
inheritance. This leads well into your next point...

>
> OC could possibly "fix" some of them by simply extending and extending
> via sub-classing. However, sub-classing may not lead to proper
> factoring. If you cannot "open" other classes, you cannot share the
> code because one may have to move the method higher up the tree, or
> move it to a different node on the tree and reshuffle the hierarchy.
>
> Here is an example class hierarchy where the indentation level
> indicates tree level:
>
> A
> --A1
> ----A11 needs method B
> --A2 has method B
>
> Later into the project it turns out that class A11 needs method B, but
> still uses other methods found only in A1. Thus, we cannot put A11
> under A2 but have to refactor the tree. You either have to duplicate B,
> change the interface to A and it's children's interface (which requires
> "opening"), move ("open") the other methods in A11 to B, and/or change
> ("open") A2.
>
> Thus, OC can objectively conflict with duplication factoring for
> certain sets of changes. Or is factoring out duplication NOT one of
> your principles? You can avoid a lot of "problems" if you simply allow
> duplication, but few want to endorse duplication you will find out.

I'm no fan of duplication. However, I don't normally concern myself with
"factoring out duplication" as much as designing for commonality. Let's
look at your example tree above. You have two statements of "need". There
are two ways to interpret this. In my mind, I would translate the verb
'needs' to mean 'uses' (in the UML sense) but I don't think that is what you
meant, because both A11 and A2 could simply create an entire different
object that contains method B and call it. I think you meant 'exposes' as
in the notion that the behavior or property 'B' is common to both A11 and
A2, but not A1.

What you are getting at is a common situation, really. Reality doesn't
break down as a simple heirarchy. Clearly, I could refactor the tree by
either moving B to another class in the tree or changing the inheritance
structure. You would easily be able to counter with an example of where
this would be a bad idea. We could go around for days. Instead, I would
call upon one of the principles from the Gang of Four: prefer composition
over inheritance.

To your problem, using this notion, a class can be composed of a matrix of
behaviors, where each behavior is independently assigned in to the class,
keeping the interface constant. This can be done by understanding the
reason for the behavior B and factoring up an interface to support that
reason. It is implemented using a Bridge pattern, where an object is
assigned in (composition) and held by another object to seperate the
interface from the implementation.

So did I keep my object "closed for modification?" I could certainly have
simply extended my base class to allow the addition of my bridge objects,
without modifying them at all. In that sense, I could be didactic and
create a subclass of A (call it A-prime) and then change the inheritance
tree to look like this:
> A
> - A-prime exhibits B (nullable)
> ---A1
> ------A11 implements method B
> ---A2 implements method B

This will work just fine. I am hesitant to do it because I believe that the
resulting inheritance chain is too steep. In other words, the Open-Closed
principle, if it were the only one, doesn't prevent code rot. However, it
is not the only principle. I would also say that the Single Responsibility
principle must be considered. This one says that a class must have one and
only one reason to change. In this case, the reason, for class A, is that
the fundamental understanding of all A objects has changed, in that A needs
to exhibit behavior B. In this context, I would consider changing the
interface for A (on a case-by-case basis).

For the sake of argument, let's say that I did counterbalance Open-Closed
with Single-Responsibility and found that one outweighs the other. For me,
it would play like this:

> A exposes interface to the methods of B
> ---A1
> ------A11 contains object that implements method B
> ---A2 contains object that implements method B

If the implementation of the behavior is a single interface, then a simple
Bridge pattern will suffice. If more than one behavior 'B' needs to be
attached to individual objects of type A, this can be done using a decorator
pattern. The factory (or in some cases, the constructor) can be used to
isolate the place where this relationship is created. For those classes
that do not exhibit the behavior (A1), the composed object would not be
assigned or would be assigned an object that does nothing when called.

So, did I walk away from the Open-Closed principle? Not at all. I simply
counterbalanced it with other principles and made a judgement about which
would yeild more readable code. Can other people come to a different
conclusion? Sure.

[and pulling another salient tidbit]
> IIRC, Open-Closed is in part based on Meyer's "single
> choice principle", which is used against case statement lists, but
> fails to use it against duplicate "lists" of methods in a class even
> though it requires multiple visits to add new methods, the stated
> reason for avoiding "duplicate lists". If one type of duplication is
> bad but another is good, he failed to clarify *why*.

I assume that you mean that if a virtual method is added to the base class,
it now has to be implemented in each of the direct descendents. This is
true. In some cases, it is OK to add it to the base class. In other cases,
that is inappropriate. It may even propogate down more than one generation.
This is rare but it happens.

Note that a public method can be added to the base class that relies on a
method that is implemented privately in the decendents. This is called the
Strategy pattern and allows for minor variations in logic to be seperate
from the common code.

The point is that a method belongs where its cohesion is high. Case
statement lists are difficult to measure using cohesion (Single
Responsibility) as the yardstick because they don't have to perform similar
things. Each part of a case statement can do many thing, which makes them
less controllable. They are still useful, but I would consider them to be
less useful in many situations because of this failing. Derived objects do
not fail this test, and in fact, can yeild more efficient code in specific
situations.

In this sense, adding a list of behaviors to descendents of a base class is
appropriate where those behaviors constitute a new understanding of the base
class behavior and where a variation exists in the child class. At any
level of the heirarchy, the interface can become a concrete implementation
that is inherited, so the addition of something to a base class may not
imply change to the children at all, or it may apply to just the applicable
ones.

As I said before, the reason that case statements are often criticized is
that the code that goes in them in not restricted to any conceptual
framework. You can't therefore logically prevent yourself from doing
something foolish, like making one decision that affects two behaviors
(thereby binding the behaviors together logically). This is the code
equivalent of the data notion: use a field for one thing only. You want to
use a logic block for one thing only. By assigning objects that are
restricted by interface, you can (hopefully) give yourself the structure to
develop code that avoids unintentional bindings like the ones occasionally
found in long case statements.

>
> As far as why those p/r principles work, I don't fully know nor claim
> to fully know. They just do. Part if the benefit is from a general
> principle called "divide and conquer". The RDB and/or table engines
> focus on and standardize collection-oriented issues allowing the coding
> part to focus mostly on one task at a time without worries about
> taxonomies etc in code. OO tends to reinvent collection-orientation
> from scratch for each and every class, creating duplicate interface
> patterns because otherwise encapsulation is violated. Encapsulation
> actually CAUSES interface duplication. Thus, you get things like getX,
> findX, deleteX, listX, getY, findY, deleteY, listY, getZ......over and
> over and over again.

I suppose this happens sometimes. I'm no great fan of these kinds of
objects. I think this comes from the debatable notion that every business
data row must be represented by an object with native properties and get/set
pairs for each one. I'm not a big subscriber to this notion. There are
times when this is useful... for configuration structures, for example,
where there is only one value for a particular data value (for example).

I would say that some of the application patterns developed for business
apps in the OO world have been hobbled by this, and you may be reacting to
this. I would state that these folks took one path, where I would choose a
different one: one based on the notion that you develop using the simplest
possible code and you create specializations only where you need to. If you
look at business OO patterns from this philosophy, and using the tenants of
Commonality-Variability analysis (CVA), you won't get objects that do
nothing but map to field names.

In this sense, you wouldn't have a business object called "purchaseOrder"
with a property for "purchaseOrderId". You'd have a data object called
"purchaseOrder" that simply contains (and, with conditions, exposes) a
DataSet that contains the purchase order, as well as allows other methods
and properties to be attached that perform specific functions (like
controlled assignment, validatation, persistence, communication, and
cross-cutting concerns like security, logging, and caching). This gives you
strong typing (you cannot confuse a purchase order with a customer) but
allows data-bound controls to manage the presentation and modification of
the underlying data.

Some would ask: why not just use an O/R framework? The problem with O/R
frameworks may be their promise as well... you can quickly create a
simplified view of the underlying data in a manner that is very specific to
data representation and update. The downside is that objects created this
way are often difficult to extend. They are essentially a structured
programming solution to an object oriented programming problem.

--
--- Nick Malik [Microsoft]
MCSD, CFPS, Certified Scrummaster
http://blogs.msdn.com/nickmalik

Disclaimer: Opinions expressed in this forum are my own, and not
representative of my employer.
I do not answer questions on behalf of my employer. I'm just a
programmer helping programmers.
--


.



Relevant Pages

  • Re: OO Design induces an existential crisis
    ... > use of a conceptual basis for creating interface boundaries. ... >> And, some of the are vague, such as Open-Closed Principle. ... >> move it to a different node on the tree and reshuffle the hierarchy. ... Or is factoring out duplication NOT one of ...
    (comp.object)
  • Re: How to use JTree with very large data?
    ... > The first implementation I came up with is just creating a tree model ... > the tree structures in memory, and the fact that the user may toggle ... > references and the node duplication). ... The TreeModel interface never assumes that each node implements the TreeNode ...
    (comp.lang.java.gui)
  • Re: How to use JTree with very large data?
    ... > The first implementation I came up with is just creating a tree model ... > the tree structures in memory, and the fact that the user may toggle ... > references and the node duplication). ... The TreeModel interface never assumes that each node implements the TreeNode ...
    (comp.lang.java.programmer)
  • Re: Disadvantages of Dependency Inversion?
    ... principle does not imply layering is a similarly basic principle. ... I accept the explicit seperation of interface from implementation does ... a subset of their interface conforms to CI (type substitutability). ... for me subtype is short for substitutable type. ...
    (comp.object)
  • Re: Disadvantages of Dependency Inversion?
    ... principle does not imply layering is a similarly basic principle. ... MN>B....component B is dependent on component A for an interface. ... created, an existing dependency is changed, and a new dependency appears. ... a subset of their interface conforms to CI (type substitutability). ...
    (comp.object)