Divide and conquer (abstract, interfaces, layers, declarative, recursive, replicate, concurrent)
Increment (compose, indirect, iterate)
Approximate (good enough)
Phases (When and Where to apply them)
requirements (ensuring completeness)
architecture/process (choosing interfaces)
process/techniques (devising implementation).
If you only remember three things
Keep it simple
Write a spec
At least write down the state
Then write down the interface signature
Abstract with interfaces
Next write the abstraction function F from code to spec
Finally show that each executation step preserves F.
Goals are in conflict
Engineering is the art of making tradeoffs!
For instance among features, speed, cost, dependability, and time to market.
Some simpler oppositions are:
For Adaptable, between evolving and fixed, long-lived and one-shot, monolithic and extensible, scalable and bounded.
For Dependable, between deterministic and
non-deterministic, volatile and persistent, precise and approximate,
reliable and flaky, consistent and eventual.
For Incremental,
between indirect and inline, dynamic and static,
experiment and plan, discover and prove.
It also helps to choose the right coordinate system:
Notation (vocabulary, translate; power)
State: name-value map vs log (list of updates)
Function: code vs table vs overlays
Functionality: Getting it Right!
The most important hints are about getting it to do the
things you want it do do.
An interface that separates an implemenation of some abstraction
from the clients who use the abstraction.
It consists of the set of
assumptions that each programmer needs to make about the other program
in order to demonstate the correctness of his program
Defining interfaces is the most important part of system design.
The interface design must satisfy three conflicting requirements:
It should be simple
It should be complete
It should admit a sufficiently small and fast implementation
Each interface is like a small programming language:
it defines a set of objects and the operations that can be used to
manipulate the objects.
Recall Hoare's discussion on good language design:
simplicity
security
fast translation
efficient object code
readability
Keep it simple
Do one thing at a time, and do it well.
An interface should capture the minimum essentials of an abstraction.
Don't generalize; generalizations are generally wrong.
The interface should not promise more than the implementer knows
how to deliver.
Neither abstraction nor simpicity is a substitute for getting it right.
Other corollaries:
Make it fast, rather than general or powerful
Don't hide power. The purpose of abstractions is to conceal undesirable propreties; desirable properties should not be hidden.
From Interfaces to Specifications
Hard questions when writing a spec:
What does the system really do?
What should you abstract away?
What are the modules?
Can you do any useful proof?
Spec makes modularity precise (design, correctness, documentation)
Do it recursively
Refinement: one main's implementation is another man's spec.
Composition: use actions from one spec in another.
How to Write a Spec
Find out what the state is; choose it to make the spec clear, not to match the code.
Describe the actions (1) what they do to the state
(2) what they return
What "implements" means (you can't tell Y and X by looking at only the external actions)
Divide actions into external and internal.
Y implements X if every external behavior of Y is an external behavior of X, and Y's liveness property implies X's liveness property.
Continuity for Debugging & Maintenance
Keep basic interfaces stable.
Keep a place to stand if you have to change interfaces
Making implementations work
Plan to throw one away; you will anyhow.
Keep secrets of the implementation. Secrets are assumptions about an
implementation that client programs are not allowed to make.
Divide and conquer. Reduce a hard problem into several easier ones.
Use a good idea again instead of generalizing it.
Handling all the cases
Handle normal and worst cases separately.
The normal case must be fast
The worst case must make some progress
Speed: Making it fast
Split resources in a fixed way if in doubt, rather than sharing them.
Use static analysis if you can.
Dynamic translation.
Cache answers to expensive computations.
Use hints to speed up normal execution.
When in doubt, use brute force.
Compute in background when possible.
Fault-tolerance
Making a system reliable is not really hard, if you know how to
go about it. But retrofitting reliability to an existing design
is very difficult.
End-to-end.
(two issues: requires a cheap test for success; possible performance
defects)
Log updates to record the truth about the state of an object.
Make actions atomic or restartable.
An atomic action (a.k.a. transaction) is one that either completes
or has no effect.