A composite is basically an object that contains other objects, where both the composite object and its contained objects all implement the same interface. Using the interface on the composite should invoke the same methods on all of the contained objects without forcing the external client to do so explicitly. Whew, that was a mouthful.
Here, that interface is the
matches method, which accepts a
date instance and returns a
bool. Python is a
dynamically typed language, so I don't need to define this interface formally
(which is fine by me).
How do I implement the composite? Like this:
class CompositePattern: def __init__(self): self.patterns =  def add(self, pattern): self.patterns.append(pattern) def matches(self, date): for pattern in self.patterns: if not pattern.matches(date): return False return True
The composite pattern asks each of its contained patterns if it matches the specified date. If any fail to match, the whole composite pattern fails.
I have to confess that I cheated here. I wrote more code than I needed to pass the test! Sometimes I get ahead of myself. Sorry. It turned out OK this time because all of the tests are passing, but I need to create a test that should not match, just to be sure I have everything working correctly:
def testCompositeDoesNotMatch(self): cp = CompositePattern() cp.add(YearPattern(2004)) cp.add(MonthPattern(9)) cp.add(DayPattern(28)) d = datetime.date(2004, 9, 29) self.failIf(cp.matches(d))
Cool. It passes.
It might be a little difficult to see this, but the composite contains a
DayPattern that matches the 28th and I'm matching it against the
29th, which is why I expect the
matches method to return
So I can match dates again. Big deal--I was already doing that. What about wild cards?
I'll write a test to match my anniversary with the new classes:
def testCompositeWithoutYearMatches(self): cp = CompositePattern() cp.add(MonthPattern(4)) cp.add(DayPattern(10)) d = datetime.date(2005, 4, 10) self.failUnless(cp.matches(d))
It just works. Why?
YearPattern in the composite requiring the passed-in
date to match any specific year. Wild cards now work by not specifying
any pattern for a given component. Remember when I thought I might need a
class to do the wild card matching? I was wrong!
At this point, I feel really good about the new approach and will just delete the old tests and code.
I'll also refactor the tests a bit. Did you notice that every one of the new tests contained a duplicate line? I did. It started to bother me, but that's what test fixtures are for:
class PatternTests(unittest.TestCase): def setUp(self): self.d = datetime.date(2004, 9, 29) def testYearMatches(self): yp = YearPattern(2004) self.failUnless(yp.matches(self.d)) def testYearDoesNotMatch(self): yp = YearPattern(2003) self.failIf(yp.matches(self.d))
I've only shown the first two test cases (in the fixture previously known as
NewTests) but now all of the test cases refer to the date as
self.d instead of constructing a local
It's not a huge refactoring, but it makes me feel better. You do want me to
feel the best I can about my code, don't you? Of course you do.
I did have to change
testCompositeWithoutYearMatches to use
this date instead of my anniversary. As cute as it was to throw that date in
there, I decided I'd rather have clean code without duplication than
I also took this opportunity to add some named constants for weekdays:
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(0, 7)
Now I can use these instead of the hard-coded constants I had in the weekday tests, and also delete the comments I had explaining what the constants represented. Intention-revealing code beats comments any day of the week.
Where am I now? Before switching gears, I planned to write a test for a pattern that matched the last Thursday of every month. It's time to do that now:
def testLastThursdayMatches(self): cp = CompositePattern() cp.add(LastWeekdayPattern(THURSDAY)) self.failUnless(cp.matches(self.d))
Cool. A new class to implement!
The implementation for this class is slightly more complicated than the others:
class LastWeekdayPattern: def __init__(self, weekday): self.weekday = weekday def matches(self, date): nextWeek = date + datetime.timedelta(7) return self.weekday == date.weekday() and nextWeek.month != date.month
Oops. It doesn't pass. Why?
The date I'm trying to match in the test is a Wednesday, not a Thursday! I
need to fix the test (not forgetting to rename it) to add a test where I expect
the match to fail (which I should have done before implementing
def testLastWednesdayMatches(self): cp = CompositePattern() cp.add(LastWeekdayPattern(WEDNESDAY)) self.failUnless(cp.matches(self.d)) def testLastWednesdayDoesNotMatch(self): cp = CompositePattern() cp.add(LastWeekdayPattern(WEDNESDAY)) self.failIf(cp.matches(self.d))
Rats. The first test passes but the second one fails. The date created in
setUp is the same for every test case so it will always be a
Wednesday, but I need a date that's not on a Wednesday to make this test pass.
Rather than creating a new date in this test case (and ignoring the one created
setUp), I'll move both of these tests into a new fixture--one
specific for testing the
class LastWeekdayPatternTests(unittest.TestCase): def setUp(self): self.pattern = LastWeekdayPattern(WEDNESDAY) def testLastWednesdayMatches(self): lastWedOfSep2004 = datetime.date(2004, 9, 29) self.failUnless(self.pattern.matches(lastWedOfSep2004)) def testLastWednesdayDoesNotMatch(self): firstWedOfSep2004 = datetime.date(2004, 9, 1) self.failIf(self.pattern.matches(firstWedOfSep2004))
The Open-Closed Principle
Isn't it nice being able to add new functionality without changing existing
classes? This is part of what Bertrand Meyer called the Open-Closed Principle:
"Software entities (classes, modules, functions, etc.) should be open for
extension, but closed for modification." With the new approach of using the
Composite pattern, I can extend the behavior of the system by writing a new
class with a
Now they both pass and the tests use everything they create. Nice.
While moving these tests into a new fixture, I also noticed that I created a
CompositePattern that contained only one pattern. That's kind of
pointless, so I stopped doing it.
Should I move the test cases that exercise the various pattern classes into their own fixtures? That's a tendency that many, including me, often have. It's sometimes useful to resist that urge, though. As Dave Astels, author of Test-Driven Development: A Practical Guide, puts it: a fixture is "a way to group tests that need to be set up in exactly the same way." In other words, a fixture is not a container for all of the tests for a single class, or at least, it doesn't have to be.
Having said that, I prefer it when all of the test cases in a fixture exercise the same class. In harmony with Dave's definition of fixtures, I just don't require that all of the test cases that exercise the same class be in the same fixture. Make sense?
Suppose that I have four test cases for the class
Foo but half
setUp code than the other half. I'd split those
cases up into two fixtures, even though they both exercise the same class. If I
then started writing tests for the class
Bar and discovered that some
of its tests could use the same
setUp code as one of the class
Foo fixtures, I would not just shove those tests into one
of the existing fixtures.
Wouldn't that mean I've duplicated the
setUp code for the two
fixtures? Duplication is evil! If I thought the duplication was enough to be a
problem, I would Extract
Superclass the duplicated code out of the two fixtures. Yes, you can--and
should--refactor your tests, too.
When starting on a new project, I create one fixture with no
setUp method and add all of my test cases to that one fixture.
Eventually, I reach the point where I need to refactor the fixture, and I do it.
Remember: do the simplest thing that could possibly work first. Then refactor
What, though, is the benefit of ensuring that all of the test cases in a fixture only exercise one class? Well, besides making it more cohesive (and obeying the Single Responsibility Principle (PDF)), think about what might happen when you decide a class is no longer necessary. You'll need to delete the tests for that class, too. It's a lot easier to delete a whole test fixture than to look at each test case in a fixture to see if it exercises the class you just deleted.
Think you won't delete classes? Think again. You saw me delete the
DatePattern class and all of its tests earlier, didn't you? It
wasn't hard. I felt good about it, too.