Lezioni di design da Zio Bob
Summary: cool simple program design lesson on Uncle Bob’s blog
Uncle Bob scrive:
In my previous blog (RubarianNotation) I had posted this little code snippet from a Mad Libs program:
def translate(text) questions_and_text = split_questions_from_text(text) answers_and_text = replace_questions_with_answers(questions_and_text) answers_and_text.join endThe variable questions_and_text held on to a list of strings which alternated between questions and text. The question strings looked like this ((an adjective)), and the text was just raw text. So, for example, if my original document looked liks this
((name of someone in room)) took a ((a noun)) today and ((a past-tense verb)) his back.
We would expect the questions_and_text variable to look like this:
["((name of someone in room))", " took a ", "((a noun))", " today and ", "((a past-tense verb))", " his back."]So this variable holds a list of strings of alternating questions and answers.
I did not find the name questions_and_answers satisfying, and started to think that it would be better to use a name more like list_of_strings_of_alternating_questions_and_answers. This variable name is long, so I thought it might be better to create a set of standard abbreviations like ls_alternating_questions_and_answers. And then I stopped myself and realized that I was just reinventing the horror of Hungarian notation. I took this to mean that my program had a design flaw.
So I refactored the program to improve the design. Here’s the impact of that refactoring on the previous snippet of code:
def translate(document) game = make_game(document) game.play(GameContext.new(@asker)) endWhat a difference! Remarkably, the first line of both snippets parses the incoming text into a convenient form. The second line of both processes that convenient form, asking the approriate questions and replacing the questions with the answers. And yet the implication of the names between the two snippets is remarkably different. In the first case they are rife with implementation detail. In the second, they tell you whats going on at an abstract level.
There is a more important difference between the two snippets. In the first we are operating on data structures, which is why we want to identify the structure of those data structures. In the second we are telling objects what to do, which is why we don’t care about the structure of the objects and can focus on their intent instead.
Of course this is just good old OO. But for an old C++/Java/C#-head, like me, this is something of an eye-opener. Ruby has very rich primitive types like lists and maps. You can use them to create lists of lists and maps of lists and lots of other deep and convoluted structures. Indeed, that’s what I had done in the first snippet. I had created a semantically rich data structure composed of a list of strings that alternated between raw text and questions. What I have come to realize is that although slinging these rich data structures around is fun, it’s also bad.
As I said in the previous post, we want our variables to hold objects that we can tell what to do, we don’t want our variables holding data structures that we operate on. If we use the former strategy, then the variable names don’t have to encode the structure of the data. Rather they, coupled with the methods that are invoked against them, let us know what we expect the object to do.
Questo articoletto insegna una cosa preziosa: evitare di passare in giro array e liste. In Java, per esempio, spesso uno si chiede se un dato metodo debba restituire un array, oppure una lista:
List employees(); // meglio questo Employee[] employees(); // oppure questo? List<Employee> employees(); // o magari questo?
In molti casi la risposta potrebbe essere nessuno dei tre. Meglio restituire una classe ad hoc:
Employees employees();
Nel primo caso, il codice per iterare sulla collezione dipende da che tipo di collezione ho scelto.
// caso array Employee[] e = employees(); for (int i=0; i<e.length; i++) { e[i].printSlip(); } // caso lista List e = employees(); for (Iterator i=e.iterator(); i.hasNext(); ) { ((Employee) i.next()).printSlip(); }
Potrei addirittura essere tentato di codificare il tipo della variabile “e” nel nome: empList, empArray. Brutto no? Usando la classe ad hoc invece abbiamo:
Employees e = employees(); e.each(printSlip);
o addirittura
employees().each(printSlip);
dove “printSlip” è un oggetto di un’altra classe ad hoc. (Scuse a Francesco Cirillo per avere copiato il suo esempio!)
Questo principio di design è un caso particolare di un principio più generale che consiglia di evitare setters and getters. Ad esempio è brutto
return rectangle.width() * rectangle.height();
Molto meglio mettere dentro la classe Rectangle un metodo per calcolare l’area:
return rectangle.area();
In questo modo il nostro codice non si interessa più alle viscere dell’oggetto che riceviamo, ma delega il comportamento all’oggetto stesso. Questo codice continuerà a funzionare anche se gli passiamo un oggetto che rappresenta un cerchio, purché quest’oggetto risponda al messaggio “area”.
Il duo pragmatico ha coniato questo slogan: Tell, Don’t Ask. Non chiedere a un oggetto cosa contiene. Digli di eseguire la sua operazione.
October 23rd, 2006 at 17:35
Concordo pienamente, è molto bello(forse troppo) poter iterare tra gli elementi di una classe con una sola riga di codice. Es.
employees().each(printSlip);
però penso che la maggior parte delle volte risulta inutile creare un oggetto ad hoc (es. ‘printSlip’) e un metodo (.each) unicamente per gestire, come in questo caso, una sola riga di codice(rallenta sia la programmazione che l’esecuzione). E comunque il ciclo for non scompare ma viene solo spostato nel metodo .each.
Possiamo però sfruttare meglio le funzionalità del linguaggio di programmazione per rendere il codice più elegante Es.
// caso array
Employee[] e = employees();
for (int i=0; i
October 23rd, 2006 at 20:37
Marco,
ci siamo persi il resto del tuo esempio. Puoi ripostarlo?
October 23rd, 2006 at 21:27
Scusa…come al solito sono riuscito a combinare pasticci…
Concordo pienamente, è molto bello(forse troppo) poter iterare tra gli elementi di una classe con una sola riga di codice. Es.
employees().each(printSlip);
però penso che la maggior parte delle volte risulta inutile creare un oggetto ad hoc (es. ‘printSlip’) e un metodo (.each) unicamente per gestire, come in questo caso, una sola riga di codice(rallenta sia la programmazione che l’esecuzione). E comunque il ciclo for non scompare ma viene solo spostato nel metodo .each.
Possiamo però sfruttare meglio le funzionalità del linguaggio di programmazione per rendere il codice più elegante Es.
// caso array
Employee[] e = employees();
for (int i=0; i
October 23rd, 2006 at 21:33
Concordo pienamente, è molto bello(forse troppo) poter iterare tra gli elementi di una classe con una sola riga di codice. Es.
employees().each(printSlip);
però penso che la maggior parte delle volte risulta inutile creare un oggetto ad hoc (es. ‘printSlip’) e un metodo (.each) unicamente per gestire, come in questo caso, una sola riga di codice(rallenta sia la programmazione che l’esecuzione). E comunque il ciclo for non scompare ma viene solo spostato nel metodo .each.
Possiamo però sfruttare meglio le funzionalità del linguaggio di programmazione per rendere il codice più elegante Es.
// caso array
Employee[] e = employees();
for (int i=0; i
Può essere riscritto(se non ricordo male la sintassi di java) così:
for (Employee e: employees()) {
e.printSlip();
}
Molti linguaggi di programmazione forniscono un for ‘speciale’ detto for-each che permette di scorrere tutti gli elementi di un array(o di una collezione). Anche se ha delle limitazioni(si può scorrere solo dall’inizio alle fine e non avendo un indice, generalmente inutile, non possiamo sapere in che posizione siamo) perché non utilizzarlo? Visto che capita molto spesso di dover ciclare tra tutti gli elementi di una classe, perché dovremmo mettere sempre lo stesso codice dentro gli oggetti e non far fare questo ‘sporco’ lavoro direttamente al linguaggio di programmazione?
Per cicli complessi(o cicli che si prevede debbano essere utilizzati più volte) ritengo però indispensabile l’utilizzo di un oggetto ad hoc e di un metodo .each come proposto da Matteo. Si potrà però sempre utilizzare il for-each all’interno del metodo .each :-)
Ringrazio Matteo per gli spunti di riflessione sempre ben argomentati,
Marco
October 23rd, 2006 at 21:47
chiedo venia…ho sbagliato a inserire il simbolo <
il for corretto è
// caso array
Employee[] e = employees();
for (int i=0; i<e.length; i++) {
e[i].printSlip();
}
non è possibile editare i propri messaggi?
October 24th, 2006 at 12:18
L’argomento ha del merito; ma l’idea è di fare scomparire i cicli del tutto… Una volta che hai una classe Employees (collezione di Employee) questo diventa la “casa” naturale di tutti i cicli che hanno a che fare con le collezioni di Employee: printSlips(), averageSalary(), computeBenefits(), …
Se la struttura dati è una semplice collezione, può valere la pena di usare un’array o lista “nuda”, invece di una classe-collezione ad hoc. Siamo pragmatici!
Nel caso invece di una struttura dati più complessa, tipo una hash di liste o una lista di liste, o una lista di hash,… conviene senz’altro incapsularla in una classe ad hoc.