Friday, October 3, 2008

How to Write Great OO Day One

There's no shortcut to experience. Writing good object oriented code takes experience, but here are three practices to help you get off on the right foot day one with even the grumpiest of gray beards:

  1. Write all your code using Test Driven Development(TDD)

  2. Follow the Rules of Simplicity

  3. Tell Don't Ask


Write All Your Code Using TDD


Code written test first and code written test last is very very different code. Code written test first is loosely coupled and highly cohesive. Code written test last often breaks encapsulation when some property or private method needs to be exposed to the test because the class wasn't designed to be tested. If you write your code test first, your dependencies will be better and your code will be loosely coupled and highly cohesive. More on this later.

Follow the Rules of Simplicity


Code is simple when it:

  1. Runs all the tests

  2. Contains no duplication

  3. Expresses all the intent

  4. Utilizes the fewest classes and methods


It's important to notice that I've used an ordered list. The order is important. A single GodClass with a single main() method is not simple. It may run all the tests, but in any program more complex than "Hello, world!" it certainly will contain duplication and not express all the intent.

My struggle with the Rules of Simplicity focused around the If Bug. I didn't understand how following the rules of simplicity would head off someone writing if heavy code. One could argue, and I tried, that if heavy code does not express intent. But when you read code like

if (mobile.getType() == MobileTypes.STANDARD) {
alert();
}

It's pretty darn easy to see the intent. In the context of whichever method this is in, if the mobile is of type STANDARD then alert. How much more intent do you need?

Then I had a little epiphany. If there's code like that, then certainly in other places in the code there's more. There's probably code like:

if (mobile.getType() == MobileTypes.GAS) {
registerGasReading();
}

and

if (mobile.getType() == MobileTypes.TEXT) {
sendTextMessage();
}

and

if (mobile.getType() == MobileTypes.LOCATION) {
notifyLocation();
}

Do you see it? I sure do. Violations of rule 2. Many many violations of rule 2. And the worst kind of violations of rule 2. Duplication in many different pieces of the code. Duplication that is going to be very very hard to find. So to help prevent this, I've included

Tell Don't Ask


Tell Don't Ask simply means don't ask an object about it's state and then do something. Tell the object to do something. This means all those if examples become:

mobile.alert();

and

mobile.registerGasReading();

and

mobile.sendTextMessage();

and

mobile.notifyLocation();

Now suppose that there were some of those if clauses splattered throughout the code that had duplicate implementations. In the if heavy version, they would be very hard to find, but in the tell don't ask version, all the implementations are in Mobile. All in one place and ready for sniffing out and eradicating.


Listening to your tests will also help you keep your code simple.

public interface Alarm {
void alert(Mobile mobile);
}

public class Siren implements Alarm {
public void alert(Mobile mobile) {
if (mobile.getType == MobileTypes.STANDARD) {
soundSiren();
}
}
}

public class TestSiren extends TestCase {
public void test_alert() {
LocationMobile mobile = new LocationMobile();
Siren siren = new Siren();
siren.alert(mobile);
assert(sirenSounded());
}
}

If you listened closely to your test, it would be asking you, "Why do you need a LocationMobile to test the Siren?" Why indeed? It seems that Sirens shouldn't even know about LocationMobiles.

public class LocationMobile {
private Alarm alarm;
public LocationMobile(Alarm alarm) {
this.alarm = alarm;
}
public void alert() {
alarm.alert(); // alert on Alarm no longer needs a mobile
}
}

public class TestLocationMobile() extends TestCase {
public void test_alert() {
Alarm alarm = EasyMock.createMock(Alarm.class);
alarm.alert();
EasyMock.replay(alarm);
Mobile mobile = new LocationMobile(alarm);
mobile.alert();
EasyMock.verify(alarm);
}

It looks like I've just swapped the dependencies. Instead of Alarm depending on Mobile, I now have Mobile depending on Alarm. But if you look closely at the test, the real dependency was Siren knowing about LocationMobile. A concrete class depended on another concrete class. This violates the Dependency Inversion Principle(DIP). The second example has LocationMobile depending on the interface Alarm. A concrete class depending on an abstraction. This satisfies DIP.

If you write all your code TDD, follow the Rule of Simplicity, and Tell Don't Ask then you'll be on the path to becoming a better OO programmer. Good OO code is easy to read and maintain, but can be hard to write. At least at first. The more you write the better you'll become, and the more experience you will get. In the meantime, these practices should get you well on your way.