Fabulous Adventures In Coding
Eric Lippert is a principal developer on the C# compiler team. Learn more about Eric.
Last time on FAIC a user asked for guidance on the potential pitfalls of refactoring an automatic property into a regular explicit property. This is just an example of a far more general problem: how can we design programs so that they are easy to get right when things inevitably change in the future?
This is an incredibly difficult question to answer, one which whole books could be written on. Today, I'll just give three general points to think about.
First: Premature generality is expensive.
Designing code to be future-proof without a clear understanding of what the future holds often leads to excess generality; generality has very real costs, and they might be higher than the benefit accrued.
If you do a good job of solving the problem at hand with the tools at hand, if the code is clean and organized and has few moving parts, then it will be easier to generalize it in the future if you need to in order to solve a problem then.
Design your code to solve the well-understood problem. If you design it to solve an unknown future problem, odds are good that you're going to solve it poorly, making even more work for the future.
Second: Represent in your model only those things which are always in the problem domain and whose class relationships are unchanging.
The relationships between the classes TrafficLight, TrafficSign, Vehicle, Car, Truck, Pedestrian, Roadway, Intersection, SignalTiming and RightOfWayLaw are likely to be eternal and unchanging.
Things change. The semantics of a particular RightOfWayLaw might change subtly or grossly as the city council does its job. There might be new subclasses of Vehicle created in the future. A specific traffic light might have a faulty implementation that the rest of the system needs to handle gracefully. But each of these situations is about changing the implementation details of a class, never changing its relationships with other classes.
If the class relationships are future-proofed then it is a lot easier to edit the implementations of those classes without having to worry that the whole system will thereby get messed up.
The easiest way to keep the relationships straight is to base them as much as possible on concepts directly from the problem domain.
Third: Keep your policies away from your mechanisms.
The mechanism of a traffic light works the same way no matter what policies like light timings at rush hour are. If mechanisms (TrafficLight) are separated from policies (SignalTiming) then you can change the mechanism to a more efficient one without worrying that you’re going to inadvertently change policy, and can change policy without worrying that you’re going to break mechanism.
My earlier example of a bank balance was deliberately an example of what goes wrong when mechanism and policy become conflated. The “Balance” getter was originally a mechanism, but after the edit, it became a security policy enforcement tool.
You must then ask yourself, “where in this code do I care about enforcing the policy, and where do I care about executing the mechanism?” and make the appropriate edits. After the edit, everywhere that needs policy enforcement needs to use “Balance”, everywhere that needs to effect the action of the mechanism needs to use “balance”.
What are the odds that there is going to be a bug introduced, given that we now have a difference which is important and almost invisible? Pretty high! This difference should probably be made more visible by renaming the backing store to something that calls it out as semantically different from the property accessor.
Let's bring this back to the question at hand. What guidance do we propose for future-proofing turning automatic properties into regular properties? The guidance that I am proposing here is:
This reminds me of issues I've seen with developers and databases. The tendency is to thing of the user interface or process, and model the DB to act as a backing store for that process.
What I always advise is "model the world, not the activity". In other words, as closely as possible create the datamodel based on the entities and relationships of the problem domain, NOT on the actual problem being solved.
A lot of times you will end up with a technically more complex data model, but over a very few iterations the complexity pays off in being able to change(fix) your workflow without fighting with or remodeling your data.
The absolute worst outcome is to take a workflow based datamodel, alter the workflow and then try to avoid changing the datamodel. After a couple iterations that system becomes nearly incomprehensible as half of things on the UI have a direct relationship with data, and half some some completely confusing derivation.
In short, databases model things and code models activities, therefore the design of the datamodel should be derived from things and the design of the code should be modeled from activities. The job of the programmer is to bridge the gap.
With regards to premature generality, the non-coding example that popped immediately into my head was the bus tunnel system in Seattle. They tried early on to plan for a future problem, solved it badly, and had to rip the whole thing out and start over.
That was the best OO back-to-basic post I've read in 2009 ;)
Thank you for submitting this cool story - Trackback from DotNetShoutout
#1 future proof rule:
Do not use the latest model, coding style, methodology, third party control, api call, etc. Using the latest new thing will increase the chance your system is married to a buggy, poorly supported, poorly thought out, etc concept, library, tool, api, etc.
This is one of the traps I've seen people with less than 5 years experience fall into where they avoid solving the business problem and think that applying the latest methodology, object model, library, etc will ensure them delivering a functioning system that meets business needs.
I ask whether or not their system/design will a) be maintainable by someone with less than 2 years experience and b) be maintainable by someone on a monday morning where they did not sleep the previous two days. This helps cut down on the premature optimization increasing code complexity needlessly as well as the increased code complexity for the sake of developer ego boost by writing code as if it is a creative writing project instead of a solution to a business problem.
Brilliant posting Eric! I can't help hoping that this will be the first of such in one of your intermittent themed series of posts.
#1 rule: Separate your object graph construction (aka wiring, aka 'new') from your logic.
If you do this, you end up with manual dependency injection and a very flexible system which allows your objects to be wired together to suit unforeseen circumstances. It's a shame no language designer has yet created a language which enforces this.
This and many more tips on Misko Hevery's blog: http://misko.hevery.com/ syndicated on the Google Testing blog.
All these is good advise but could seem a little too much diluted.
It all comes down to using and knowing when/how to use design patterns.
Future-Proofing A Design means writing the code in such a way that is easy to modify/change in the future, this is covered very well in the "Open Close Principle" (Open to extension but close to modification) also having a good understnading of Encapsulation, ecapsulation of data is good but encapsulating behavior is even better(Ex: encapsulate responsibility using the strategy pattern, or encapsulate creation using the factory pattern), because that way the specific behavior can be isloated to one area, less redundant and easier to change in the future.
If you don't know how to use the "commonality and variability metrix" pattern from netobjectives, please do yourself a favor the check it out, it will help you write software that is easier to change and therefore ready for the future, using concepts like strategy and factory pattern.
Great post Eric, and an excellent comment from Darren Clark