In the previous blog post, we discussed why design is needed. We saw that using abstractions (functions, classes, modules etc) is a way to allow the code to change safely. We briefly touched on the idea that introducing abstractions has a down side: it can create software that’s difficult to understand. Therefore, the challenge is to find the abstractions that best fit the problem.
How do we know which abstractions are fit and which aren’t? To answer this question, we’ll go on a quest to understand design better.
In how many ways can you print “Hello world!”?
Here are a few “Hello world!” programs:
Version 1: Classic
class HelloWorldApp { public static void main(String[] args) { System.out.println("Hello World!"); } }
Version 2: String concatenation
class HelloWorldApp { public static void main(String[] args) { System.out.println("H" + "e" + "l" + "l" + "o" + " " + "W" + "o" + "r" + "l" + "d" + "!"); } }
Version 3: Procedural style
class HelloWorldApp { public static void main(String[] args) { print("Hello World!"); } public static void print(String message){ System.out.println(message); } }
Version 4: Object oriented style
class HelloWorldApp { public static void main(String[] args) { new HelloWorldPrinter(System.out).print(); } } class HelloWorldPrinter{ public HelloWorldPrinter(PrintStream stream){ this.stream = stream; } public void print(){ stream.println("Hello world!"); } }
Version 5: Functional-like style
class HelloWorldApp { public static void main(String[] args) { LetterPrinter hPrinter = new LetterPrinter("H"); LetterPrinter ePrinter = new LetterPrinter("e"); LetterPrinter lPrinter = new LetterPrinter("l"); LetterPrinter oPrinter = new LetterPrinter("o"); LetterPrinter spacePrinter = new LetterPrinter(" "); LetterPrinter wPrinter = new LetterPrinter("W"); LetterPrinter rPrinter = new LetterPrinter("r"); LetterPrinter dPrinter = new LetterPrinter("d"); LetterPrinter exclamationPrinter = new LetterPrinter("!"); LetterPrinter newLinePrinter = new LetterPrinter(System.lineSeparator()); LetterPrinter[] helloWorldPrinters = { hPrinter, ePrinter, lPrinter, lPrinter, oPrinter, spacePrinter, wPrinter, oPrinter, rPrinter, lPrinter, dPrinter, exclamationPrinter, newLinePrinter}; helloWorldPrinters.stream().forEach(printer -> printer.print()); } } class LetterPrinter{ String letter; public LetterPrinter(String letter){ this.letter = letter; } public print(){ System.out.print(letter); } }
It took me 10 minutes to generate the “Hello world!” programs above. I’m sure if you try you can come up with many more. If a simple program can be written in so many ways, it’s obvious that complex programs can be written in virtually infinite ways. So the question is: which solution is good and which isn’t?
A Counter-Intuitive Conclusion About Design
I’m sure that while reading these examples, you’ve also judged them. You looked at some of them and thought “why do that?”. Based on what criteria did you make the judgment? I’m guessing you’ve applied a set of design principles that you have learned or discovered during your career.
The conclusion is simple, albeit counter-intuitive:
Any design is a set of constraints we as programmers choose to follow when writing code
Programmers don’t have to use classes, but choose to. We don’t have to use lambdas, we choose to do so. We don’t have to use more than one file to write a program, we choose to do so. Our choices define the design.
Design Principles
Design principles are nothing more than sets of constraints that we choose to follow. These constraints have been shown in time to work well together with a certain purpose. Since we’re focused on changeability we’ll only touch design principles that optimize for change.
I know and apply today three different sets of principles:
- The UNIX design principles, as stated in “The Art of UNIX Programming” by Eric Raymond
- The SOLID principles, as gathered by Robert C. Martin (you can learn them at our workshop )
- The Four Elements of Simple Design, as I learned them from J.B. Rainsberger and apply in my TDD and refactoring practice
and what I consider to be the core of all principles: favor low coupling and high cohesion. I combine them with elements I’ve learned about Design by Contract or Domain Driven Design. I add testability to the mix because I can change the software faster whenever I can verify it faster.
We’ll explore these sets of principles in the next articles.
Until then, did you know that we’ll discuss a lot about design at I T.A.K.E. Unconference? Register now to benefit from the collective experience of European software developers.