Re: OOP/OOD Philosophy
- From: "Nick Malik [Microsoft]" <nickmalik@xxxxxxxxxxxxxxxxxx>
- Date: Sat, 2 Jul 2005 08:48:23 -0700
Hello Plan 9,
(Is that a reference to "Plan 9 from Outer Space," perhaps? Ahhh... a fan
of bad SF Cinema :-)
>> OOD helps you to organize and represents the information. All people
>> makes a mental design to resolve a problem and then programs the
>> solution. In OO terms one first designs an OO solution and then programs
>> it in an OO language (or not :) ).
>
> But non-OOD is also design. Non-OOD focuses more on deriving the
> algorithms, while OOD focuses more on the data, although the two
> intersect. One can derive data from the algorithms/relations and vice
> versa. So, given two different ways of doing something, and a former
> way that is clearer and more direct, why use the latter? If it's an
> issue of flexibility/re-usability, what's the thinking behind that?
Really, the goal is not so much to reuse things as to seperate the things
that change a different times, to make them easier to change. We start with
the limitations of people and create languages that those people can use.
If you watch the evolution away from OO and towards things like AOP and
lightweight frameworks, it is part of an ongoing process towards the
seperation of "things that change rarely" from "things that change
frequently."
The obvious first efforts were the function libraries that would come with a
language. We all knew that there had to be a way to produce the square root
of a number. That mechanism is older than computer science (although many
implementations exist). The fundamental definition doesn't change very
often so it is easy to place something like SQRT() in a math library and be
assured of its longetivity. That's procedural.
What OO gave us was a way to abstract that thinking a bit more... to look at
the activities of our applications and find those activities that are,
themselves, fundamental and rarely changing. If we allow those activities
to operate on interfaces, and not on actual items, we can seperate these
fundamental activities (rarely changing) from the implemented objects
(frequently changing).
In this way, we earn reuse, but not by seeking it. We are seeking ease of
maintenance, and ease of understanding.
>
> Here's an example. Let's say we have a cash register program with the
> options to add, subtract, print a receipt or clear the total.
>
> Non-OO analysis:
> =========
> Op = current operation invoked
> Price = Price Entered
> X' = value change for X for next state
> Total = running total
> ==========
> Then, the state of the system (assume it waits for input) is:
> ==========
> (Op = Start ^ Op' = Clear) v
> (Op = Add ^ Total' = Total + Price) v
> (Op = Sub ^ Total' = Total - Price) v
> (Op = Clear ^ Total' = 0) v
> (Op = Print ^ Printed(Total))
> ==========
Ah, predicate calculus. I haven't done this in years. For a while, I was
pretty good in ML and later in Prolog. However, you'll have to forgive my
rustiness in the notations you used. They are not directly familiar, even
though I believe that I understand what you are trying to say.
This is a very logical approach to the state of a single object. Your
example, however, is not typical. Most applications are not like a cash
register.
A cash register is a machine that holds state for a single long-running
transaction. The state is a series of transactions against inventory. Even
in this very simple description, your model is too light, in that you have
to represent, somehow, the inventory aspect of modern cash registers. If
you do not, your example devolves into an adding machine and a cash drawer.
So let's evolve an adding machine into a cash register...
/// note: the following code is considerably simplified.
class PurchaseTicket
{
public double RunningTotal = 0;
public AddToReciept(Item MyItem, int Quantity, Outputter PrintOutputter)
{
RunningTotal += MyItem.Price * Quantity;
PrintOutputter.PrintLine("{0}/t{1} @ {2}/n",MyItem.Description,
Quantity, MyItem.Price);
}
public DeductFromReciept(Item MyItem, int Quantity, Outputter
PrintOutputter)
{
RunningTotal -= MyItem.Price * Quantity;
PrintOutputter.PrintLine("{0}/t{1} @ {2}
Credit/n",MyItem.Description, Quantity, MyItem.Price);
}
}
There is no 'clear' in that the PurchaseTicket only exists for a single
customer. When the next customer comes, a new ticket is created. If an
entire transaction is started over, the exact same logic applies.
Some would argue that an item should print itself. I disagree. The ticket
would know the format of the output. There is a grey area here. The point
is that placement of the "knowledge" (what does output look like) needs to
make sense to a developer. That way, when the crack open the code a year
later, they can find it fairly quickly.
We have a dependency on the notion of an Outputter, and it has the
interesting method of PrintLine(). Other than that, the code above has no
way of knowing (or caring) if the Outputter is actually an interface and
that the object passed in is simply one that implements that interface.
Importantly, we can add inventory functions fairly readily because we use
the Item object to contain information about the thing we are adding to our
ticket. The interface, above, doesn't change very much. One thing that
does change: we can raise an error if we attempt to remove things from the
ticket that were never in there in the first place:
class PurchaseTicket
{
public double RunningTotal = 0;
private List<Item> TicketList = new List<Item>();
public AddToReciept(Item MyItem, int Quantity, Outputter PrintOutputter)
{
RunningTotal += MyItem.Price * Quantity;
PrintOutputter.PrintLine("{0}/t{1} @ {2}/n",MyItem.Description,
Quantity, MyItem.Price);
for (int i = 0; i < Quantity; i++)
TicketList.Add(MyItem);
}
public DeductFromReciept(Item MyItem, int Quantity, Outputter
PrintOutputter)
{
if (TicketList.CountOf(MyItem) < Quantity)
raise ApplicationException("Item does not exist in this
quantity in the ticket");
for (int i = 0; i < Quantity; i++)
TicketList.Remove(MyItem);
RunningTotal -= MyItem.Price * Quantity;
PrintOutputter.PrintLine("{0}/t{1} @ {2}
Credit/n",MyItem.Description, Quantity, MyItem.Price);
}
public void CommitOnPayment(InventoryManager Iman)
{
foreach (Item TicketItem in TicketList)
Iman.RemoveFromInventory( TicketItem );
}
}
In this example, I added a new dependency. We are now coupled to the
definitions of a List. That List has a couple of interesting methods, like
CountOf, Add, and Remove. I don't know what other methods it has, nor should
I care. There are a lot of things I could say about the List, but they
would be off topic.
The point is that, by using OO programming, I've encapsulated the idea of a
list of items. That list maintains a running total of items that need to
removed from inventory when the CommitOnPayment method is called.
I can very easily implement this program using Item as a concrete class. The
neat thing about OO is that, later, I can decide to use DIP (either
definition :-) and change Item to an interface. I can implement the Item
interface in another part of the application in any way that I'd like, and
the code in this part would not change at all.
This is an example of the Liskov Substitution Principle. [paraphrased -
badly] Any subtype of a type can be substituted for any other subtype as
long as the code refers to the type.
>
> Given I'm not knowledgable about OOD, please forgive a possible
> butchering of OOD, but here's how I can see an OOD approach to the
> problem. First, I identify the nouns in the system:
>
> User
> CashRegister
> Total
> Operation
> Price
You are on thin ice already.
>
> Now the relationships. The User interacts with the CashRegister; in
> response, the CashRegister creates the appropriate Operation and allows
> access to the Price. Price and Total are just numbers. Furthermore,
> since there are several types of Operations, Operation is an abstract
> base class with Add, Sub, Print, and Clear as children sharing a
> consistent interface to allow for more streamlined code.
Interesting analysis. Not a good one.
Ask yourself the question: in my system, do I have competing needs? If I do
not, then use the simplest possible implementation that I can. If I do,
then look for what is in common between them, and what is variable. Pull
the variations down in the inheritance tree, and push the commonality up.
Prefer composition over inheritance. Make your interfaces open for
extension but closed for modification (google the "Open Closed Principle").
I would NOT suggest that the operations Add, Sub, Print, and Clear are
variations. In fact, on your list of items, I'd say that they are quite
common to the concept of a PurchaseTicket (as described above). Certainly,
you could add other types of purchase tickets (say... something that is
electronically transmitted rather than being individually scanned). In that
case, I'd create an interface from PurchaseTicket, and move my code above to
a concrete child of that interface. The calling code would be
none-the-wiser, but I'd be able to create as many different types of
purchase ticket as my customer needs, while limiting the changes to the
fundamental notions of a purchase ticket.
> So, we get
> the following (assuming a non-event model and garbage collection for
> simplicity):
>
> loop
> CashRegister.Interact
> op = CashRegister.Op
> op.DoOp(Total, CashRegister.Price())
>
> However, this is a bit of a kludge. Clear and Print need only one
> parameter, but take 2 in order to conform with the interface. In
> addition, Print gets mutable access to the Total even though it doesn't
> need it, which is unsafe. Furthermore, the analysis up to this point
> was not as clear (or IMO as verifiable) as the previous one.
I'd say: look for what you WANT to encapsulate. Why in the world would you
want to encapsulate this operation at this time? You have stated no
business need for this encapsulation? Certainly, you can encapsulate
operations. In fact, the decorator, command, strategy and
chain-of-responsibility patterns all focus on different approaches to the
problem of encapsulating an operation. However, your comments imply that
you would START there, and I, for the life of me, can't see any reason to do
so.
>
> On the other hand, maybe I was solving the wrong problem. Maybe I need
> to look at the pattern of this program and build the architecture then
> customize it.
Yech.
Use someone else's architecture. Most OO systems have frameworks that they
operate in. Use that. Build only what you need. Abstract only what you
need to abstract.
As far as building your own architecture: YAGNI ("You Ain't Gonna Need It").
> In this case, what I'm trying to build is a type of
> machine that accepts operations, parameters, and can maintain a state
> that is the result of the previous operations. Analyzing it this way,
> we get:
No. You are trying to build a cash register. Your "procedural" example
made no notion of a machine with abstract operations. Why add requirements
the moment you enter the OO world?
>
> User
> Machine
> Params
> OutputState
> Operation
>
> Params are what the Machine returns as data -- they are
> instruction/data pairs (where data can be an additional collection).
> OutputState can store any number of outputs and allows read/write
> access. Otherwise, the semantics are the same:
>
> [Assume output is an instance of OutputState]
> loop
> Machine.Interact
> plist = Machine.Params
> foreach i in plist.Size
> Operation op = Factory.CreateOp(plist[i].Op)
> op.DoOp(plist[i].Data, output)
>
That is the most unreadable bit of code I've seen in a long time. I believe
one of the other posters put up a good quote: "to be reusable, you have to
first be usable." That bit is neither.
You have certainly hit on one of the problems with OO analysis when it
abstracts the wrong things: you can obfuscate the code so wildly as to make
it completely unmaintainable. At that point, you've completely defeated the
purpose of Object Oriented development.
When you look at something like the snip above, your "gut" should say: "this
code smells bad" and you should look for opportunities to refactor it.
> This is the generalized pattern, and by deriving different Operations,
> Factories, OutputsStates, and even Machines we can simulate a wide
> variety of machines -- perhaps even primitive operating systems.
Why would we want to? Was this a requirement of the cash register? Once
again, encapsulate what you need to encapsulate, when you need it, and not
before. OO is a balance. You can go too far (hint: you have).
> We can accomplish things like history lists, screen writes, etc, all with
> the same basic framework, because we solved a general problem.
My code (above) accomplishes the exact same things, and you get the added
benefit of being able to read it.
> Now,
> all future machine-like tasks will consist solely of deriving the
> appropriate classes.
>
> In fact, this whole thing could be made a method of a machine class:
>
> Machine.Run(factory, output)
>
> Then people simply derive from this class, over-ride Interact (and
> anything else they want), and provide the necessary implementations.
>
> Comments?
>
Don't ever write code that I have to maintain. :-)
>
>
>> Design is more abstract than programming, you can design thins that are
>> impossible to translate directly to the code (then you need to
>> "normalize" things before programming it).
>
> Right, which is what I'm seeing from both cases. An analysis of the
> problem simply states relations and some of these relational statements
> can't even be checked by code (take relations involving quantifications
> over infinite sets or involving convenience functions that don't exist
> in the implementation language). What differs here is what we are
> solving. Are we solving the problem at hand, or do we choose to solve
> a generalization one of whose instances is the problem at hand,
> ostensibly for more flexibility?
We solve the problem at hand, using mechanisms that can be generalized WHEN
we need them (and not before).
We don't do anything "ostensibly" for flexibility. We write flexible code
because it is actually a second nature to do so. This is "object thinking".
>
>> > This is why I need to understand the philosophy. I want to understand
>> > how to "think OOD". I don't care about specific design techniques
>> > unless they help illustrate this shift in thinking. What can you tell
>> > me about this? What references (online and printed are fine) can you
>> > point me to? I'd love something that contrasts the two methodologies
>> > and provides examples to drive it home. Something that explains and
>> > justifies OOD from a more philosophical perspective.
I'm going to recommend a very readable book called "Design Patterns
Explained" by Shalloway and Trott. Make sure to get the second edition.
There is an extended section on Commonality Variability Analysis. CVA was
introduced by Jim Coplien but his original work went out of print, so you'll
need to get it second hand (somewhat). The nice thing about this book is
that it is written from the standpoint of an evolution in thinking. The
author describes "aha" moments and how they led to a different approach in
the ways to solve problems.
I hope this helps.
--
--- 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.
--
.
- Follow-Ups:
- Re: OOP/OOD Philosophy
- From: Robert C . Martin
- Re: OOP/OOD Philosophy
- From: topmind
- Re: OOP/OOD Philosophy
- Prev by Date: Re: OOP/OOD Philosophy
- Next by Date: Re: OOP/OOD Philosophy
- Previous by thread: Re: OOP/OOD Philosophy
- Next by thread: Re: OOP/OOD Philosophy
- Index(es):
Loading