Princípio de menor espanto (POLA) e interfaces

17

Um bom quarto de século atrás, quando eu estava aprendendo C ++, fui ensinado que as interfaces deveriam perdoar e, na medida do possível, não se importar com a ordem em que os métodos foram chamados, já que o consumidor pode não ter acesso à fonte ou documentação em vez de esta.

No entanto, sempre que mentorizei programadores juniores e desenvolvedores seniores, eles reagiram com espanto, o que me fez pensar se isso era realmente uma coisa ou se estava fora de moda.

Tão claro quanto a lama?

Considere uma interface com estes métodos (para criar arquivos de dados):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

Agora, é claro que você pode simplesmente passar por eles em ordem, mas diga que não se importa com o nome do arquivo (pense a.out) ou com o cabeçalho e a sequência do trailer incluídos, basta ligar AddDataLine.

Um exemplo menos extremo pode ser a omissão de cabeçalhos e trailers.

Ainda outro pode estar definindo as seqüências de cabeçalho e trailer antes que o arquivo seja aberto.

Esse é um princípio de design de interface que é reconhecido ou apenas o caminho POLA antes de receber um nome?

OBS: não fique atolado nas minúcias dessa interface, é apenas um exemplo para o bem desta pergunta.

Robbie Dee
fonte
10
O princípio de "menor espanto" é muito mais prevalente no design da interface do usuário do que no design da "interface do programador de aplicativos". O motivo é que não se pode esperar que um usuário de um site ou programa leia todas as instruções antes de usá-lo, enquanto um programador deve, pelo menos em princípio, ler os documentos da API antes de programar com eles.
Kilian Foth
7
@KilianFoth: Tenho certeza de que a Wikipedia está errada sobre isso - o POLA não é apenas sobre design de interface do usuário, o termo "princípio da menor surpresa" (que é exatamente o mesmo) também é usado por Bob Martin para design de funções e classes em seu Livro "Código Limpo".
Doc Brown
2
Muitas vezes, uma interface imutável é melhor de qualquer maneira. Você pode especificar todos os dados que deseja definir no momento da construção. Nenhuma ambiguidade restante e a classe se torna mais simples de escrever. (Às vezes, esse esquema não é possível, é claro.)
usr
4
Discordo completamente sobre o POLA não se aplicar a APIs. Aplica-se a qualquer coisa que um humano cria para outros humanos. Quando as coisas agem conforme o esperado, elas são mais fáceis de conceituar e, portanto, criam uma carga cognitiva menor, permitindo que as pessoas façam mais coisas com menos esforço.
Gort the Robot

Respostas:

25

Uma maneira de se manter fiel ao princípio de menor espanto é considerar outros princípios, como ISP e SRP , ou mesmo DRY .

No exemplo específico que você deu, a sugestão parece ser a de que existe uma certa dependência de ordem para manipular o arquivo; mas sua API controla o acesso ao arquivo e o formato dos dados, que cheira um pouco a uma violação do SRP.

Editar / Atualizar: também sugere que a própria API está solicitando que o usuário viole o DRY, porque precisará repetir as mesmas etapas sempre que usar a API .

Considere uma API alternativa em que as operações de E / S sejam separadas das operações de dados. e onde a própria API 'possui' a ordem:

ContentBuilder

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

Com a separação acima, ContentBuildernão é necessário "fazer" nada além de armazenar as linhas / cabeçalho / trailer (talvez também um ContentBuilder.Serialize()método que conheça a ordem). Seguindo outros princípios do SOLID, não importa mais se você define o cabeçalho ou o trailer antes ou depois da adição de linhas, porque nada ContentBuilderé gravado no arquivo até que seja passado para ele FileWriter.Write.

Ele também tem o benefício adicional de ser um pouco mais flexível; por exemplo, pode ser útil gravar o conteúdo em um criador de logs de diagnóstico ou talvez passá-lo pela rede em vez de gravá-lo diretamente em um arquivo.

Ao projetar uma API, você também deve considerar o relatório de erros, seja um estado, um valor de retorno, uma exceção, um retorno de chamada ou outra coisa. O usuário da API provavelmente espera detectar programaticamente quaisquer violações de seus contratos ou até outros erros que não pode controlar, como erros de E / S de arquivos.

Ben Cottrell
fonte
Exatamente o que eu estava procurando - obrigado! A partir do artigo ISP: "(ISP) afirma que nenhum cliente deve ser forçado a depender de métodos que não usam"
Robbie Dee
5
Esta não é uma resposta ruim, no entanto, ainda assim, o construtor de conteúdo pode ser implementado de uma maneira em que a ordem das chamadas SetHeaderou seja AddLineimportante. Para eliminar essa dependência de ordem não é ISP nem SRP, é simplesmente POLA.
Doc Brown
Quando o pedido é importante, você ainda pode satisfazer o POLA definindo as operações para que a execução de etapas posteriores exija um valor retornado das etapas anteriores, reforçando a ordem com o sistema de tipos. FileWriterpoderia exigir o valor da última ContentBuilderetapa do Writemétodo para garantir que todo o conteúdo de entrada seja concluído, tornando InvalidContentExceptiondesnecessário.
21816 Dan Lyons
@ DanLyons Sinto que isso é bastante próximo da situação que o solicitante está tentando evitar; onde o usuário da API precisa conhecer ou se preocupar com o pedido. Idealmente, a própria API deve impor o pedido, caso contrário, está potencialmente pedindo ao usuário que viole o DRY. Essa é a razão para se separar ContentBuildere permitir FileWriter.Writeencapsular esse pouco de conhecimento. A exceção seria necessária caso algo estivesse errado com o conteúdo (por exemplo, como um cabeçalho ausente). Um retorno também pode funcionar, mas não sou fã de transformar exceções em códigos de retorno.
precisa
Mas definitivamente vale a pena adicionar mais notas sobre DRY e solicitar a resposta.
precisa
12

Não se trata apenas de POLA, mas também de impedir o estado inválido como uma possível fonte de erros.

Vamos ver como podemos fornecer algumas restrições ao seu exemplo sem fornecer uma implementação concreta:

Primeiro passo: não permita que nada seja chamado antes de um arquivo ser aberto.

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

Agora deve ser óbvio que CreateDataFileInterface.OpenFiledeve ser chamado para recuperar uma DataFileInterfaceinstância em que os dados reais podem ser gravados.

Segunda etapa: verifique se os cabeçalhos e os trailers estão sempre definidos.

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

Agora você deve fornecer todos os parâmetros necessários antecipadamente para obter DataFileInterface: nome do arquivo, cabeçalho e trailer. Se a sequência de trailer não estiver disponível até que todas as linhas sejam gravadas, você também poderá mover esse parâmetro para Close()(possivelmente renomeando o método para WriteTrailerAndClose()) para que o arquivo pelo menos não possa ser concluído sem uma sequência de trailer.


Para responder ao comentário:

Eu gosto da separação da interface. Mas estou inclinado a pensar que sua sugestão sobre aplicação (por exemplo, WriteTrailerAndClose ()) está prestes a violar o SRP. (Isso é algo com o qual luto várias vezes, mas sua sugestão parece ser um exemplo possível.) Como você reagiria?

Verdade. Não queria me concentrar mais no exemplo do que o necessário para expressar minha opinião, mas é uma boa pergunta. Nesse caso, acho que chamaria isso Finalize(trailer)e argumentaria que não faz muito. Escrever o trailer e fechar são meros detalhes de implementação. Mas se você não concorda ou tem uma situação semelhante em que é diferente, aqui está uma solução possível:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

Na verdade, eu não faria isso neste exemplo, mas mostra como realizar a técnica consequentemente.

A propósito, presumi que os métodos realmente devam ser chamados nessa ordem, por exemplo, para escrever sequencialmente muitas linhas. Se isso não for necessário, eu sempre preferiria um construtor, como sugerido por Ben Cottrel .

Fabian Schmengler
fonte
1
Você caiu na armadilha que eu lhe expliquei explicitamente para evitar desde o início. Não é necessário um nome de arquivo - nem o cabeçalho e o trailer. Mas o tema geral da divisão a interface é um bom modo +1 :-)
Robbie Dee
Ah, então eu entendi errado você, pensei que isso estivesse descrevendo a intenção do usuário, não a implementação.
Fabian Schmengler
Eu gosto da separação da interface. Mas estou inclinado a pensar que sua sugestão sobre aplicação (por exemplo WriteTrailerAndClose()) está próxima de uma violação do SRP. (Isso é algo com o qual lutei várias vezes, mas sua sugestão parece ser um exemplo possível.) Como você reagiria?
kmote
1
resposta @kmote era demasiado longo para um comentário, ver meu update
Fabian Schmengler
1
Se o nome do arquivo for opcional, você poderá fornecer uma OpenFilesobrecarga que não exija uma.
precisa saber é o seguinte