Last week Chet and I taught a delightful XP Experience class in Delaware. As often happens–where by often I mean always–the participants ran into trouble trying to apply Simple Design ideas when it came time for the system to demonstrate the ability to store and retrieve information. Let’s talk about that.
Hmm. First we have to think of an app. I don’t want to do the one we use in the class: that would make it too easy for the next vict^H^H^H^H lucky participants. I’m at the Seattle’s Best coffee shop in Borders, so how about this:
Whenever someone buys something in our coffee shop, we remember how many things they have bought, and every time they buy another five things, the sixth one is free. People and their purchases are recorded by their membership number in our Constant Caffeination Club.
Clearly this requires a database. Surely we will have millions of members in the CC Club as soon as the word gets out about this fabulous deal. We can’t possibly build this system in a TDD style without using some kind of database software.
To make this even more difficult, we’ll build the software in Python, since I just got Python and haven’t much of an idea how to program in it. Fortunately, I also have a trial of JetBrains’ PyCharm, which is very helpful. I’ll be buying that before the week is out, I think. Naturally, I begin with a hookup test:
import unittest class NoDBTests(unittest.TestCase): def test_hookup(self): self.assertTrue(True) if __name__ == '__main__': unittest.main()
With great good fortune, this goes green. Now let’s TDD a class into existence. I’m thinking I’ll start with the Club member record, and … oh, let’s see … the way it works is that we get the member record and when they buy something for X dollars, we send the member record a message and it responds with the amount to charge the member … either X or zero, probably. This will take a couple of tests, or one growing test. Let’s see:
def test_memberCharge(self): member = MemberRecord() chargeAmount = member.charge(10) self.assertEqual(10,chargeAmount)
This ought to serve to get us going. PyCharm tells me there is no MemberRecord class and offers to build it for me. I allow that. It builds it into the current file. I can live with that for now. I expect that when I run the tests it will go red-bar on charge not existing … and it does. I will add that, as follows:
class MemberRecord(object): def charge(self, amount): return 0
This gets me the next expected red-bar, namely 10 != 0, telling me that my implementation of charge() is wrong. Naturally, I fix it perfectly like this:
class MemberRecord(object): def charge(self, amount): return 10
OK, there can be no doubt that this is the definitive implementation of charge. So it is time for another test. Let’s have our guy charge six things and check that the first five come back charging, and the sixth comes back free. I’m sure this is going to work.
def test_sixthOneFree(self): member = MemberRecord() chargeAmount = member.charge(5) self.assertEqual(5, chargeAmount) chargeAmount = member.charge(5) self.assertEqual(5, chargeAmount) chargeAmount = member.charge(5) self.assertEqual(5, chargeAmount) chargeAmount = member.charge(5) self.assertEqual(5, chargeAmount) chargeAmount = member.charge(5) self.assertEqual(5, chargeAmount) chargeAmount = member.charge(5) self.assertEqual(0, chargeAmount)
Damn. Didn’t quite work. Turns out returning 10 was wrong. I need to return the amount sent in. Easy fix:
class MemberRecord(object): def charge(self, amount): return amount
Darn. (Not quite as bad as damn.) The first five asserts worked fine but the sixth one returned five instead of zero. What’s up with that?
N.B. I’m not really this stupid. I’m just having fun. Really. You can trust me on this.
Anyway, we need to implement the counting from one to five to make this work. Should be easy enough:
class MemberRecord(object): def __init__(self): self.__purchaseCount = 0 def charge(self, amount): self.__purchaseCount+=1 return amount if self.__purchaseCount != 6 else 0
Pretty big change there, but let me point out that it was mostly addition, not rewriting.
Now of course, I was just doing the absolute simplest thing there, and it is clear that a test of more than six purchases will fail to give any more freebies. Let’s try 12:
def test_twelvthOneFree(self): member = MemberRecord() for i in range(5): member.charge(6) chargeAmount = member.charge(6) self.assertEqual(0, chargeAmount, "first one bad") for i in range(5): member.charge(6) chargeAmount = member.charge(6) self.assertEqual(0, chargeAmount, "second one bad")
I did this in two steps to be sure that the first one still works. Sure enough, the test fails saying “second one bad”. I’m thinking I have a fix for that … I’ll do a mod check on the count. I could zero it but I think the mod is easier and a bit more flexible. I understand mod, so others might disagree. Anyway here goes:
def charge(self, amount): self.__purchaseCount+=1 return amount if self.__purchaseCount%6 != 0 else 0
As I thought, this works just fine. We are green all the way, and I’m sure that the system is working just fine. Might be a good idea to beef up this final test just a bit, to make sure that it’s not always returning zero for some reason. Like this:
def test_twelvthOneFree(self): member = MemberRecord() for i in range(5): self.assertEquals(6,member.charge(6)) chargeAmount = member.charge(6) self.assertEqual(0, chargeAmount, "first one bad") for i in range(5): self.assertEquals(6,member.charge(6)) chargeAmount = member.charge(6) self.assertEqual(0, chargeAmount, "second one bad")
Still green, and a bit more robust. Naturally, you could have gone faster, but you’re not writing an article on how to do TDD, and you probably don’t like going down to the finest grain possible. I do. One result of that is that never once in this bit of coding did I get a surprise. (Yes, I was faking it. I wanted you to think I was having a good time.)
I like working with no surprises. It gives me confidence that I’m coding what I set out to code. When I do get a surprise, I try to back out my last changes, and go in smaller steps. When I forget to do that, and start debugging instead, pretty soon I’m strewing print statements around or even going into the debugger. So far I don’t know how to work the PyCharm debugger and I’m hoping never to learn.
Hey! I thought this was supposed to be about databases???
Hey, yourself. Try to pay closer attention. This is totally about databases.
Why is this even remotely about databases???
Well, here’s why:
We have just defined a core element of our member database record, namely the purchase count, and made sure that it works. Now when we read a member record in from our database, we can instantiate it into our class MemberRecord and send it messages to decide what to do. This is OO here, my young padawan, and that’s how we do it.
Yeah, sure, old man, but what about the membership number? That's the key, according to the story, and you don't even have it. And isn't there just a small matter of actually storing and retrieving these babies?
Patience, youngling. Each thing comes in its time. Let’s see what the next tests bring us.
I’m thinking we need something like this: We’ll have two members, say numbers two and six. You are number six. We’ll have them each buy some stuff, in some alternating pattern, and make sure that they each get their freebies at the right time. I’ll start with this test:
def test_twoAndSixBothGetFreeStuff(self): members = MemberCollection() for purchaseFromTwo in range(5): self.assertEquals(4, members.member(2).charge(4)) self.assertEquals(0, members.member(2).charge(4))
This isn’t my complete plan, but it is enough to fail. The idea here is that MemberCollection is my database of MemberRecords, and the method member() returns the MemberRecord whose number is given. First cut solution goes like this:
class MemberCollection(object): def member(self, memberNumber): return MemberRecord()
We don’t really have a collection yet: we just return a new member every time. The result of this, of course, is that we always start at zero purchases, so the sixth purchase is not free.
I can see at least two ways to go from here. I could save a single record and return it always. That would make it work for one person and not for the second, because their purchases would add together. I could make two records, and return the appropriate one, simulating the collection.
There is also a need to add the member number to the MemberRecord, which I will do by extending the constructor. I’ll have to choose between a default number, or modifying the tests. I’ll figure that out when I get there.
Then finally I’ll need a collection, which I can drive out with a third member, or refactor in when the current test works. It’s early days yet. I don’t mind speculating about these things, but I’m not going to do them until the time comes. I think first, I’ll just save one record. That should get this part of the test going, then I’ll extend the test, or write a new one, to break the one-record solution.
class MemberCollection(object): def __init__(self): self.__record = MemberRecord() def member(self, memberNumber): return self.__record
That gets our current test working: the single record in the database works for for our one guy. Now I can extend this test, or write a new one. If I write a new one, this one should be renamed. I’ll do that.
def test_memberTwoStillGetsFree(self): members = MemberCollection() for purchaseFromTwo in range(5): self.assertEquals(4, members.member(2).charge(4)) self.assertEquals(0, members.member(2).charge(4)) def test_memberSixDoesNotInterfereWithTwo(self): members = MemberCollection() for purchaseFromTwo in range(5): self.assertEquals(4, members.member(2).charge(4)) self.assertEquals(4, members.member(6).charge(4), "six interfered") self.assertEquals(0, members.member(2).charge(4), "two got no discount")
This fails as expected, “six interfered”. Member six and member two are sharing the same record. I will fix this by making two records:
class MemberCollection(object): def __init__(self): self.__record2 = MemberRecord() self.__record6 = MemberRecord() def member(self, memberNumber): return self.__record2 if memberNumber == 2 else self.__record6
And our test is green. We now have a database of records. A very small database, mind you, but it is working perfectly for our two members. What’s next? Well, we could refactor a bit, but it seems to me that we’d really be putting in new functionality. So I’ll write another test. I think I’ll process three people at once this time, number two, number six, and number one.
Also, while I’m thinking of it, I think I’ll create some manifest constants for those magic person numbers, as they are not showing up very well in the tests. Here goes:
class NoDBTests(unittest.TestCase): personOne = 1 personTwo = 2 personSix = 6 ... def test_threeAtOnce(self): members = MemberCollection() for purchaseNumber in range(5): for person in [2, 6, 1]: self.assertEquals(3, members.member(person).charge(3), "improper free one") self.assertEqual(0, members.member(self.personOne).charge(3)) self.assertEqual(0, members.member(self.personTwo).charge(3)) self.assertEqual(0, members.member(self.personSix).charge(3))
Naturally, the self.personOne changes were made elsewhere as appropriate. You can look for those when I provide the final code at the end. Right now, we get the “improper free one” message, as the current implementation of the database will reuse person number six’s record when asked for person number one. (Interesting how that turned out if you were thinking of The Prisoner, but for our database it just won’t do.)
Now we need to build a collection that will work for as many people as we have. One way to do it, certainly, would be to build it up before the tests. We could argue that members will have to already be signed up before their purchases will be counted. Or we could add them dynamically, creating a MemberRecord for any key we don’t already have. I think I’ll go that way, mostly to learn a little more Python. With luck I won’t get in trouble. Let’s see.
I’ll make a collection named members, and a function findMember(memberNumber) that looks for a member. If it finds the member, it’ll return them, otherwise it will create a new one, add it to members, and return it. And I’ll need to add a memberNumber “member” to the MemberRecord. Got that? OK, here goes.
class MemberRecord(object): def __init__(self, memberNumber=-1): self.__purchaseCount = 0 self.memberNumber = memberNumber def charge(self, amount): self.__purchaseCount+=1 return amount if self.__purchaseCount%6 != 0 else 0 class MemberCollection(object): def __init__(self): self.members =  def member(self, memberNumber): memberRecordList = filter(lambda m: m.memberNumber == memberNumber, self.members) if len(memberRecordList) > 0: return memberRecordList else: newMember = MemberRecord(memberNumber) self.members.append(newMember) return newMember
The above code makes the tests green. We now have a MemberRecord that knows its member number, and a MemberCollection that can find MemberRecords by number, and add them on the fly as needed.
You’ll note that I named the finding method member(), not findMember(). Oversight on my part and perhaps we’ll change that now that we are green. You may be wondering how that method works. It goes like this: create a list of all the records numbered “memberNumber”. If the list has more than zero elements (by construction it can only have zero or one), return the zeroth element. If it has zero, create a new one of the desired number, add it, return it.
I’m not unduly fond of this method but it was the first thing I could think of that wasn’t an explicit loop. In retrospect, I may prefer the explicit loop. We’ll see.
You may be wondering about the lambda. That’s an inline function, returning whether x.memberNumber == (the provided) memberNumber. I think that’s how ya do that.
Anyway, now that we are green, let’s review what has happened. Then, time and space permitting, I’ll clean up the code a bit.
Database? Why Yes, In Fact It Is.
Without ever doing any SQL or other SQuirreLy database stuff, we have begun to design and implement an important record in our system, the MemberRecord. We can continue to evolve it, adding meaningful fields and behavior, as long as we want. Because the record is an object, we have at least a decent chance of encapsulating behavior in the right place.
Speaking of that, should our collection be looking at the member number? Or should we have an "isMemberNumber(n)" method on the record? Possibly the latter but the way it is now may lead more more readily toward a database solution. Viz:
We have also done important design on the database “table” that contains MemberRecords. In particular, we have identified that our only needs (so far) are the ability to find a record by its number, and to add a record of a given number. This finding simplifies any actual database we may do in the future to put these records into some persistent collection. If we are the kind of people who use stored procedures, for example, we have just identified the only ones we’ll need.
Again, so far. As we continue to evolve this little app, we may find more. The good news is that they’ll all be nicely located in the MemberRecord and MemberCollection.
And its fast. Our seven tests execute in 0.122 seconds on my ancient tablet PC.
But What About ...
Doubtless at this point you have some “what about” questions. I think you can answer them. I’m here to point the way, not drag you along the path. :) But I will take one question just to show that while I am unreasonable, I don’t want to appear that way:
When we finally plug in the real database, how should we do it so that these tests don't all break?
I’d look into topics like “pluggable behavior”. I could imagine a switch inside MemberCollection that cuts it over to using the real database code, or a different class, maybe OracleMemberCollection with the same behavior but implemented in Oracle. Either way, the switch setting or use of the new class will be plugged into the system at some higher level than we presently have represented.
Please think about other issues, and see how you might address them. For extra credit, try working this way, and solve the issues as they arise. If you keep the objects well encapsulated, I’m sure you’ll do just fine.
I think this article is more than long enough already. I’ll post the final code in a separate piece.
P.S. PyCharm is really nice. I am not associated with JetBrains in any way other than as a user. And I like it.
P.P.S. Comments are on, at least for a while.
Thanks for reading!