Friday, February 23, 2007

Design Patterns are not Recipes

The controversy

Over the past few months, I've been seeing some Design Patterns backlash on the blogosphere, I guess it is accompanying the agile anti-hype phase that's also strong these days. The most rabid attack came from Mark Dominus, making the case that many innovations in programming languages in the past decades were in response to common practices that could be described as patterns, had the pattern format been known back then. Take, for instance, what we now know as a procedure call: code to stack up register values and return address followed by a jump instruction. Before assemblers became available, programmers used to recode this every time they wanted a subroutine. Dominus sees this observation as a sign that something is wrong with the patterns movement:
"Identification of patterns is an important driver of progress in programming languages. As in all programming, the idea is to notice when the same solution is appearing repeatedly in different contexts and to understand the commonalities. This is admirable and valuable. The problem with the "Design Patterns" movement is the use to which the patterns are put afterward: programmers are trained to identify and apply the patterns when possible. Instead, the patterns should be used as signposts to the failures of the programming language. As in all programming, the identification of commonalities should be followed by an abstraction step in which the common parts are merged into a single solution."
Professor Ralph Johnson, one of the original "Gang of Four" members, responded:
"No matter how complicated your language will be, there will always be things that are not in the language. These things will have to be patterns. So, we can eliminate one set of patterns by moving them into the language, but then we'll just have to focus on other patterns. We don't know what patterns will be important 50 years from now, but it is a safe bet that programmers will still be using patterns of some sort."
Dominus retorts with subtler arguments, and I encourage you to read his article to avoid any misinterpretations of my part. But I'll still quote what I believe is the core of his thinking:

"What I imagine is that when pattern P applies to language L, then, to the extent that some programmer on some project finds themselves needing to use P in their project, the use of P indicates a deficiency in language L for that project.

The absence of a convenient and simple way to do P in language L is not always a problem. You might do a project in language L that does not require the use of pattern P. Then the problem does not manifest, and, whatever L's deficiencies might be for other projects, it is not deficient in that way for your project.

(...)

But to the extent that some deficiency does come up in your project, it is a problem, because you are implementing the same design over and over, the same arrangement of objects and classes, to accomplish the same purpose. If the language provided more support for solving this recurring design problem, you wouldn't need to use a "pattern". Consider again the example of the "subroutine" pattern in assembly language: don't you have anything better to do than redesign and re-implement the process of saving the register values in a stack frame, over and over?"

My take

Mark seems to be worried that programming language innovation will be stifled by the patterns movement, on account of programmers being taught to mimic patterns on their code instead of applying that energy on incorporating them into programming languages. It is important to first acknowledge, as himself put it, that it is valuable to identify commonalities in software design, one could even say this is a precondition for evolving a programming language. So, he sees the problem in not working to embody these commonalities in the language. From my exposure to the patterns literature, I believe the movement is not in any way against incorporating patterns as programming language features. It is implicit in this passage from GoF, page 4:
"Our patterns assume Smalltalk/C++-level language features, and that choice determines what can and cannot be implemented easily. If we assumed procedural languages, we might have included design patterns called "Inheritance," "Encapsulation," and "Polymorphism." similarly, some of our patterns are supported directly by the less common object-oriented languages. CLOS has multi-methods, for example, which lessen the need for a pattern such as Visitor"
And explicit in Ralph Johnson's aforementioned blog post: "Many of the patterns will get subsumed by future languages, but probably not all of them". Now that we know that design patterns are contextual and can be incorporated as language features, we must ask if the emphasis of the patterns movement isn't displaced. Why go to the trouble of publishing large books describing at length things like intent, motivation, participants, consequences, related patterns, etc, when we could just inventory all patterns and shove our favorites in our programming languages? One very simple reason is that not all patterns would yield benefit from being reified. Dominus says that people often mentioned to him MVC as an example of a pattern so complex that it could not be absorbed in a programming languge. But, of course, they were all wrong and he was right, since novel systems like Ruby on Rails or subtext do exactly that, definitively confirming his thesis that the only impediment was an atrocious lack of imagination on the part of his opponents! This is all very strange to me, since "MVC" came to life as pretty concrete software - it was the GUI development framework bundled with Smalltalk-80. Many years later, after several separate versions were developed, it was described and published as an (architectural) pattern. Leaving object orientation early history aside, lets get back to the matter of patterns that aren't targets for reification. One example is the Façade(185) design pattern. As one of the most frequently applied GoF patterns, described having as it's Intent to "provide a unified interface to a set of interfaces in a subsystem", there can be no doubt that it is indeed recurring. Looking at the Structure and Participants sections, we can see that it is very simple, just a Façade class that depends on many other (unidentified) subsystem classes inside a boundary. A consequence of this simplicity is that there would be little gain in incorporating this pattern into a programming language, as it doesn't really represent specific interactions that have to be recoded each time the pattern is applied.

One could respond that if that is the case, then the pattern is useless. It describes a solution so simple that a programmer who has never heard of the "Façade design pattern" would probably be able to construct it by himself. The error in this judgment is that a pattern's merit isn't measured by how interestingly or usefully it gives a solution to a problem. Knowing that its not hard to invent Façade does not detract from the fact that being able to talk about Façade is a big design win in itself. Patterns are as much an aid to communication as they are vehicles to disseminate knowledge.:
"Naming a pattern immediately increases our design vocabulary. It lets us design at a higher level of abstraction. Having a vocabulary for patterns lets us talk about them with our colleagues, in our documentation, and even to ourselves. It makes it easier to think about designs and to communicate them and their trade-offs to others."[GoF, page 3]
On the other hand, we already mentioned that some of the patterns are indeed amenable to reification. In this case there are two paths to choose from: coding it as a library atop usual language abstraction mechanisms or incorporating it as a basic language feature. Dominus second post argues that this choice is a mere implementation detail. Now, to paraphrase him, his point seems completely daft, which may I interpret to mean that there's something that went completely over my head. The difference is obvious: if some feature can be implemented as a library, any programmer that wants it can write it exactly one time and reuse as appropriate; no language modification required. As a contributor to CPAN , he should know well that nowadays much software reuse is done through open source libraries, lowering the recode count to zero for it's users. This is a crucial distinction, as adding something to the language has a huge impact on the whole technical ecosystem, from increased barrier to entry to pernicious interaction between features (non-orthogonality).

Lets take another look at Dominus' central proposition:
"(...) when pattern P applies to language L, then, to the extent that some programmer on some project finds themselves needing to use P in their project, the use of P indicates a deficiency in language L for that project. "
I believe the most important question here is: what does it mean to "use [pattern] P"? If you take it to mean follow the steps from P's description, possibly copying some code from the "Implementation" section, then this reasoning makes sense. But patterns are not merely recipes, and a pattern language is not a cookbook! One way in that patterns differ from recipes is that, as we saw in Façade, the Solution section can be of little importance compared to the communication gain from just naming the pattern. Another difference is that there is wide latitude in implementation strategies for each pattern, not to mention cases where multiple patterns present themselves as design alternatives for a set of forces.

Speculating a bit, I believe the origin of this antagonism toward design patterns is that the idea of a recurrent structure is antithetical to a certain ethos present in the software development community. Mark Dominus summarizes this feeling well in this passage "As in all programming, the identification of commonalities should be followed by an abstraction step in which the common parts are merged into a single solution." It can also be formulated as a call to arms: Don't repeat yourself! Or better yet, DRY! (the love for TLAs is another part of the programming ethos :). I don't dispute that eliminating repetition is an important design heuristic, but I sometimes wonder if it can be taken too far, becoming a sort of factorization fetishism that is detrimental to productivity.

7 comments:

Rodrigo said...

Só para contribuir um pouco, com meu vastíssimo conhecimento em inglês.
Já notou que a linguagem java dá de graça o padrão Strategy com interfaces? Que o Java 5 implementa um padrão de metadados que é o annotation. O C# já trás um observer de bandeja. É interessante ver que certos recursos de linguagem não esbarram nos padrões por acidente.

Anonymous said...

E vamos nós para uma série de "padrões = templates".
Blocks em Smalltalk substituem o papel de comands. Subprojeto commands da Jakarta oferecem commands e chains.
Iterator é uma classe em Java. Prototype é uma implementação de Cloneable com deep copy. O Listener é uma implementação do padrão Observer no AWT/Swing/SWT do Java.

Outros padrões são implementados de uma maneira ou de outra. Por exemplo, qualquer aluno de primeiro de computação esbarra no padrão composite (listas ligadas, árvores, grafos, etc) com a diferença que a implementação pode ou não possuir herança, enquanto que o padrão descrito no GoF usa obrigatoriamente (seria algum tipo de obsessão por heredidade!?).

Rafaeldff said...

Oi anonymous. Vamos la, então.

Primeiro tenho que deixar claro que eu procuro refutar a tese "todos os padroes sao templates". Isso nao implica em que eu afirme que "nenhum padrao eh um template". Ao contrario: Iterator, por exemplo, tem uma estrutura bastante rigida e sem muita latitude para o implementador.

Os outros exemplos sao mais complicados. Blocks em Smalltalk facilitam muito a aplicacao do padrao Command, mas dizer que eles "substituem o papel de commands" eh errado. Por um lado, blocks servem para mais coisas do que commands (sao otimos para implementar Strategies, por exemplo). Por outro, as vezes faz sentido programar explicitamente uma classe para o Command, um exemplo, citado no GoF, eh quando se deseja guardar estado no command para permitir Undo. Prototype eh um padrao bastante obvio (o primeiro uso citado eh do Sutherland em 1963!), e, acredito eu, vale mais pela adicao ao vocabulário do que pela estrutura (portanto, nao faz sentido considera-lo como um template). Nisso ele eh similar ao Facade, que eu mencionei no post, e ao Mediator.

Quanto ao Composite, acho dificil que uma lista ligada se encaixe como uma realizacao do padrao. Veja o Intent: "Compose objects into tree structures to represent part-whole hierarquies". A heranca no caso, serve para distinguir o comportamento das folhas dos demais nos, usando polimorfismo para isso. Me parece uma aplicacao perfeitamente adequada de heranca. E "acusar" de obsessao por hereditariedade a obra que tornou famosa a expressao "favor composition over inheritance" eh meio estranho.

[Peco desculpas pelos acentos, problemas tecnicos]

Rodrigo said...

Rafael,

Você é figura cara. E certamente uma pessoa bem esclarecida sobre o controverso assunto: "design patterns". Sim, sim... Eu mudei muito minha opinião depois da minha última postagem. Mas faço aqui algumas observações, talvez defendendo meu pensamento anterior:
1) Ok! Concordamos que alguns padrões podem vir pré-implementados. Eu arrisco citar Strategy, Iterator, Observer e o Delegate.
2) Outros a receita não ajuda muito como no caso do Prototype.
3) (Mas, e o que eu acho mais interessante é que) eu reforço minha opinião sobre o Composite e as listas ligadas.
(1) Toda lista é uma árvore.
(Um caso particular de árvore onde cada nó possui um único filho.)
(2) Pela intenção, uma lista ligada utiliza uma estrutura em árvore (conforme (1),) para representar a hierarquia parte-todo.
CQD.
Vou adiante: Por (2), posso sugerir uma implementação da "intenção" do Composite em C, dessa forma:
struct node {
struct* node next;
int value;
}
Lindo, né!? Eu fico até emocionado...
4) Agora, faço a seguinte pergunta: Eu devo julgar os padrões pela intenção ou pela "implementação" (isto é, a descrição do Padrão no GoF, "obsessivo" por heranças). Se você me disser que devo julgar pela implementação então a conclusão que chego é que meu tipo node não é um Composite, mas seria do contrário.
5) Como eu disse, acho o GoF obsessivo por heranças. Implementações mais simples satisfazem perfeitamente as intenções propostas. Dou outro exemplo: Decorator
Em java...
public class Component {
}
public class RodrigoDecorator extends Component {
private Component component;
}
Nada de classes "concretas" que herdem de Decorator. Meu RodrigoDecorator já é uma classe concreta. O dia que eu tiver a necessidade de mudar isso, eu refatoro.

6) E qual seria a vantagem de utilizar implementações mais simples para descrever um padrão? Porque assim seria mais óbvio o "padrão". No caso do Decorator, toda utilização de composição + agregação seria um exemplo de RodrigoDecorator. O exemplo complicado do GoF de Decorator também teria um RodrigoDecorator.
7) Claro que continua fazendo sentido dizer que uma Interface específica é um Strategy... É uma forma de comunicar aos leitores daquela Interface que o autor tinha em mente uma seleção de Algoritmos (talvez em tempo de execução). Mas eu acho que seria mais "honesto" validar que Interface é a implementação de Strategy em Java ( afinal, toda vez que você utiliza uma interface, você permite uma seleção de implementações em tempo de execução).


É isso ai!


(Me desculpe poluir seu blog internacionalizado com outro post em português :( )

Rafaeldff said...

"Internacionalizado" que nada... Termos mais adequados são "indeciso" ou "esquizofrenico", com essa alternação inconsistente entre português e inglês (mais certo seria eu manter blogs separados, mas ja tenho dificuldade em levar esse e o da sun; mais um é a ultima coisa que eu quero :). Falando no assunto, acho que já ta na hora de vc abrir um seu, hein?

Vamos por partes, como diria Leibniz:
1) Ainda não encontrei Delegate descrito em forma de padrao, mas eu concordaria com o Iterator e o Observer (muito embora, o observer de margem a uma variedade grande de implementacoes; veja a descricao dele no POSA).
2) Blz.
3)Toda arvore eh um grafo; todo grafo eh um conjunto de arestas, que sao pares ordenados, que tb são conjuntos. Logo toda lista ligada eh um conjunto. Se ela for finita então dá ate para provar que não passa de superconjuntos aninhados do conjunto vazio. Por isso que ciência da computação é diferente de engenharia de software (se é q isso existe). Volto ao intent: uma lista ligada tem a intenção de representar "whole-part hierarchies"? Eu acho que a resposta e não, e por isso trata-la como se fosse um composite não ajuda em nada.
4)Vou deixar o John Vlissides responder essa:
“It seems you can’t overemphasize that a pattern’s Structure diagram is just an example, not a specification. It portrays the implementation we see most often. As such the Structure diagram will probably have a lot in common with your own implementation, but differences are inevitable and actually desirable. At very least you will rename the participants as appropriate for your domain. Vary the implementation trade-offs, and your implementation might start looking a lot different from the Structure diagram.
As for whether one is following the pattern or not, who cares? The pattern is a means to an end, not an end itself. Following it in any strict sense is immaterial. If the pattern solves your problem directly, that’s great; if you have to bend it a bit, that’s great too. Even if the pattern merely inspires you toward an altogether different solution, it has still proven useful. The only potential problem here lies in the documentation phase, when you’re describing your solution in terms of patterns. You don’t want to mislead someone with irrelevant patterns. If you identify a set of classes as adhering to a pattern, make sure they fulfill the pattern’s intent. If the connection is tenuous, don’t mention the pattern; otherwise you’re sure to confuse more than clarify. ”

5 e 6) Vou reler o Decorator e te respondo depois ;)
7) Eu acho que interface em java serve para muito mais coisa do que só implementar o strategy. Primeiro, serve para o State também, que eh quase um padrão irmao. Depois, junto com creation methods*, eh muito util para separar a definição de uma API de sua implementação, tipo os métodos da classe java.util.Collections. Estes são os usos que me passaram pela cabeça agora, mas não tenho duvida que existem muitos mais que não se encaixam bem como sendo “strategies”.


*Que eu normalmente chamaria de factory methods, mas quero eviter confusao com o factory-method do GoF que e um conceito bem mais especifico)

Hilbert said...

Hello! I read this article! Big thanks to author, very interesting. Write more.

Cristiano said...

Very interesting this discussion about PAtterns! Thanks a lot for you all! ;-)