A consistência deve ser preferida à convenção de programação?

14

Ao projetar uma classe, a consistência no comportamento deve ser preferida à prática comum de programação? Para dar um exemplo específico:

Uma convenção comum é a seguinte: se uma classe possui um objeto (por exemplo, ele o criou), é responsável por limpá-lo quando terminar. Um exemplo específico seria no .NET que, se sua classe possuir um IDisposableobjeto, ele será descartado no final de sua vida útil. E se você não o possui, não o toque.

Agora, se olharmos para a StreamWriterclasse no .NET, podemos encontrar na documentação que ela fecha o fluxo subjacente quando está sendo fechada / descartada. Isso é necessário nos casos em que o StreamWriterinstanciado passa um nome de arquivo, pois o escritor cria o fluxo de arquivos subjacente e, portanto, precisa fechá-lo. No entanto, também é possível passar em um fluxo externo que o gravador também fecha.

Isso me incomodou muitas vezes (sim, eu sei que você pode criar um invólucro sem fechamento, mas esse não é o ponto), mas aparentemente a Microsoft tomou a decisão de que é mais consistente sempre fechar o fluxo, não importa de onde ele veio.

Quando encontro esse padrão em uma de minhas classes, geralmente crio um ownsFooBarsinalizador que é definido como falso nos casos em que FooBaré injetado pelo construtor e verdadeiro caso contrário. Dessa forma, a responsabilidade de limpá-lo é passada ao chamador quando ele passa a instância explicitamente.

Agora, estou me perguntando se talvez a consistência deva ser favorável às melhores práticas (ou talvez minha melhor prática não seja tão boa)? Algum argumento a favor / contra?

Editar para esclarecimentos

Com "consistência", quero dizer: o comportamento consistente da classe sempre tomando posse (e fechando o fluxo) versus "prática recomendada" para tomar posse de um objeto apenas se você o criou ou transferiu explicitamente a propriedade.

Como um exemplo em que é irritante:

Suponha que você tenha duas classes (de alguma biblioteca de terceiros) que aceitam um fluxo para fazer algo com ele, como criar e processar alguns dados:

 public class DataProcessor
 {
     public Result ProcessData(Stream input)
     {
          using (var reader = new StreamReader(input))
          {
              ...
          }
     }
 }

 public class DataSource
 {
     public void GetData(Stream output)
     {
          using (var writer = new StreamWriter(output))
          {
               ....
          }
     }
 }

Agora eu quero usá-lo assim:

 Result ProcessSomething(DataSource source)
 {
      var processor = new DataProcessor();
      ...
      var ms = new MemoryStream();
      source.GetData(ms);
      return processor.ProcessData(ms);
 }

Isso falhará com uma exceção Cannot access a closed streamno processador de dados. É um pouco construído, mas deve ilustrar o ponto. Existem várias maneiras de corrigi-lo, mas, no entanto, sinto que trabalho em torno de algo que não deveria.

ChrisWue
fonte
Você se importaria de elaborar por que o comportamento ilustrado do StreamWriter causa dor? Deseja consumir o mesmo fluxo em outro lugar? Por quê?
Job
@Job: Eu esclareço minha pergunta
ChrisWue

Respostas:

9

Também uso essa técnica, chamo de 'transferência' e acredito que mereça o status de um padrão. Quando um objeto A aceita um objeto descartável B como um parâmetro de tempo de construção, ele também aceita um booleano chamado 'handoff' (que o padrão é false) e, se for verdadeiro, o descarte de A cascata para o descarte de B.

Eu não sou a favor da proliferação das escolhas infelizes de outras pessoas, por isso nunca aceitaria as más práticas da Microsoft como uma convenção estabelecida, nem consideraria o seguimento cego das escolhas infelizes das outras pessoas como "comportamento consistente" de qualquer forma, forma ou forma .

Mike Nakis
fonte
Nota: Eu escrevi uma definição formal desse padrão em uma postagem no meu blog: michael.gr: O Padrão "Handoff" .
13133 Mike Nakis
Como uma extensão adicional do handOffpadrão, se essa propriedade for configurada no construtor, mas existir outro método para modificá-la, esse outro método também deverá aceitar um handOffsinalizador. Se handOfffoi especificado para o item atualmente em espera, ele deve ser descartado, o novo item deve ser aceito e o handOffsinalizador interno atualizado para refletir o status do novo item. O método não precisa verificar as referências antigas e novas quanto à igualdade, pois se a referência original foi "transferida" todas as referências mantidas pelo chamador original devem ser abandonadas.
Supercat
@supercat Concordo, mas tenho um problema com a última frase: se a transferência for verdadeira para o item em espera, mas o item recém-fornecido for igual por referência ao item em espera, e então descartá-lo. está efetivamente descartando o item recém-fornecido. Eu acho que o fornecimento do mesmo item deve ser ilegal.
Mike Nakis
O reabastecimento do mesmo item geralmente deve ser ilegal, a menos que o objeto seja imutável, não possua nenhum recurso e não se importe com o descarte. Por exemplo, se as Disposesolicitações forem ignoradas para os pincéis do sistema, um método "color.CreateBrush` poderá, à sua vontade, criar um novo pincel (que exigiria descarte) ou retornar um pincel do sistema (que ignoraria o descarte). A maneira mais simples de evitar reutilização de um objeto se ele se preocupa com disposição, mas permitir a reutilização se isso não acontecer, é para eliminá-lo.
supercat
3

Eu acho que você está certo, para ser honesto. Eu acho que a Microsoft mexeu com a classe StreamWriter, especificamente, pelos motivos que você descreve.

No entanto, já vi muitos códigos em que as pessoas nem tentam descartar seu próprio fluxo porque a estrutura faz isso por elas, portanto, corrigi-lo agora fará mais mal do que bem.

pdr
fonte
2

Eu iria com consistência. Como você mencionou para a maioria das pessoas, faz sentido que, se seu objeto criar um objeto filho, ele será o proprietário e o destruirá. Se for fornecida uma referência externa, em algum momento, "externa" também está segurando o mesmo objeto e não deve ser de propriedade. Se você deseja transferir a propriedade de fora para o seu objeto, use os métodos Anexar / Desanexar ou um construtor que aceite um booleano que deixe claro que a propriedade é transferida.

Depois de digitar tudo isso para obter uma resposta completa, acho que a maioria dos padrões de design genéricos não necessariamente se enquadram na mesma categoria que as classes StreamWriter / StreamReader. Eu sempre pensei que a razão pela qual a MS escolheu o StreamWriter / Reader automaticamente assumir a propriedade é porque, neste caso específico, ter propriedade compartilhada não faz muito sentido. Você não pode ter dois objetos diferentes simultaneamente lendo / gravando no mesmo fluxo. Tenho certeza de que você poderia escrever algum tipo de divisão de tempo determinística, mas isso seria um inferno de um antipadrão. Então é provavelmente por isso que eles disseram, já que não faz sentido compartilhar um fluxo, vamos sempre assumi-lo. Mas eu não consideraria esse comportamento como uma convenção genérica em todas as classes.

Você pode considerar o Streams um padrão consistente, onde o objeto que está sendo transmitido não pode ser compartilhado do ponto de vista do design e, portanto, sem nenhum sinalizador adicional, o leitor / gravador apenas assume sua propriedade.

DXM
fonte
Esclarei minha pergunta - com consistência, eu quis dizer o comportamento de sempre tomar posse.
ChrisWue
2

Seja cuidadoso. O que você considera "consistência" pode ser apenas uma coleção aleatória de hábitos.

"Coordenando interfaces de usuário para consistência", Boletim SIGCHI 20, (1989), 63-65. Desculpe, sem link, é um artigo antigo. "... um workshop de dois dias com 15 especialistas não conseguiu produzir uma definição de consistência". Sim, trata-se de "interface", mas acredito que se você pensar em "consistência" por um tempo, descobrirá que também não pode defini-lo.

Bruce Ediger
fonte
você está certo, se os especialistas se reúnem em círculos diferentes, mas ao desenvolver o código, achei fácil seguir algum padrão existente (ou hábito, se você preferir) com o qual minha equipe já está familiarizada. Por exemplo, outro dia, um de nossos funcionários estava perguntando sobre algumas operações assíncronas que tínhamos e quando eu disse a ele que se comporta da mesma maneira que a E / S sobreposta do Win32, em 30 segundos ele entendeu toda a interface ... neste caso, ambos eu e ele viemos do mesmo fundo back-end do servidor Win32. Consistência ftw! :)
DXM
@ Bruce, entendi seu ponto de vista - pensei que estava claro o que eu queria dizer com consistência, mas aparentemente não era.
ChrisWue