Home > Archive > Extreme Programming > November 2006 > "Big Picture" considerations during design
You are viewing an archived Text-only version of the thread.
To view this thread in it's original format and/or if you want to reply to
this thread please [click here]
| Author |
"Big Picture" considerations during design
|
|
| Scott Meyers 2006-11-07, 6:58 pm |
| At OOPSLA, I participated in DesignFest, an activity where groups of
people work together over the course of several hours to design a
solution to some problem. Our group was to design a system for a
vetrinary clinic. The problem description gave information about what
the system would have to be able to handle (e.g., billing, patient
histories, etc.), then listed a series of prioritized use cases.
The moderator of our group asked each person to describe how they'd
approach the problem. I said I'd do it iteratively, at each iteration
checking my evolving design against the overall problem specification to
make sure I hadn't done anything contradictory to what we'd ultimately
have to achieve. Another member of the group said he'd focus on the
first use case, coming up with tests for it and then coding tests and
solution in classic TDD fashion. I got the impression that during work
on this first use case, he'd essentially ignore aspects of the problem
not mentioned in that use case.
This got me to thinking: when doing design for XP (or any agile
methodology), how much of the "big picture" does one take into account
when working on the tests and implementation for specific use cases? On
one hand, changing requirements means that the big picture may turn out
to be inaccurate, so it may make sense to ignore it, thinking only about
what needs to be implemented in the current iteration. On the other
hand, some requirements are unlikely to change (e.g., the vet clinic's
basic approach to billing and patient histories), so designing and
implementing a solution to a particular use case in a way that you know
will have to be changed later seems short-sighted.
As a specific example from DesignFest, the financial responsibility for
most pets is borne by a single owner, but race horses can have financial
responsibility shared by many owners, each of whom should be billed in
proportion to their ownership interest. The first (highest priority)
use case we were given considered a dog owner who came in for a routine
exam. This use case had no need for dealing with shared ownership or
proportional billing. My approach to design would have provided for
shared ownership and billing, since this works fine when the number of
owners is one. I got the impression that the person favoring the TDD
approach would have ignored shared ownership and written tests and code
only for a single owner. (I never found out, unfortunately, because
this person failed to return after we broke for lunch.)
I'm interested in others' perspectives on how much of the "big picture"
should be taken into account when doing design and implementaton for
individual iterations. Does it make sense to write code for common
special cases that you know will have to be generalized -- changed --
later to handle less common cases? Is my approach to design an example
of the bad old days of trying to do too much up front?
To clarify, my approach isn't to try to design everything at the outset,
it's just to try to make sure that the design at any given time isn't
obviously incapable of handling requirements we know we're eventually
going to have to satisfy.
All insights appreciated,
Scott
| |
|
| Scott Meyers wrote:
> I got the impression that during work
> on this first use case, he'd essentially ignore aspects of the problem
> not mentioned in that use case.
Y'all probably had a short timeframe too...
> This got me to thinking: when doing design for XP (or any agile
> methodology), how much of the "big picture" does one take into account
> when working on the tests and implementation for specific use cases?
I did a "planning game" by remote this morning. I talked on the phone about
general goals and line-items details for this iteration. Then I wrote an
e-mail with these three sections:
- goals
- things /not/ to do this iteration
- user stories
Focus on the middle item. They are not "nice to have" or "must have next";
they are just things we know _not_ to do right now. They even include ways
to derail the program. Writing them down under a big banner saying "we
promise not to do these things" helps to analyze them, and manage
expectations, and prepare for next w 's goals. We won't forget them, and
we might successfully avoid doing them!
Don't tell my clients, but I feel like a little angel on my left shoulder is
saying, "Awe, you can get by with just a few more lines there! Pass the
tests, sync, ship, and pull the next userstory!"
While the devil on the other shoulder is saying, "Featurize that fringe
case! Productize those acceptance tests! Show you're a Real Programmer!
Write an extra feature and get retroactive permission! Don't you have any
stones?"
Uh, yeah, but I try to save them up for USENET...
> On
> one hand, changing requirements means that the big picture may turn out
> to be inaccurate, so it may make sense to ignore it, thinking only about
> what needs to be implemented in the current iteration. On the other
> hand, some requirements are unlikely to change (e.g., the vet clinic's
> basic approach to billing and patient histories), so designing and
> implementing a solution to a particular use case in a way that you know
> will have to be changed later seems short-sighted.
Sorting features in order of business priority is a requirements technique,
and also a design technique. Deferring features helps you prepare for
accomplishing them, when you get there.
> My approach to design would have provided for
> shared ownership and billing
I think the presence of the database, smack in the middle of that
engineering task, tainted your judgement. I don't think you would have got
hung up on a use case restriction that only affected the GUI.
Now imagine if we had magic fairyland RDMSes that maintained all our
customer's live data _while_ we refactored the living snot out of them.
Rename tables, normalize, resize, etc. If we grew up with such databases,
maybe we wouldn't hesitate before aggressively simplifying a schema.
For my current project, a logged-in user has a one-to-one relationship with
their little data record - their MacGuffin. The reason they log in. I
originally wrote two tables there, and then I forced myself to merge them
into one. That means I have one stinkin' record going all the way from name
and password to private details of the MacGuffin.
Oh, and I'm using Ruby on Rails. I will check to see if its task "rake
db:migrate" will actually refactor a live database with live user data
before we go online, and I will check against these normalization and
refactoring issues before we do. I might also (with a little help from the
little guy over my right shoulder) nudge my client towards userstories that
might force a better database schema. "What if one user has two MacGuffins?"
But if that nudge fails - guess what? - going online and getting 1 customer
is /much/ more important than infinitely supporting ten thousand of them!
> (I never found out, unfortunately, because
> this person failed to return after we broke for lunch.)
OOPSLA attendees who do that should be lined-up and fish-slapped.
> I'm interested in others' perspectives on how much of the "big picture"
> should be taken into account when doing design and implementaton for
> individual iterations. Does it make sense to write code for common
> special cases that you know will have to be generalized -- changed --
> later to handle less common cases? Is my approach to design an example
> of the bad old days of trying to do too much up front?
There is just one more aspect to consider. Much of traditional analysis goes
into acceptance tests following these guidelines:
1. short
2. estimatable
3. high priority
4. testable
If they don't have 1, they get a high estimate, and you split them by
priority and try again.
If they don't have 2, then they don't have something else on the list.
"Research for 2 months" is a bad userstory.
If they don't have 3, then they don't belong in this iteration, so tear them
up and throw them away. They should not taint subsequent iterations. (Keep
the goals and committment schedules around!)
If they don't have 4, then they need more analysis, and maybe more
supporting features and stories.
So if you balance the analytic force of storytests against the ideal of
deferring expensive decisions until the last cheap responsible moment, then
your tests will help to specify those future iterations. And help your
project live long enough to _have_ future iterations!
--
Phlip
[url]http://www.greencheese.us/Z Land[/url] <-- NOT a blog!!!
| |
| Scott Meyers 2006-11-08, 3:59 am |
| Phlip wrote:
> I think the presence of the database, smack in the middle of that
> engineering task, tainted your judgement. I don't think you would have got
> hung up on a use case restriction that only affected the GUI.
I don't understand this comment. The word "database" never arose in our
discussions. (GUI did, but I kept beating people about the head and
shoulders about it until they stopped doing it. Some people are unable
to conceive of any way of interacting with software that's not a GUI.
Sigh.) We were just trying to figure out basic things like which
classes we'd need and more or less what they'd do. But "create invoice"
is a pretty basic piece of functionality (needed by all use cases), and
with shared ownership, multiple invoices need to be created. (Sort of.
The spec actually called for the person bringing the horse in to get
an invoice and all other owners to have the charge appear on their next
monthly statement.)
As I said, User Story 1 had only a single owner, but something like User
Story 3 had multiple owners, so the need to support multiple owners was
not that far in the future. Should we have ignored it and focused only
on the needs of User Story 1?
> So if you balance the analytic force of storytests against the ideal of
> deferring expensive decisions until the last cheap responsible moment
And how do you identify the last cheap responsible moment? Surely part
of that calculation is keeping in mind what you'll have to accomplish in
future iterations. No?
Scott
| |
|
| Scott Meyers wrote:
> As I said, User Story 1 had only a single owner, but something like User
> Story 3 had multiple owners, so the need to support multiple owners was
> not that far in the future. Should we have ignored it and focused only on
> the needs of User Story 1?
Then the question is whether the small margin of simplicity in implementing
US1 provided enough momentum to exceed the cost of then refactoring US3 into
US1.
I may have assumed you subconsciously feared that refactoring database
tables would cost more. Sometimes a refactor is indeed hard. When we play
the averages, and keep the design simple, and write lots of tests, the
average cost of all refactors is very low. A strategy of growing the code is
almost always more cost-effective than planning the code's design.
Sometimes we indeed p ahead at future requirements. When we write only
enough code to pass the current test, we might adjust its design so those
requirements will be easy. But we never increase the line count to do that.
We never make the code harder to understand, just to get ready for future
requirements.
>
> And how do you identify the last cheap responsible moment?
The trivial answer is: When the onsite customer, oblivious to technical
concerns, requests a feature that cannot be cheaply implemented any other
way.
Of course there's more to it than that...
> Surely part of that calculation is keeping in mind what you'll have to
> accomplish in future iterations. No?
The canonical example here is the data store. Teams often start a project by
taking out a credit card and buying some big tools, such as a database.
The heavier your plane, the longer the runway you need. A database does
_not_ make life simpler, especially when you first start out.
So suppose a team were brave enough to commit their data to flat files (or
to >cough< YAML). That will work in the short term, before they need ACID.
If they never need it, they can live with flat files. Robert C Martin likes
to tell a story about the Fitnesse project, a wiki. It worked for a long
time with only flat files. One day a customer asked to use a different data
store. Because its design was very clean, the Martins were allegedly able to
plug out their flat file modules and plug in that data store in some small
time, such as one day.
You don't get that by planning to someday swap in a database. You get it by
removing duplication from your current code. Then any code that accesses
flat files should fold together into common modules, under a simple
interface.
Now suppose your Onsite Customer says, "more than one customer can change
the same data at the same time". You now need a real database.
Because you refactored and simplified your code early and often, the stuff
that accesses those flat files is naturally compartmentalized. Most of the
code is unaware of the data's source, and just treats live records as
objects.
When you examine how to install a database, you have more than a bunch of
text requirements, more than crafty UML. You also have live code, expressing
real specifications. You can, for example, look at the volume of data, and
select a database of the correct size, with a known scaling profile.
Ignorance does not force you to buy the biggest database possible. You now
have the best information to help the decision.
Then, all your code has tests. The code will show what kind of SQL
statements to write, and the tests must still all pass after you install the
database.
So let's state this problem another way. Suppose our team lead says, "We
need to identify the best database, and the best way to use it. So we are
going to do a research project first, before we buy the database. We will
specify our database by writing production code as if it used our database.
Then we will see how far we get, and we will use the code to then proved the
database matches our requirements exactly.
"Oh, by the way, we will also ship software and get paid, _before_
committing to the Total Cost of Ownership of a database. And if our
experiment proves we don't need one, we take the money we would have spent
on it, and divvy it up!"
When the database arrives, you don't just slam it in. You can install it in
phases, one table at a time.
Halfway thru this giant refactor, if someone busts into the room and yells,
"The boss's boss just got back from Thailand early and she's bringing a
surprise visitor who might invest and they need to see the absolute latest
version of the program right now!" you don't panic. You just hit the test
button.
The code could be running half one pattern and half another. Or half one
datastore and half another. The tests don't care, and because they defend
behavior that matters to the customers, then the customers don't care. You
can integrate, sync, and ship, all a partial refactor underway.
Emergent design typically will not lead to a major refactor. If it does - if
business requirements change - the investment in tests is slightly more
important than any investment in a proactive design. And when you clean up
your designs, you should "architect the negative space" to make future
changes even easier.
--
Phlip
[url]http://www.greencheese.us/Z Land[/url] <-- NOT a blog!!!
| |
| Scott Meyers 2006-11-08, 3:59 am |
| Phlip wrote:
> I may have assumed you subconsciously feared that refactoring database
> tables would cost more. Sometimes a refactor is indeed hard. When we play
> the averages, and keep the design simple, and write lots of tests, the
> average cost of all refactors is very low. A strategy of growing the code is
> almost always more cost-effective than planning the code's design.
But if the new functionality changes the interface, we have to update
the tests, too (and convince ourselves that the tests still work, but
I'm going to ignore that aspect of things except to remark that from
what I've read, more than one agile team (XP or not) has overlooked that
tests are software, too, and thus must be maintained). I'd expect that
an interface for a pet with exactly one owner would be different from an
interface for a pet with potentially multiple owners, so if we know that
we'll eventually have to support multiple owners (which, in the case of
Design Fest, we did know, even though that feature was not used in US1),
creating an unnecessarily complicated interface for US1 costs us (in
both implementation and testing) when implementing US1, but it saves us
later code and test modification when we get to US3. But we can't take
that into account unless we think about US3 when designing/coding for
US1. Which leads me to believe that thinking about US3 during work on
US1 is not unreasonable.
> Sometimes we indeed p ahead at future requirements. When we write only
> enough code to pass the current test, we might adjust its design so those
> requirements will be easy. But we never increase the line count to do that.
> We never make the code harder to understand, just to get ready for future
> requirements.
I'm thinking that US1 suggests an interface like this:
Owner Pet::getOwner() const;
But US3 suggests something more like this:
list<Owner> Pet::getOwners() const;
Working with the latter is likely to take more lines than the former,
because dealing with a collection is more complex than dealing with a
single object. I don't see how your "never increase the line count"
criterion is practical in this kind of a situation.
Scott
| |
| Laurent Bossavit 2006-11-08, 3:59 am |
| Scott,
> creating an unnecessarily complicated interface for US1 costs us (in
> both implementation and testing) when implementing US1, but it saves us
> later code and test modification when we get to US3.
How do we know whether the savings are enough to offset the costs ?
Suppose that we figured out the effort saved at US3 were *exactly* equal
to the extra cost incurred in US1. Then everyday economics tells us that
we should defer the cost (implement the one-user version of US1). There
is a discount rate which makes "money later" worth a little less than
"money now".
Laurent
| |
| Laurent Bossavit 2006-11-08, 3:59 am |
| Scott,
> And how do you identify the last cheap responsible moment? Surely part
> of that calculation is keeping in mind what you'll have to accomplish in
> future iterations. No?
The more we know about future requirements, the better. While working on
the use case that calls for a single owner, I'd much prefer to know
about the later use cases as well, especially if they invalidate the
single owner assumption.
That leaves open the question of *when* we should reflect that knowledge
in our design, and *how much* of it. (As an example of the latter
choice, you said you would anticipate the change from single-owner to
multiple-owner, but you seemed to say that the difference between "one
invoice to each owner" and "invoice only the person bringing in the
horse" was less important, more a matter of detail.)
If we know about the future need for multiple owners, we can ensure,
while working on the first use case and assuming a single owner, that we
retain the flexibility needed to move to multiple owners easily. For
instance, we will avoid having many, many calls to Pet::getOwner all
over the code. (Perhaps that would be the case even if we *didn't* know
about the future requirements, since we are keeping a sharp lookout for
any duplication and diligently refactoring when we spot it. But if we do
know about future requirements, then our sense of duplication can be
backed by concrete considerations.)
Laurent
| |
|
| Scott Meyers wrote:
> I'm thinking that US1 suggests an interface like this:
>
> Owner Pet::getOwner() const;
>
> But US3 suggests something more like this:
>
> list<Owner> Pet::getOwners() const;
>
> Working with the latter is likely to take more lines than the former,
> because dealing with a collection is more complex than dealing with a
> single object. I don't see how your "never increase the line count"
> criterion is practical in this kind of a situation.
That's not the kind of planning I meant. I meant that if you predict US3,
you can write US1 in ways that simplify and predict US3. Try this:
list<Attribute> Pet::getAttributes(string key) const;
Owner Pet::getOwner() const;
Now you have duplication; the word 'get'. You can fix it by making Owner
specialize Attribute & ride on the Attributes system, and whacking getOwner.
And you don't fix it in other ways because you plan for US3.
Your code must always work within a chain of accountability like this:
committed user stories ->
passing storytests ->
developer tests ->
interfaces ->
production code
If you don't have committed user stories specifying more than one pet owner,
then you can't have more than one pet owner in the interfaces, unless if
that were already simple and tested.
Tests drive the interfaces, via Intentional Programming. The cognitive
burden of one extra list in one interface is indeed very low. We don't want
to compound that over all the committed user stories, and over all the
potential user stories. If we add extra lines here and there that predict
everything, they won't have storytests, so they will add incredible risk.
They might have bugs. We won't know they don't have tests when we go to
refactor them. We might then use them, and add sneaky bugs.
Our designs should follow a curve of simplicity, under the pressure of
testing and refactoring to remove lines. So at each point in its growth the
code is as simple as possible. The only thing better than a simple design is
one that grew into its current state thru many simpler configurations.
Put another way, it's not good enough to envision a simple design, then
progress to the more complex one. Your code should experience that simple
design, first, to get it ready for the more complex one.
We should leave business decisions to the business side. They might say, "We
are going to target this cult of Lapp-Sikhs we just discovered, and they
restrict all their members to only have one pet with one owner. And each pet
has a special red cap with multiple owners that we must track." You
shouldn't tell the business side that feature will cost extra because it has
to work within the speculative code you wrote.
Your goal as a developer is to provide the highest velocity for any kind of
new requirement. Speculative code gambles with that formula. An agile
project:
- can accept any requirement at any time
- can deliver any integration
- minimizes the time between specifying a feature and
letting end-users profit from it
You ought to read the Lean Development books for more about "just in time
requirements" here.
--
Phlip
[url]http://www.greencheese.us/Z Land[/url] <-- NOT a blog!!!
| |
| Ron Jeffries 2006-11-10, 6:57 pm |
| Hi Scott ... thanks for one of the best questions we've seen here for a while.
On Tue, 07 Nov 2006 09:59:31 -0800, Scott Meyers <usenet@aristeia.com> wrote:
>As a specific example from DesignFest, the financial responsibility for
>most pets is borne by a single owner, but race horses can have financial
>responsibility shared by many owners, each of whom should be billed in
>proportion to their ownership interest. The first (highest priority)
>use case we were given considered a dog owner who came in for a routine
>exam. This use case had no need for dealing with shared ownership or
>proportional billing. My approach to design would have provided for
>shared ownership and billing, since this works fine when the number of
>owners is one. I got the impression that the person favoring the TDD
>approach would have ignored shared ownership and written tests and code
>only for a single owner. (I never found out, unfortunately, because
>this person failed to return after we broke for lunch.)
>
>I'm interested in others' perspectives on how much of the "big picture"
>should be taken into account when doing design and implementaton for
>individual iterations. Does it make sense to write code for common
>special cases that you know will have to be generalized -- changed --
>later to handle less common cases? Is my approach to design an example
>of the bad old days of trying to do too much up front?
>
>To clarify, my approach isn't to try to design everything at the outset,
>it's just to try to make sure that the design at any given time isn't
>obviously incapable of handling requirements we know we're eventually
>going to have to satisfy.
I've been thinking about this on the drive over to the Brighton Borders, and
chatted with Chet about it when he got here. Here are some thoughts:
It would surely be my standard practice to implement the "bill the owner" story
in the simplest posible way. I would like to /know/ that multiple billing was
coming up, but I wouldn't do much about it.
I say wouldn't do /much/. What I would do includes thinking about multi-owner
billing, and if there were two equally simple ways of doing single-owner
billing, picking the one most likely to last. I call this "leaning in the
direction" of the ultimate design. But I think in general, it would come down to
getting the owner and sending him a message to create an invoice.
I do the simplest thing with the initial stories because that's sufficient to
get all the necessary objects in place, and elaborations then go in easily when
the objects are right for their initial purpose.
In our discussion, Chet said that he'd do the simple thing, but that he would
try extra hard to get the design clean. He said that if one's normal mode is 85%
of absolute perfect concentration, he'd try for 92. We agree that if the objects
are clean, everything is likely to turn out OK.
I was thinking about what the group billing solution is likely to be. I can
imagine someone (none of us, I'm sure) writing some code that gets all the
owners, loops over them finding their proportions of ownership, then loops over
them creating an invoice for each one. I've seen a lot of implementations of
such things that would turn into weird procedural code somewhere in the system.
But then I realized that a far better solution would be one using a kind of
Composite pattern. The Owner is an object that can be a person ... or can be a
collection of persons, with various billing methods depending on whether it's a
person, a collection billed in proportion, or a collection with a single billing
entity, or whatever.
It came to me that with this design, the simplest approach, getOwner, is also
arguably the best approach, because it's likely to give us the best chance to
have a clear head and see the more elegant solution. My thought is that having
committed to the notion of a single owner, we might actually be more likely to
think of the Composite solution to retain the single owner notion at the top,
while putting the multiplicity inside.
Now all this is very speculative ... but it explains why I like to think about
the future, but implement the present.
Thanks again for a good question!
--
Ron Jeffries
www.XProgramming.com
I'm giving the best advice I have. You get to decide if it's true for you.
| |
| Ron Jeffries 2006-11-10, 6:57 pm |
| On Tue, 07 Nov 2006 21:54:52 -0800, Scott Meyers <usenet@aristeia.com> wrote:
>I'm thinking that US1 suggests an interface like this:
>
> Owner Pet::getOwner() const;
>
>But US3 suggests something more like this:
>
> list<Owner> Pet::getOwners() const;
>
>Working with the latter is likely to take more lines than the former,
>because dealing with a collection is more complex than dealing with a
>single object. I don't see how your "never increase the line count"
>criterion is practical in this kind of a situation.
Perhaps neither interface is right. both getOwner or getOwners methods seem to
me to be likely to violate the law of Demeter.
I believe that what Phlip may have been saying -- or, more accurately, something
that I would say that sounds to me like what he said -- is that while I might
make design choices during US1 that favored moving toward multiple owners, I
would not choose an implementation for US1 that required more than the minimum
amount of code to support that story.
I'd pick the one most likely to be easy to convert, in other words, but I'd try
not to invest now in something I'd need later.
All of this is a matter of judgment. My long experience in doing the simplest
thing to the point of stupidity suggests that the risks of change are not as
high as we might fear -- given that we are going to keep the code we do write as
clean as possible.
I would therefore suggest to people with questions like yours that they try
pushing much further toward simplicity than may seem safe, paying close
attention to what happens. Sooner or later, if one pushes far enough, one may
come to a point of waste. I think that those who try the simplicity approach
will be surprised at how much further away that point is than they feared.
Regards,
--
Ron Jeffries
www.XProgramming.com
I'm giving the best advice I have. You get to decide if it's true for you.
| |
|
| Ron Jeffries wrote:
> I believe that what Phlip may have been saying -- or, more accurately,
> something
> that I would say that sounds to me like what he said -- is that while I
> might
> make design choices during US1 that favored moving toward multiple owners,
> I
> would not choose an implementation for US1 that required more than the
> minimum
> amount of code to support that story.
To call it "leaning in the direction of" US2, the important goal for US1 is
to accomplish it with the "least cognitive burden". In programming that
usually comes down to the minimum amount of code.
--
Phlip
[url]http://www.greencheese.us/Z Land[/url] <-- NOT a blog!!!
|
|
|
|
|