The Artima Developer Community
Sponsored Link

Weblogs Forum
OOP Case Study: The Bank Account Class (Part 3)

19 replies on 2 pages. Most recent reply: Nov 27, 2004 2:19 PM by Michael Feathers

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 19 replies on 2 pages [ 1 2 | » ]
Christopher Diggins

Posts: 1215
Nickname: cdiggins
Registered: Feb, 2004

OOP Case Study: The Bank Account Class (Part 3) (View in Weblogs)
Posted: Nov 18, 2004 11:12 AM
Reply to this message Reply
Summary
I have been looking mock objects and test driven design lately. The main issue, I see brought up over and over again is that people want to modify existing code by inserting mock objects, but that it is hard to do because of code dependencies. I argue that proper use of abstraction at the implementation level would make it much easier.
Advertisement
I was perusing Michael Feather's blog and his entry entitled Frustration Driven Development caught my eye. Particularly because it talked about a desire for new programming languages which always grabs my attention!

The post is very interesting in that it does a good job summarizing points that I have seen made elsewhere with regards to difficulty in inserting mock objects into existing code. From what I have seen so far, it appears that the problem lies not in lack of features in the languages used, but rather ineffectual use of abstraction in code.

To try and paraphrase the example is essentially that given the following code :

class Account
{
	private balance = 0;
	private log = new TransactionLog();
	
	deposit(value) {
		log.addEntry(balance, value);
		balance += value;
	}
	...
}
The goals is to create a test case without having to actually use the TransactionLog class implementation as it stands because it is too slow (establishes a connection to a database and commits changes and all of that). So we want to insert a mock object for TransactionLog.
class Account
{
	private balance = 0;
	private log = new MockTransactionLog();
	
	deposit(value) {
		log.addEntry(balance, value);
		balance += value;
	}
	...
}
Michael isn't advocating this particular Bank Account design, he was trying to illustrate that he seeks a way to modify existing code by inserting mock objects at particular points. I don't have a way to modify existing code inside of a class without rewriting the class, but I do have a way of writing classes in such a way that we can reduce dependencies (i.e. decrease couping).

For me this is a perfect example of why abstraction is useful at the implementation level and it ties in with my previous blog entries (OOP Case Study: A Bank Account class Part 1 and Part 2) about how an Account class should be built from the group up using different layers of abstraction.

Here we have an Account class which is overloaded with responsibility, it is the lowest level of representation of the Account, but it also carries the responsibility of commiting all deposits to a log. The purpose of abstraction is to separate responsibilities into different layers of abstraction. The account class would have been much better off if it was written as two classes and one interface:

class RawAccount 
{
  private balance = 0;
  deposit(value) {
    balance += value; 
  }
  ...
} 

class TransactionalAccount
{
  RawAccount acc;
  private log = new TransactionLog();	
  deposit(value) {
    log.addEntry(balance, value);
    acc.deposit(value);
  }
  ...
}

interface IAccount {
  deposit(value);
  ...
}
Whereever the original Account class was used it should be replaced with the IAccount interface type. This alone though still hasn't solved the problem though of replacing TransactionLog() with a mock object but it does do one thing, it makes it easier to refactor existing code.

In order to have a mock object it would be easy and logical to add a template parameter to TransactionAccount which provides the concrete type of the transaction log:

class TransactionalAccount<class LogType>
{
  RawAccount acc;
  private log = new LogType();	 
  deposit(value) {
    log.addEntry(balance, value);
    acc.deposit(value);
  }
  ...
}
This small change requires a rewriting of only the relatively few points where the TransactionalAccount is instantiated. This is an example of how increased abstraction reduces code coupling making code easier to manage and modify. Some may argue that we can't always change existing code, and I agree. It is though a very reasonable practice to instill in an organization which is attempting to follow a test driven design methodology. We can't always change existing poorly designed code, but we can always encourage better coding practices in the future.

If programming history has taught us anything it is that more abstraction is generally a good thing.


Bruce Eckel

Posts: 875
Nickname: beckel
Registered: Jun, 2003

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 18, 2004 5:04 PM
Reply to this message Reply
Which language are you programming in? I thought it was Java until I saw:


class TransactionalAccount<class LogType>


Which is neither C++ nor Java. You mention template parameters which implies C++, but the rest of your code appears to be Java.

Christopher Diggins

Posts: 1215
Nickname: cdiggins
Registered: Feb, 2004

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 18, 2004 6:21 PM
Reply to this message Reply
My apologies, I am writing in psuedo-Java 1.5. I am not familiar with the precise syntax for templates/generics in Java.

Steve Love

Posts: 20
Nickname: essennell
Registered: Sep, 2004

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 18, 2004 11:53 PM
Reply to this message Reply
From what I have seen so far, it appears that the problem lies not in lack of features in the languages used, but rather ineffectual use of abstraction in code.

Yup. The use of mocks requires not only sensible abstraction but decoupled objects too. Michael Feather's blog wanted to be able to instantiate any class regardless of the dependencies - a difficult problem to be sure!

One way I've found which makes mocking easy is the wide use of interface classes. You touch on this here, too.

What you describe at the end is what I know as "paramaterise from above", another way to reduce dependencies. You "parameterise" with a generic type. In this example, however, I think I'd have an interface class for the logging type, and pass that to the account.

interface ILog
{
}
 
class Account : IAccount
{
    Account( ILog log );
}
 
//...
 
IAccount testAccount = new TestAccount( new MockLog() );
IAccount realAccount = new TransAccount( new TransactionalLog() );


Note that IAccount is an interface allowing accounts to be mocked elsewhere for testing, too!

I'd do it this way because it might be useful to be able to create a named object of MockLog type, and fiddle with it to cause different behaviour in the account class.

Wide use of interfaces generally reduces the coupling of a system because it forces you (the programmer) to concentrate on the interface rather than the detail. The ease of making Mock Objects when interfaces are used is just a side effect of this.

Vincent O'Sullivan

Posts: 724
Nickname: vincent
Registered: Nov, 2002

typo? Posted: Nov 19, 2004 1:53 AM
Reply to this message Reply
IAccount testAccount = new TestAccount( new MockLog() );
IAccount realAccount = new TransAccount( new TransactionalLog() );
I assume that it's a typo and that you meant to write:
IAccount testAccount = new TransAccount( new MockLog() );
IAccount realAccount = new TransAccount( new
TransactionalLog() );
otherwise the testAccount object would contain no code in common with realAccount (it being the TransAccount that you want to test).

Vince.

Steve Love

Posts: 20
Nickname: essennell
Registered: Sep, 2004

Re: typo? Posted: Nov 19, 2004 2:28 AM
Reply to this message Reply
> I assume that it's a typo and
> that you meant to write:
IAccount testAccount = new
> TransAccount( new MockLog() );
> IAccount realAccount = new TransAccount( new
> TransactionalLog() );
otherwise the testAccount
> object would contain no code in common with realAccount
> (it being the TransAccount that you want to test).
>
> Vince.

Well it depends what's under test, and where that functionality is. TBH the example was off the top of my head, and not from real code (really?).

Suppose TransAccount and TestAccount share some code via an abstract base class, which in turn exposes IAccount's interface. The transactional nature of TransAccount might be tested elsewhere - it's the common stuff being tested. TransAccount would surely have other dependencies, and mocking them all for this test is overkill, so a TestAccount object is created to exercise the common functionality.

Just an idea; I agree that tests should certainly exercise "real" code - having everything mocked out defeats the object!

Vincent O'Sullivan

Posts: 724
Nickname: vincent
Registered: Nov, 2002

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 19, 2004 2:38 AM
Reply to this message Reply
> The goals is to create a test case without having to
> actually use the TransactionLog class implementation as it stands
:
> Whereever the original Account class was used it should be
> replaced with the IAccount interface type. This alone
> though still hasn't solved the problem though of replacing
> TransactionLog() with a mock object but it does do one
> thing, it makes it easier to refactor existing code.

You've stated the problem that you're addressing (mocking the TransactionLog class) but actually addressed a different problem (making the code easier to refactor).

To get a mock Log into the account class you implement the Log as an interface (as you stated) and then pass either the real Log implementation or the mock Log implementation into the class via its constructor (or less optimally, via a setLog(Log l) method. e.g.
class Account
{
    private int balance = 0;
    private Log log;
 
    Account(Log newLog)
    {
        this.log = newLog;
    }
 
    deposit(value) 
    {
        log.addEntry(balance, value);
        balance += value;
    }
	...
}
 
interface Log
{
    void addEntry(int b, int v);
}
 
class TransactionLog implements Log
{
    :
}
 
class MockLog implements Log
{
    :
}
I'm not familiar with Java 1.5 generics either, but I'm fairly confident that it doesn't add any anything that's needed to address this particular problem.

Vince.

Michael Feathers

Posts: 448
Nickname: mfeathers
Registered: Jul, 2003

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 19, 2004 4:57 AM
Reply to this message Reply
> The post is very interesting in that it does a good job
> summarizing points that I have seen made elsewhere with
> regards to difficulty in inserting mock objects into
> existing code. From what I have seen so far, it appears
> that the problem lies not in lack of features in the
> languages used, but rather ineffectual use of abstraction
> in code.
...
> This small change requires a rewriting of only the
> relatively few points where the TransactionalAccount is
> instantiated. This is an example of how increased
> abstraction reduces code coupling making code easier to
> manage and modify. Some may argue that we can't always
> change existing code, and I agree. It is though a very
> reasonable practice to instill in an organization which is
> attempting to follow a test driven design methodology. We
> can't always change existing poorly designed code, but we
> can always encourage better coding practices in the
> future.
> <p/>

Chris, I'm glad you posted this because it gives me an opportunity to say something that I don't think I would have otherwise.

I agree with you, that programmers need to learn how to use abstractions better, and the bulk of my consulting and teaching has been focused on that. But, where I think we fail in the industry is that we have not accepted the reality that there is a lot of really crappy code out there. A lot.

I visit and work with ~10 teams a year. I'm in regular contact with a larger number, and while they appreciate learning how to use abstractions well, and they do, they also have to deal with their old crappy code every day.

The technique you mentioned above is part of one I call Template Redefinition in my book Working Effectively with Legacy Code. Great technique if you have generics. In the write up I mention that if you are in a language that allows type aliases (like C++ with typedef) you can rename the class as you make it a template and then typedef it to the old name in the header. When you do that, you don't have to rename all of the uses in the code base. In some code bases there aren't just a few places where you would've had to do that renaming, there are thousands. For this example, another technique actually is simpler to apply. It is called Parameterize Constructor. I like to reserve Template Redefinition for cases where template arguments parameterize classes in classes. In those cases, Template Redefinition is just about all you can do.

But techniques aside, I agree with you. What you outline is a standard message we've had in the industry: improve design skill. But, I think we need to focus one something else as well. We need to realize that many people have screwed up and that there are things we need to do to fix that beyond helping people acquire better design skills.

And, that could mean designing languages as if things could go wrong.

Steve Love

Posts: 20
Nickname: essennell
Registered: Sep, 2004

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 19, 2004 7:09 AM
Reply to this message Reply
> But techniques aside, I agree with you. What you outline
> is a standard message we've had in the industry: improve
> design skill. But, I think we need to focus one something
> else as well. We need to realize that many people have
> screwed up and that there are things we need to do to fix
> that beyond helping people acquire better design skills.
>
> And, that could mean designing languages as if things
> could go wrong.

What does this mean?

Java and C# were created (partly at least) because there was a lot of crappy C and C++ code "out there", and some of the ways in which that crappy code could be written was taken out of Java and C#.

Now we also have a lot of crappy code written in Java and C#, none of which helps the fact that crappy code in any language is written by programmers who:

a) don't care
b) haven't sufficient training
c) haven't sufficient time to care or take care
d) try real hard but still write crappy code

(d) are the people who will read forums like this one, learn stuff, become better programmers. To some extent, those in category (b) and (c) will too. IME it's those in (a) who write the most code - make it work NOW, release it and forget it ever existed.

More new languages are unlikely to change that, and can never change the fact that there is a lot of crappy code written in lots of different languages, until we have an AI compiler that can read bad C++ (or whatever) and spit out great x-new-funky-language.

Techniques such as the ones you mention are important when trying to write good new code and interface with legacy code. That doesn't take away the fact that if new code is written with better design and coding techniques, it's still better code. If we can push the average standard up just a bit, it's an improvement.

Christopher Diggins

Posts: 1215
Nickname: cdiggins
Registered: Feb, 2004

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 19, 2004 7:29 AM
Reply to this message Reply
Hi Michael,

I think your point is a very salient one. The current state of affairs for the majority of code is truly pathetic. For Heron I took a pessimistic approach to code but rather than anticipating the need to repair legacy code, what I did was try and enforce, or at the very least encourage, good practices from the beginning.

I think that giving programmers new ways to write horrible code (i.e. by violating encapsulation), would be a very dangerous thing for a new language. Programmers are like children, if you give them a bunch of tools to play with, you can count on them starting with the most dangerous. I am though on the other hand, optimistic that once Heron is ready for prime-time, programmers will be more mature in their understanding and application of abstractions, and organizations will stop letting unadulterated garbage into their source code.

Mind you I am sympathetic to your concerns, and once Heron matures, there may come a point where we need to talk again, and you can say Chris, see I was right, look at all the crappy legacy Heron code we have to repair now. I will then concede that my experiment in trying to advance the state of programming failed, and I will modify Heron to give whatever it takes to get the job done.

As a final note the functionality you described is available in AspectJ, http://eclipse.org/aspectj/ . AspectJ was designed expressly to allow the modification of existing code unobtrusively.

Christopher Diggins

Posts: 1215
Nickname: cdiggins
Registered: Feb, 2004

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 19, 2004 7:38 AM
Reply to this message Reply
Hi Steve,

> From what I have seen so far, it appears that the
> problem lies not in lack of features in the languages
> used, but rather ineffectual use of abstraction in
> code.

>
> Yup. The use of mocks requires not only sensible
> abstraction but decoupled objects too.

Good point. I sometimes interchange decoupling and abstraction in my writing and speaking. I always use abstraction to decouple concerns in my software. I am not sure honestly how abstraction is useful if it doesn't decouple objects.

> Michael Feather's
> blog wanted to be able to instantiate any class regardless
> of the dependencies - a difficult problem to be sure!
>
> One way I've found which makes mocking easy is the wide
> use of interface classes. You touch on this here, too.
>
> What you describe at the end is what I know as
> "paramaterise from above", another way to reduce
> dependencies. You "parameterise" with a generic type. In
> this example, however, I think I'd have an interface class
> for the logging type, and pass that to the account.

I like the interface approach you outline very much as well. The interfaces version you describe is even more general than the generic version because:
a) the type is not required to be known at compile-time
b) the constructor may now be non-trivial

I am pleased to see use of interfaces being encouraged. They are a central theme for the Heron programming language http://www.heron-language.com , and the C++ Object Oriented Template Library http://www.cdiggins.com/ootl.html .

Ravi Venkataraman

Posts: 80
Nickname: raviv
Registered: Sep, 2004

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 19, 2004 8:06 AM
Reply to this message Reply
The problem here seems to be that we want a way to test the code without changing the original code.

The solutions proposed suggest redesign, or changing the Account class/interface to include a new constructor that takes a <Log> object.

Is there a way we can test this without having to change the original code? Let us assume that the original code is based on good design and ougth not to be touched.

The key issue here relates to the behaviour of the TransactionLog class. Its desirable behaviour is to have it work normally in production, but not do anything while testing.

Therefore, one solution that suggests itself is to have a parameter (system property) set while testing that essentially says, do nothing.

Using a system property the code would be:

class TransactionLog{
   public TransactionLog()
   {
      // if (system.property == "test") do nothing
      // else
      //    do the usual things.
   }
   ....
}


One advantage of using System level properties is that we do not have to change existing code except for the TransactionLog class. Even this change is minimal.
Another advantage is that we can easily use this for debugging. A somewhat more sophisticated implementation could let the property be set at a user/session level and
applied to the session rather than to the whole application.


Another solution is to add a parameter to the constructor of TransactionLog. This would result in code that's already been suggested in this thread.

class TransactionLog{
   public TransactionLog(boolean testing){
      if (! testing)
        // do usual thing 
      else
        // do nothing
        
   }
   ...
}
[\java]
 
This option of changing the TransactionLog constructor does require changing the Account class too. Furthermore, changing the TransactionLog constructor will affect other classes that use TransactionLog. Assuming that the Account class/interface was designed properly, I would like to cause as little change to the Account class as possible. 
 
The System Property method is less intrusive, requiring change only at one place, and the general idea is applicable to other situations. 
 
Note that using the system method is akin to using AOP (almost, but not quite).

Bruce Eckel

Posts: 875
Nickname: beckel
Registered: Jun, 2003

How do you tell someone their code/design is bad? Posted: Nov 19, 2004 8:25 AM
Reply to this message Reply
Michael, I'd like to start a discussion about this:
http://onthethought.blogspot.com/2004/11/how-do-you-tell-someone-their.html
The publisher just sent me your book; perhaps you were suggesting that you discuss it there.

Michael Feathers

Posts: 448
Nickname: mfeathers
Registered: Jul, 2003

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 20, 2004 6:00 AM
Reply to this message Reply
> > And, that could mean designing languages as if things
> > could go wrong.
>
> What does this mean?
>
> Java and C# were created (partly at least) because there
> was a lot of crappy C and C++ code "out there", and some
> of the ways in which that crappy code could be written was
> taken out of Java and C#.
>
> Now we also have a lot of crappy code written in Java and
> C#, none of which helps the fact that crappy code in any
> language is written by programmers who:
>
> a) don't care
> b) haven't sufficient training
> c) haven't sufficient time to care or take care
> d) try real hard but still write crappy code
>
> (d) are the people who will read forums like this one,
> learn stuff, become better programmers. To some extent,
> those in category (b) and (c) will too. IME it's those in
> (a) who write the most code - make it work NOW,
> release it and forget it ever existed.
>
> More new languages are unlikely to change that, and can
> never change the fact that there is a lot of crappy code
> written in lots of different languages, until we have an
> AI compiler that can read bad C++ (or whatever) and spit
> out great x-new-funky-language.
>
> Techniques such as the ones you mention are important when
> trying to write good new code and interface with
> legacy code. That doesn't take away the fact that if new
> code is written with better design and coding techniques,
> it's still better code. If we can push the average
> standard up just a bit, it's an improvement.

Steve, thanks. In the blog entry that Chris quoted I was talking about adding first class support for testing in languages. The idea was that tests could have special access to classes to allow easy mocking without affecting the encapsulation of the code in production.

As I mentioned in the blog, this by itself could have some undesirable effects. If it is hard to test something, that in itself is valuable design feedback, so a little cheat like that in a language could be slightly counter-productive. However, it would help us with one case: the case where you have programmers who didn't know any better and dug themselves into a hole. A feature like that could help them dig out. And, as I mentioned before, I think that we have to open our eyes and realize that there are a lot of people who are in this situation.

I'm all for teaching people how to design better and helping organizations alter their development so that they aren't continually under the gun. It is what I do for a living. But, to me, it is only half of what needs to be done.

Michael Feathers

Posts: 448
Nickname: mfeathers
Registered: Jul, 2003

Re: OOP Case Study: The Bank Account Class (Part 3) Posted: Nov 20, 2004 7:13 AM
Reply to this message Reply
> I
> am though on the other hand, optimistic that once Heron is
> ready for prime-time, programmers will be more mature in
> their understanding and application of abstractions, and
> organizations will stop letting unadulterated garbage into
> their source code.

That's something to wish for. :-) But, you know, entropy wins a lot of the time. Best of luck, though.

> As a final note the functionality you described is
> available in AspectJ, http://eclipse.org/aspectj/ .
> AspectJ was designed expressly to allow the modification
> of existing code unobtrusively.

As I mentioned in the blog, AOP is nice, but it isn't quite as tuned toward testing concerns as what I was proposing.

Flat View: This topic has 19 replies on 2 pages [ 1  2 | » ]
Topic: JavaPolis 2004 Previous Topic   Next Topic Topic: Implementation Driven Design

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use