in software design.
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
San Martin de Fowler
Principles bridge values (universal but often difficult to apply directly) and patterns (which are clear to apply but specific).
Cost Understand + Cost Change + Cost Test + Cost Deploy
Code communicates well when a reader can understand it, modify it, or use it.
What is the purpose of this code?
public List<int> getThem(){
var list1 = new List<int>();
forEach(var x : TheList){
if(x.Status == 4) {
list1.add(x.Id);
}
}
return list1;
}
What kinds of things are in the List?
public List<int> getThem(){
var list1 = new List<int>();
forEach(var stockVehicle : StockVehicles){
if(stockVehicle.Status == 4) {
list1.add(stockVehicle.Id);
}
}
return list1;
}
What is the significance of the value 4?
public List<int> getThem(){
var list1 = new List<int>();
forEach(var stockVehicle : StockVehicles){
if(stockVehicle.Status == Blocked) {
list1.add(stockVehicle.Id);
}
}
return list1;
}
How would I use the list being returned?
public List<int> getThem(){
var blockedVehicleIds = new List<int>();
forEach(var stockVehicle : StockVehicles){
if(stockVehicle.Status == Blocked) {
blockedVehicleIds.add(stockVehicle.Id);
}
}
return blockedVehicleIds;
}
What paradigm am I working on?
public List<int> getThem(){
var blockedVehicleIds = new List<int>();
forEach(var stockVehicle : StockVehicles){
if(stockVehicle.isBlocked()) {
blockedVehicleIds.add(stockVehicle.Id);
}
}
return blockedVehicleIds;
}
Could be this code close to that ordinary written prose?
public List<int> getThem(){
StockVehicles
.Where(vehicle => vehicle.isBlocked())
.Select(vehicle => vehicle.Id)
.ToList();
}
Eliminating excess complexity enables those reading, using, and modifiying programs to understand them more quickly.
Essential vs accidental complexity
Sometimes, however, I find a simplication that would make a program harder to understand. I choose communication over simplicity in these cases.
Why the expected result is 49.25?
bank.addRate("USD", "GBP", rate);
bank.commission(commission);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(49.25, "GBP"), result);
Be explicit, although a bit of coupling maybe is the comparative cost
bank.addRate("USD", "GBP", rate);
bank.commission(commission);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(100 / rate
* (1 - commission, "GBP"), result);
Turning it into a property based test, increase the benefits
forAll(doublesGreaterThan(0)) { rate ->
forAll(doublesGreaterThan(0)) { commission ->
bank.addRate(USD, GBP, rate);
bank.commission(commission);
assertThat(bank.convert(Note(100, USD), GBP),
equalTo(Note(100 / rate * (1 - commission), GBP)))
}
}
Simplicity is in the eye of the beholder. What is simple to an expert programmer, familiar with the power tools of the craft, might be overwhelmingly complex to a beginner.
Simplicity, familiarity
public List<int> getThem(){
StockVehicles
.Where(vehicle => vehicle.isBlocked())
.Select(vehicle => vehicle.Id)
.ToList();
}
Simplicity, familiarity
public List<int> getThem(){
var blockedVehicleIds = new List<int>();
forEach(var stockVehicle : StockVehicles){
if(stockVehicle.isBlocked()) {
blockedVehicleIds.add(stockVehicle.Id);
}
}
return blockedVehicleIds;
}
Design for the future. Change is expensive. Make it cheap by anticipating it.
Flexibility is the justification used for the most ineffective coding and design practices.
Flexibility can come at the cost of increased complexity.
“That's why flexibility of simplicity and extensive tests is more effective that the flexibility offered by speculated design.”
“Paradox, by not considering the future of your code you make your code much more likely to be adaptable in the future”
If a change here can cause a problem there, then the cost of change rise dramatically
Code with mostly local consequences communicates effectively. It can be understood gradually without first having assemble an understanding of whole
When you have the same code in several places, if you change one copy of the code you have to decide whether or not to change all the other copies
Put logic and data it operates on near each other
Express the logic describing the What you want to do and not How you want to do.
Imperative expression
List<int> results = new List<int>();
foreach(var num in collection)
{
if (num % 2 != 0)
results.Add(num);
}
Declarative expression
var results = collection.Where( num => num % 2 != 0);
Symmetry in code is where the same idea is expressed the same way everywhere it appears in the code.
Once readers understand one half of the simmetry, they can quickly understand the other half.
array_map("process", array(1,2,3));
array_filter(array(1,2,3), "process");
offer.assignCustomer(customer);
offer.status = Offer.Accepted;
offer.changeLastModifiedDate();
offer.assignCustomer(customer);
offer.accept();
offer.changeLastModifiedDate();
offer.assignCustomer(customer);
offer.accept();
Put logic or data that changes at the same rate together and separate logic or data that changes at different rates
Change together
public void setAmount(int value, string currency){
this.value = value;
this.currency = currency;
}
Becomes
public void setAmount(Money value){
this.value = value;
}
“Programming, then, is a human task done by humans for humans.”
Kent Beck
Simple Made Easy - Rich Hickey
EconomÃa del software en 10 min - Artola & Guillermo
Communication over simpliflication example (Property based testing contribution by Nat Pryce)
Simplicidad para desarrolladores - Eduardo Ferro
Chunking or the myth of declarative - Dan North
An example of introducing symmetry to enable duplication removal - Manuel Rivero