Summary
My previous blog entry on the bank account class, sparked some lively and interesting discussion. It also revealed two very distinct approach to object oriented programming, which I label the bottom-up approach and the top-down approach.
Advertisement
I have identified two very distinct approaches to object oriented programming, which I label the bottom-up approach and the top-down approach. I will revisit the bank account examples from an earlier post OOP Case Study: The Bank Account Class, while attempting to explain the different techniques and to show how they compliment each other.
The original problem was stated as:
represent a bank account class in code
There are two ways to interpret this, as simply as possible or as realistically as possible. Each interpretation embodies a separate design approach, which are I hope to show, compatible with each other .
The Top-Down Approach
Since the bank account example lacks sufficient contextual information we can fill in the blanks use some basic assumptions we know about bank accounts in financial institutions:
any modification to the account should done in a thread-safe manner
any modifcation to the account should be recorded as a transaction
the balance should be accesible at any given date or time
The following is an example high-level representation of what an account could be which satisfies the above requirements:
class Account {
ApplyTransaction(Transaction);
TransactionHistory GetTransactionHistory();
GetBalanceAsOf(Date);
}
Following the top-down approach, we would then continue by specifying the sub-elements (i.e. Transaction and TransactionHistory) and fill in the implementation details.
The Bottom-Up Approach
The other approach, is that we instead try to construct a class which is as general as possible, but is still useful. Making as few assumptions about the bank account as possible, one specification could be:
it has a balance which can be viewed and modified
In the absence of further information, it can be argued that the simplest thing that can possibly work (TSTTCPW) is the correct answer, so I propose:
int balance;
Nothing is much simpler than an int! The bottom-up approach would be to then
specify more sophisticated classes that are composed-of or inherited-from the low-level classes as more specifications are included. For instance consider the new specification:
account viewing and updating must be possible in a thread safe manner
class Account {
Lock();
Unlock();
int GetBalance();
SetBalance(int);
fields
int balance;
}
This is arguably the simplest class which satisfies the previous requirements, and reuses the previous definition. Introducing yet another requirement:
any modifcation to the account should be recorded as a transaction
would imply a transactional account:
class TransactionalAccount {
int GetBalance();
ApplyTransaction(Transaction);
fields
Account account;
}
By now I think it should be obvious where I am going with this. Starting at the bottom and introducing new layers of abstraction I can eventually reach the same design as was proposed by the top-down approach.
Summary: Combining the Two Approaches
The the top-down approach, which is more common, is to start with the full set of specifications, design an interface, and then provide the implementation afterwards. The danger of this approach is that it often doesn't go deep enough into the so-called
"implementation details". This can lead to designs that are too brittle because the objects are overloaded with responsibility and concerns aren't properly delegated into separate classes. OOP is useful at lower levels of abstraction including implementation details. The other big danger of the top-down approach is that we can make premature assumptions about a system, based on the language used to describe it.
The bottom-up approach implies starting with classes which represent the system as generally as possible and then using them to build more sophisticated and precise classes which eventually satisfy the problem. A bottom-up approach has its own pitfalls of course, such as the fact that it can be tempting to try and fit high-level classes to correspond with the limitations of the low-level classes, and the fact that programmers sometimes don't implement solutions with the appropriate level of abstraction.
Clearly both the top-down and bottom-up approaches have their value and place in software development. It is important in any discussion of OOP techniques to be aware of both approaches, as they each can give useful and important insights into the design and implementation of software, which are just two sides of the same coin.
I started my OOP career top-down and I've gradually learned that, at least for me the bottom-up approach yields the cleanest design/implementation the quickest.
I think looking at this bank account class's development you will see a development history that is neither "top down" nor "bottom up". They're mildly interesting theories about a theoretical development processes but of limited value since that's not how the world works. You can't say using approach X yields solution Y, much as it looks enticing to do so. All that has happened is that a start and end point have been identified and the history of the route between the two points rewritten to suit the theory.
What really happened was a process of iteration from an initial proposal ("Here's how I would do it.") via a feedback loop ("What do you think?" ... "Er? Have you considered X or Y" ... "OK. How about this?") that vaguely repeats itself and meanders to an answer whose usefulness is basically a product of the people involved in the process, not in any imaginary process that may or may not have been followed.
The most important factors in any development process come from the people involved; their knowledge, experience, insight and capability. The solutions to the problems that you displayed were found by simple consultation with people who - to varying degrees - knew what they were talking about, not by adopting any imaginary theoretical approach.
> I think looking at this bank account class's development > you will see a development history that is neither "top > down" nor "bottom up". They're mildly > interesting theories about a theoretical development > processes but of limited value since that's not how the > world works.
Maybe that isn't how your world works, but they are in fact techniques that I use to design software. I have seen plenty of evidence to support the fact that people use one technique or the other when developing software, or some combination of the two, but that appears rare.
> You can't say using approach X yields > solution Y, much as it looks enticing to do so. All that > has happened is that a start and end point have been > identified and the history of the route between the two > points rewritten to suit the theory. > > What really happened was a process of iteration from an > initial proposal ("Here's how I would do it.") via a > feedback loop ("What do you think?" ... "Er? Have you > considered X or Y" ... "OK. How about this?") that > vaguely repeats itself and meanders to an answer whose > usefulness is basically a product of the people involved > in the process, not in any imaginary process that may or > may not have been followed.
I always maintained the same botom-up approach throughout my code examples, building on what I claim is the most correct low-level representation of an account class.
Throughout the conversation I was attempting to show how the original general class I proposed was correct in so much that is was sufficient and correct for building more high-level classes which satisfied more real-world requirements that were proposed.
There was no meandering, I built a design up carefully layer upon layer, never abandoning my core design. I would recommend rereading the conversation paying careful attention to how I arrived upon a more realistic design.
> The most important factors in any development process come > from the people involved; their knowledge, experience, > insight and capability.
With all due respect to the intelligent people who contributed to the previous conversation, there was little that they contributed to my design other than add to the requirements and argue for a more realistic example.
You seem to be implying that programming is always a cooperative endeavour. I have worked for a very long time as an independant software developer. Cooperation with regards to software design can be useful, but is not neccessary.
> The solutions to the problems > that you displayed were found by simple consultation with > people who - to varying degrees - knew what they were > talking about, not by adopting any imaginary theoretical > approach.
I disagree. First off, I was the one who arrived at my solutions, so perhaps I would be in a better position than you to say what process I used to arrive at my design! I always maintained throughout the discourse that the following Account class representation was correct and useful:
I used that example to build more sophisticated classes which satisfied the various specifications that were proposed to make the example less artificial. The specifications became more sophisticated, but I view them as separate specifications, not as any kind of arrival at an end-product. The problem with the whole endeavour, is that no matter what you do, without the context of a specific real-world example, you can never arrive at a single best-case solution.
Why do you maintain the idea that Lock() and Unlock() need to be exposed, despite all the discussion of how that can lead to bugs and how there are better designs? With all due respect to you, People did come up with improvements to your design, repeatedly, but you seem reluctant to adopt them. GetBalance() and SetBalance(), Lock() and Unlock() -- this is about as un-OOP, un-service-oriented design as you can get.
> With all due respect to the intelligent people who > contributed to the previous conversation, there was little > that they contributed to my design other than add to the > requirements and argue for a more realistic example.
The problem is that you're chilling discussion by arbitrarily deciding what a "bank account" would have. You are driving this design discussion with an arbitrary view of a domain.
It would be like me saying, "Let's design a class for a chair. The core interface will have an int legCount property." And someone responds, "What about the materials the chair is made of?" and I respond, "Nope -- that can be added as another layer. The legCount property is core."
> Why do you maintain the idea that Lock() and Unlock() need > to be exposed,
My point is that there are different layers of abstraction which are correct for different situiations. The most general abstraction, is the most correct in the fact of missing specifications, because it can be used to build more specific solutions. The interface class should have been renamed to:
This is the correct low-level representation of an account class for most situations. I used it successfully to design various other account representations in my previous posts. Programmers should not use this representation of an interface at the top-level of their application, but rather at the the intermediate level to create a more sophisticated design such as:
This kind of interface is a more appropriate representation of an account for the higher level of many application domains. It is a poor general solution because it incorporates too many assumptions. Just like the Withdraw and Deposit example that was proposed, was too specific to be useful in a wide range of scenarios. It is in fact a poor specialization of a transactional account, in that it assumes only two transactions and provides no ability to govern atomicity between getting of balance and modifying balance.
> despite all the discussion of how that can > lead to bugs and how there are better designs?
They are not better designs, but rather more abstract designs, with more specific requirements. The more assumptions we make the more we can refine our design, but the same basis exists.
>With all > due respect to you, People did come up with > improvements to your design, repeatedly, but you seem > reluctant to adopt them. GetBalance() and SetBalance(), > Lock() and Unlock() -- this is about as un-OOP, > un-service-oriented design as you can get.
All design has to start somewhere. You can't just ignore the basic implementation details because they are un-OOP. What people seem to misunderstand is that OOP can be applied to all levels of an implementation and abstraction. What a lot of people seem to be pushing for is essentially that OOP only applies at the top level of the application whereas the rest is "implementation details". What I am advocating is an approach of starting with as general and reusable interface as possible and refining it until we arrive at a more precise and useful class, but while reusing the more general specifications.
> > With all due respect to the intelligent people who > > contributed to the previous conversation, there was > little > > that they contributed to my design other than add to > the > > requirements and argue for a more realistic example. > > The problem is that you're chilling discussion by > arbitrarily deciding what a "bank account" would have. You > are driving this design discussion with an arbitrary view > of a domain.
We can make certain generalizations about all domains that permit us to build higher-level abstractions. That is why I am adamant about starting with a good low-level abstraction.
> It would be like me saying, "Let's design a class for a > chair. The core interface will have an int legCount > property." And someone responds, "What about the materials > the chair is made of?" and I respond, "Nope -- that can be > added as another layer. The legCount property is core."
This is a false metaphors. The goal is to start with as general a representation of a chair as possible, and then further refine it. I would recommend a chair has the property that it is "sittable". That is core. Some chairs have four legs, some chairs have three legs. Then the material can be added later.
Thanks for your comments, I hope I have clarified my position somewhat.
> > This is the correct low-level representation of an account > class for most situations.
It may be one of many possible low level representations of an account class for "most situations" but it goes well beyond the original problem that you posed. Your solution introduces a number of debatable assumptions into the design. We know for a fact that they are debateable since people are doing exactly that. At the same time you reject other suggestions on the basis that you are trying for a simple low level solution not a realistic one.
In fact, the simplest low level (bottom up) solution to the stated problem is
interface BankAccount
{
}
Nothing more. No assumptions about possible locking requirements, multithreading, setting balances, representing balances as ints, account owners, access security or anything.
> This is the correct low-level representation of an account > class for most situations. I used it successfully to
Sorry, I just don't buy it. I don't buy that you could say with any surety that this is the correct design for any real-world situation. And even if it were, I would still argue that you do not need the Lock() and Unlock() methods, for the reasons previously stated.
> > despite all the discussion of how that can > > lead to bugs and how there are better designs? > > They are not better designs, but rather more abstract > designs, with more specific requirements. The more > assumptions we make the more we can refine our design, but > the same basis exists.
I'm referring to the designs that didn't expose the locking/unlocking, because it doesn't have to be exposed, and also because you can't assume that a simple Lock() and Unlock() method will be adequate.
> All design has to start somewhere.
Yes. It starts with requirements -- and in this case, we don't have any.
> Sorry, I just don't buy it. I don't buy that you could say > with any surety that this is the correct design for any > real-world situation. And even if it were, I would still > argue that you do not need the Lock() and Unlock() > methods, for the reasons previously stated. <snip> > I'm referring to the designs that didn't expose the > locking/unlocking, because it doesn't have to be exposed,
You must have a lock and unlock somewhere in your code. You can not write a multi-threaded application without it. If you have multiple layers of abstraction, it will be exposed to one layer, and hidden at another. Simply viewing all software as being only two layers of abstraction (i.e. implementation / interface) is insufficient.
> You must have a lock and unlock somewhere in your > code. You can not write a multi-threaded application > without it. If you have multiple layers of abstraction, it > will be exposed to one layer, and hidden at another. > Simply viewing all software as being only two layers of > abstraction (i.e. implementation / interface) is > insufficient.
You must have mutexing logic somewhere in the code for a multithreaded application, but it does not need to be exposed in the Account interface.
Think of it this way: if we use the interface that you describe, then every usage of that interface is going to follow this pattern:
acct.lock(); int balance = acct.getBalance(); // do some computation acct.setBalance(newBalance); acct.unlock();
Therefore, we should push down whatever lock() and unlock() do into the implementation of Account, and replace setBalance() with doTransaction(Transaction t), the invocation of which is surrounded by the locking logic.
And lock() and unlock() are still based on your assumptions. What if, for a given bank, you had to dial a modem to perform a transaction? Would you dial the modem in lock(), and hangup the modem in unlock()? If the answer is yes, then these methods are doing a lot more than locking and unlocking, aren't they? If the answer is no, then a call to lock() hasn't really locked the account, has it? You simply can't assume that such a complicated domain is going to fit your scheme.
The simplest way to see that a "lock" and "unlock" method is unnecessary is to observe that a real bank user (teller/manager, etc.) never "locks" an account. They merely perform a transaction.
To put it crudely, what the heck is that lock method doing there? Why must the developer expose his private parts to the user? What is the user supposed to do with them?
Look at it from the end-user's point of view and life becomes much simpler.
As I mentioned in a related thread, I believe that there are simple abstractions for the account class. This thread seems to be dealing in distractions, not abstractions.
> To put it crudely, what the heck is that lock method doing > there? Why must the developer expose his private parts to > the user? What is the user supposed to do with them? > > As I mentioned in a related thread, I believe that > hat there are simple abstractions for the account class. > This thread seems to be dealing in distractions, not > abstractions.
I am trying to explain that even at the implementation level you can use an interface to hide details of the implementation. That this is still a valid and useful representation of an "account" even though it is not a high-level representation of an account.
In the earlier discussion at http://www.artima.com/forums/flat.jsp?forum=106&thread=78614 I carefully explain how we can have a low-level representation, which use that to buid an intermediate representation which we can use to build a high-level representation. They are all "accounts".
What you call "distractions" I call implementation.
The other thing is that interfaces are not simply for "end-users", they can be used for ourselves to help modularize our implementations which is precisely what I am doing.
I will say that I agree that is very useful and important to be able to model architectures as abstractly as possible using interfaces. It is not, however, incompatible with a more bottom-up approach.
To get back to your question, "what is lock doing here", well lock has to occur somewhere in the implementation, and a low-level account representation is a logical place for it. At the top level of abstraction, it would most definitely not be the right place for it.
I hope my postion is somewhat more clear on the subject.
> I am trying to explain that even at the implementation > level you can use an interface to hide details of the > implementation. That this is still a valid and useful > representation of an "account" even though it is not a > high-level representation of an account.
The problem for everyone is that you're saying one thing then coding the opposite. Implementation details should indeed be hidden and using interfaces is one way of doing it. But not the way you've done it.
The trouble is that having an Account interface is a high level abstraction - not the low level you want it to be. By including locking, directly getting and setting the balance (see "Why getter and setter methods are evil" by Alan Holub http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html) and integer balance arithmatic in the interface, your code says that "all bank accounts that conform to this interface must expose these workings in this way to any system that uses them". Implementations of an interface cannot hide methods that have been exposed by that interface.
> To get back to your question, "what is lock doing here", well lock has to occur somewhere in the implementation
Again, your text is correct: hidden "in the implementation" is the place for declaring these methods not exposed on the account's interface. Putting everything in one place is just a mess.
The following code one way one might code what you're suggesting:
This you give what your text says you're after, an Account interface that hides everthing that doesn't need to be exposed, a BankAccount class description that includes but doesn't expose the detailed locking and balance handling methods (which being different responsibilities perhaps ought to be in different interfaces). You are now free to expose only the high level methods in Account and can worry about the low level detail stuff in the classes that extend the BankAccount or implement the Locking and Balance.
As Einstein said "as simple as possible but no simpler" otherwise what you get is just simplistic.
It is fairly well known that financial institutions generally have rules regarding the payment of interest on savings and checking accounts but, in the code shown, the account ignores that fact and a method accepts an interest rate and increments the balance by the calculated amount. Worse, the Account has getBalance() and setBalance() methods, essentially removing the encapsulation around the balance field.
In that example, the BankAccount class has low cohesion because (1.) it has the additional responsiblity of interest calculations and (2.) because it lacks the repsonsiblities it should have, of being responsible for its own balance; the getter and the setter remove the encapsulation around the account balance and let any passer-by do what it will with the balance. Removing the encapsulation in this way would allow one account's balance to be arbitrarily set to negative $1,000,000.
Having the class responsible for its use by a thread lowers the cohesion even more. If this were a good idea, the class should be responsible for its own persistence as well.