O que torna o iterador um padrão de design?

9

Fiquei me perguntando o que torna o Iterator especial quando comparado a outras construções semelhantes, e isso fez o Gang of Four listá-lo como um padrão de design.

O Iterador é baseado no polimorfismo (uma hierarquia de coleções com uma interface comum) e na separação de preocupações (a iteração sobre as coleções deve ser independente da forma como os dados são estruturados).

Mas e se substituirmos a hierarquia de coleções por, por exemplo, uma hierarquia de objetos matemáticos (número inteiro, float, complexo, matriz etc.) e o iterador por uma classe que representa algumas operações relacionadas a esses objetos, por exemplo, funções de poder. O diagrama da classe seria o mesmo.

Provavelmente, poderíamos encontrar muitos exemplos semelhantes, como Writer, Painter, Encoder e provavelmente melhores, que funcionam da mesma maneira. No entanto, eu nunca ouvi nenhum deles ser chamado de Padrão de Design.

Então, o que torna o Iterator especial?

É o fato de ser mais complicado porque requer um estado mutável para armazenar a posição atual na coleção? Mas então, o estado mutável geralmente não está sendo considerado desejável.


Para esclarecer meu argumento, deixe-me dar um exemplo mais detalhado.

Aqui está o nosso problema de design:

Digamos que temos uma hierarquia de classes e uma operação definida nos objetos dessas classes. A interface desta operação é a mesma para cada classe, mas as implementações podem ser completamente diferentes. Supõe-se também que faz sentido aplicar a operação várias vezes no mesmo objeto, digamos com parâmetros diferentes.

Aqui está uma solução sensata para o nosso problema de design (praticamente uma generalização do padrão do iterador):

Para separação de preocupações, as implementações da operação não devem ser adicionadas como funções à hierarquia de classes original (objetos de operando). Como queremos aplicar a operação várias vezes no mesmo operando, ela deve ser representada por um objeto que mantém uma referência ao operando, não apenas por uma função. Portanto, o objeto operando deve fornecer uma função que retorna o objeto que representa a operação. Este objeto fornece uma função que executa a operação real.

Um exemplo:

Existe uma classe base ou interface MathObject(nome estúpido, eu sei, talvez alguém tenha uma idéia melhor.) Com classes derivadas MyIntegere MyMatrix. Para cada MathObjectuma operação Powerdeve ser definida que permita o cálculo de quadrado, cubo e assim por diante. Para podermos escrever (em Java):

MathObject i = new MyInteger(5);
Power powerOfFive = i.getPower();
MyInteger square = powerOfFive.calculate(2); // should return 25
MyInteger cube = powerOfFive.calculate(3); // should return 125
Frank Puffer
fonte
5
"O diagrama de classes seria o mesmo" - e daí? Um padrão de design não é um diagrama de classes. É uma abstração de alto nível para uma classe de soluções para um problema recorrente.
Doc Brown
@DocBrown: Certo, mas não são operações matemáticas, gravação de objetos em um arquivo, saída gráfica ou problemas recorrentes de dados de codificação, assim como a iteração?
Frank soprador
A escolha do padrão de design é subjetiva (ou seja, aos olhos dos "designers" ou das pessoas que julgam os designs). A nomeação de padrões de design deve ser independente de domínio (para que não nos distraímos ao pensar que é específico de domínio). Apenas minha opinião, eu não tenho nenhum juiz para citar.
rwong
@FrankPuffer Se você esboçar uma solução comum para gravar objetos em um arquivo, poderá anotar sua solução e chamá-la de Padrão de Gravação de Objetos, se isso for útil.
Brandin
3
Você está pensando demais nisso. Um Design Pattern é uma solução conhecida para um problema de computação comum, e isso é tudo. Você usa o padrão quando reconhece e aplica os benefícios que ele oferece.
Robert Harvey

Respostas:

9

A maioria dos padrões do livro do GoF tem o seguinte em comum:

  • eles resolvem problemas básicos de design, usando meios orientados a objetos
  • as pessoas geralmente enfrentam esses problemas em programas arbitrários, independentemente do domínio ou da empresa
  • são receitas para tornar o código mais reutilizável, geralmente tornando-o mais sólido
  • eles apresentam soluções canônicas para esses problemas

Os problemas resolvidos por esses padrões são tão básicos que muitos desenvolvedores os entendem principalmente como soluções alternativas para os recursos ausentes da linguagem de programação , o que é um ponto de vista válido para o IMHO (observe que o livro do GoF é de 1995, onde Java e C ++ não ofereciam tantos recursos como hoje).

O padrão do iterador se encaixa bem nessa descrição: ele resolve um problema básico que ocorre com muita frequência, independentemente de qualquer domínio específico, e, como você escreveu por si mesmo, é um bom exemplo de "separação de preocupações". Como você certamente sabe, o suporte direto ao iterador é algo que você encontra em muitas linguagens de programação desprezíveis atualmente.

Agora compare isso com os problemas que você escolheu:

  • escrevendo em um arquivo - isso é IMHO simplesmente não é "básico" o suficiente. É um problema muito específico. Também não há uma solução boa e canônica - há várias abordagens diferentes sobre como gravar em um arquivo e nenhuma "melhor prática" clara.
  • Pintor, codificador: o que você tem em mente, esses problemas me parecem ainda menos básicos e nem mesmo independentes de domínio.
  • ter a função "poder" disponível para diferentes tipos de objetos: à primeira vista, pode valer a pena ser um padrão, mas sua solução proposta não me convence - parece mais uma tentativa de transformar a função poder em algo semelhante a o padrão do iterador. Eu implementei muito código com cálculos de engenharia, mas não me lembro de uma situação em que uma abordagem semelhante ao seu objeto de função de energia teria me ajudado (no entanto, iteradores é algo com o qual tenho que lidar diariamente).

Além disso, não vejo nada no seu exemplo de função de poder que não possa ser interpretado como uma aplicação do padrão de estratégia ou de comando, o que significa que essas partes básicas já estão no livro do GoF. Uma solução melhor pode conter métodos de sobrecarga ou extensão do operador, mas essas são coisas sujeitas a recursos de idioma, e é exatamente isso que o "OO significa" usado pelo "Gang" não poderia fornecer.

Doc Brown
fonte
The problems solved by these patterns are so basic that many developers think their main purpose is to be workarounds for missing programming language features- A ironia é que os desenvolvedores de software usam rotineiramente padrões de design de software com 20 anos de idade, enquanto ainda acreditam que estão escrevendo códigos de última geração.
Robert Harvey
@RobertHarvey: Eu não acho que muitos desenvolvedores implementariam o padrão de iterador hoje da maneira "OO" sugerida pelo GoF. Eles o implementam normalmente pelos meios fornecidos pelo idioma ou pela sua lib padrão (por exemplo, em C # usando IEnumerablee yield). Mas para outros padrões do GoF, o que você escreveu provavelmente pode ser verdade.
Doc Brown
11
Relevante para workarounds for missing programming language features: blog.plover.com/prog/johnson.html
jrw32982 suporta Monica
8

The Gang of Four cita a definição de padrão de Christopher Alexander:

Cada padrão descreve um problema que ocorre repetidamente em nosso ambiente e, em seguida, descreve o núcleo da solução para esse problema […]

Qual é o problema resolvido pelos iteradores?

Intenção: Forneça uma maneira de acessar os elementos de um objeto agregado sequencialmente, sem expor sua representação subjacente.

Aplicabilidade: Use o padrão Iterator

  • acessar o conteúdo de um objeto agregado sem expor sua representação interna
  • para suportar várias travessias de objetos agregados
  • para fornecer uma interface uniforme para atravessar diferentes estruturas agregadas (ou seja, para suportar iteração polimórfica).

Portanto, alguém poderia argumentar que o padrão do iterador é, por definição, específico do domínio para coleções. E isso está perfeitamente bem. Outros padrões, como o padrão do intérprete, são específicos do domínio para idiomas específicos do domínio, os padrões de fábrica são específicos do domínio para a criação de objetos,…. É claro que esse é um entendimento bastante tolo de "específico de domínio". Desde que seja um par recorrente entre problema e solução, podemos chamá-lo de padrão.

E é bom que o padrão Iterator exista. Coisas ruins acontecem se você não usá-lo. Meu anti-exemplo favorito é o Perl. Aqui, cada coleção (matriz ou hash) inclui o estado do iterador como parte da coleção. Por que isso é ruim? Podemos iterar facilmente um hash com um tempo - cada loop:

while (my ($key, $value) = each %$hash) {
  say "$key => $value";
}

Mas e se chamarmos uma função no corpo do loop?

while (my ($key, $value) = each %$hash) {
  do_something_with($key, $value, $hash);
}

Esta função agora pode fazer praticamente o que quiser, exceto:

  • adicione ou exclua entradas de hash, pois elas alterariam a ordem da iteração de maneira imprevisível (em C ++ - fala, elas invalidariam o iterador).
  • itere a mesma tabela de hash sem fazer uma cópia, pois isso consumiria o mesmo estado de iteração. Opa

Se a função chamada deve usar o iterador, o comportamento do nosso loop se torna indefinido. Isso é um problema. E o padrão do iterador tem uma solução: coloque todo o estado da iteração em um objeto separado, criado por iteração.

Sim, é claro que o padrão do iterador está relacionado a outros padrões. Por exemplo, como o iterador é instanciado? Em Java, temos um genérico Iterable<T>e Iterator<T>interface. Um iterável concreto ArrayList<T>cria um tipo específico de iterador, enquanto um HashSet<T>pode fornecer um tipo de iterador completamente diferente. Isso me lembra muito o padrão abstrato de fábrica, onde a Iterable<T>é a fábrica abstrata e o Iteratoré o produto.

Um iterador polimórfico também pode ser interpretado como um exemplo do padrão de estratégia. Por exemplo, uma árvore pode oferecer diferentes tipos de iteradores (pré-encomenda, encomenda, pós-venda,…). Externamente, todos compartilhariam uma interface do iterador e produziriam elementos em alguma sequência. O código do cliente precisa depender apenas da interface do iterador, não de um algoritmo de passagem em árvore específico.

Os padrões não existem isolados, independentes um do outro. Alguns padrões são soluções diferentes para o mesmo problema, e alguns padrões descrevem a mesma solução em contextos diferentes. Alguns padrões implicam outro. Além disso, o espaço do padrão não é fechado quando você vira a última página do livro Design Patterns (consulte também sua pergunta anterior O Gang of Four explorou completamente o “Espaço do Padrão”? ). Os padrões descritos no livro Design Patterns são muito flexíveis e amplos, abertos a variações infinitas e definitivamente não são os únicos padrões existentes.

Os conceitos que você lista (escrever, pintar, codificar) não são padrões, porque não descrevem uma combinação problema-solução. Uma tarefa como "preciso gravar dados" ou "preciso codificar dados" não é realmente um problema de design e não inclui uma solução; uma "solução" que consiste apenas em "Eu sei, vou criar uma classe Writer" não tem sentido. Mas se tivermos um problema como “Não quero que gráficos meio renderizados sejam pintados na tela”, pode existir um padrão: “Eu sei, usarei gráficos com buffer duplo!”

amon
fonte
Boa resposta, obrigado. Ainda não estou completamente convencido de que o que você escreve no último parágrafo se aplica aqui. Eu editei minha pergunta para explicar o que quero dizer.
Frank soprador