I'm experiencing a rare treat: my current total immersion team is working on a brand-new, green-field product. We've had to create a new codebase entirely from scratch using somewhat unusual technology. Combine that with my immersion coaching and you have the recipe for a lot of interesting exploration.
Lately, we've been exploring software architecture. How can we make our code testable, clear, concise, and mutable? We've come up with some interesting ideas, which makes me want to try more ideas on my own. The problem is that exploring architecture takes a lot of code. It's hard to have a problem that's small enough for one person to write in a spare afternoon, but large enough to be architecturally interesting.
(How do I define "architecture," you ask? Well, it all has to do with... Look, a penguin!)
So I've created this challenge. It's artificial--very artificial--but I think it raises several interesting architectural questions. It has a UI, a persistence mechanism, and test/production configuration differences. It's a toy problem, to be sure, but I hope the rules I provide make it relevant. If not, I'll expand the problem.
I have some architectural ideas I intend to explore in a future essay, and I hope you'll chime in with your own ideas. How would you solve this problem using mock objects? What about a dependency injection framework? How about 100% immutable objects? Aggressively late evaluation? Share your solutions and we'll see how they compare.
The Challenge: Part One
Write a command-line program that loads a file into memory, ROT-13s it, displays the result on the screen, and writes the result to a file.
For example, assume you had a file named
in.txtthat contained the text "The dog barks at midnight." Running the command
rot13 in.txt out.txtwould display "Gur qbt onexf ng zvqavtug." on the screen and write the same text to a file named
Of course, ROT-13 isn't the challenge. That can be done trivially from a Unix command prompt. No, the real challenge is to demonstrate an architectural approach that would hold up in real-world use. So here are the real rules:
- Write the ROT-13 tool described above. Note that the input file must be loaded entirely into memory.
- Provide a configuration mechanism that reads and writes files from one directory when "in testing" and another directory when "in production."
- Wrap your platform's console and file I/O methods as if each call actually required a dozen lines of code. Your configuration mechanism can use file I/O directly, though.
- No slideware allowed. You can use diagrams to illustrate your architecture, but you must code the entire solution. This prevents seemingly-elegant solutions that don't work in practice.
- Use test-driven development to code everything.
Judge solutions on:
- Test Design.
- All code is tested. (See my book for precise definitions of the terms unit test, focused integration test, and end-to-end integration test.)
- The test code is as high-quality as the production code.
- All business logic code (that is, the ROT13 code) is tested with unit tests that don't do any I/O.
- All integration code (that is, the file and console I/O) is tested with focused integration tests that validate that the I/O libraries each work as expected, independently of the rest of the system.
- End-to-end integration tests (that is, tests that check the system as a whole) are kept to a minimum, or better still, entirely unneeded.
- Coupling and Cohesion.
- The code reflects the usage and the external behavior of the system more than it reflects the techniques used in coding, so that any concept (such as "loads and saves files" or "rot13") can be clearly related to specific code.
- Code related to a single concept is grouped together.
- Code for unrelated concepts (such as "rot13" and "file handling") is stored in separate locations.
- Classes' public interfaces are clear and easy to understand.
- Individual lines of code read well.
- Boilerplate code is kept to a minimum.
- Lines of code are kept to a minimum.
- There is no code that anticipates part two of the challenge.
- Quality Without a Name.
- The code feels beautiful and well designed.
- The system follows the spirit of the rules: it illustrates a general architectural approach, not a problem-specific hack.
The Challenge: Part Two
Once you have part one coded, post it. Then modify your code to support arbitrarily large files. In other words, rather than loading the whole input file into memory, operate directly on the input stream. Post your result.
Judge this portion of the challenge using the rules for part one, and also:
- A minimum number of lines of code needed to be changed.
- A minimum--preferably zero--of the code not related to file-handling needed to be changed.
- Most--preferably all--of the file-handling changes were in a single place.
Post Your Solutions!
This challenge isn't particularly difficult, but I suspect that it will be hard to create code that's well tested and performs optimally on all of the other points. In particular, I think the unit testing requirement and limitation on end-to-end tests will be difficult. (I haven't tried it yet, so I can't be sure.) Give it a try! I'd particularly like to see some baseline solutions, such as a classic three-layer architecture using mock objects or heavy use of a dependency injection framework, as well as some more radical ideas.
If you give this challenge a try, please post a link to your solution in the comments. I'm looking forward to seeing what you come up with.