public class Foo {
private static class Bar {
public void someMethod() {
}
}
}
Unit test someMethod.
More realistic, concrete examples are harder to describe because they involve an interaction over time between a somewhat vague problem description and exploration of a solution state space, where the abstractions chosen are fluid and slide around before they get into a good shape.
I, for one, tend to write code from the bottom up, i.e. creating hypothetical abstractions, small tools etc. and start composing them to solve the next problem up the abstraction stack. Then, when I find it doesn't fit quite right, I adjust, freely throwing away abstractions, rewriting them, reshaping, until the level of my tools fits the problem better. I gradually build up my abstraction level until I can move problem domain level mountains with little effort. Doing this in the form of tests just doesn't work (for me).
Writing code from the top down isn't the right answer either; not all people think that way, and besides, it can lead to solutions with very ugly implementations - the grain of the wood should influence the design of the house, if you will.
As Uncle Bob describes in his book, you never even make abstractions until you have proven duplication. You make your abstractions based off of evidence, not hypothetically planned out.
Kent Beck in his book doesn't recommend top-down, or bottom-up, but rather from known-to-unknown. Start with what you know, and work toward what you don't know.
Edit: The books I refer to are Test Driven Development: By Example - Kent Beck, and Clean Code - Robert Martin
Abstractions aren't merely for duplication removal. Abstractions are for symbolic chunking; for thinking about things at different levels.
We don't use e.g. units-of-measure types [1] because they remove duplication from our code; if anything, they add duplication. They do however clarify our thinking with help from the type checker.
I think a lot in terms of flows / pipes / functional transforms. So I tend to try and express problems in that shape, because I have a lot of mental tools that I can apply, and I know they're extremely easy to test in isolation. Creating a pipe-like thing means reducing it to a simple common push or pull stream pattern. But it's not duplication I'm removing here; I'm actively introducing an abstraction because it has proven power for creating good software and solving problems.
I try and create few, minimal abstractions that can be applied widely. Take a cue from functional languages and split out types from algorithms; if you make your types more general, you increase the reusability of your algorithms. Classical OO design tends to create a lot of types that are specific to the domain model. I happen to think that OO designs are usually poor; they tend to have a high code complexity to implemented logic ratio, and require awkward composition that leaves details hanging out.
I spent 20 years writing OO software and was a big fan, especially in the early 2000s. I still work in OO languages, but most OO code I read makes me wretch now.
I don't recognize either Kent Beck or Bob Martin as particularly noteworthy for good architectural design (Beck is on the right path with agile though). In fact, I blame TDD for Java-itis: proliferation of single-implementation interfaces (has there ever been a worse idea more often propagated by cargo culters?), poorly abstracted object graphs, leaky implementation abstractions, and more.
> Start with what you know, and work toward what you don't know
I've been programming for more than 25 years. A lot of stuff has changed in that time; what hasn't changed is patterns of abstraction. So I start out with abstractions, chosen from experience.
Read Norvig on TDD [2] - his experience matches mine. If you know a lot of software tools (i.e. abstractions, algorithms, approaches), you can apply them to a problem. TDD is a poor tool for creating new tools, though. And if you rely on code duplication for creating a tool, well, you're going to have a bad time with hard problems.
More realistic, concrete examples are harder to describe because they involve an interaction over time between a somewhat vague problem description and exploration of a solution state space, where the abstractions chosen are fluid and slide around before they get into a good shape.
I, for one, tend to write code from the bottom up, i.e. creating hypothetical abstractions, small tools etc. and start composing them to solve the next problem up the abstraction stack. Then, when I find it doesn't fit quite right, I adjust, freely throwing away abstractions, rewriting them, reshaping, until the level of my tools fits the problem better. I gradually build up my abstraction level until I can move problem domain level mountains with little effort. Doing this in the form of tests just doesn't work (for me).
Writing code from the top down isn't the right answer either; not all people think that way, and besides, it can lead to solutions with very ugly implementations - the grain of the wood should influence the design of the house, if you will.