Programação para uso futuro de interfaces

42

Eu tenho um colega sentado ao meu lado que projetou uma interface como esta:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}

O problema é que, no momento, não estamos usando esse parâmetro "end" em nenhum lugar do nosso código, ele está lá porque podemos ter que usá-lo em algum momento no futuro.

Estamos tentando convencê-lo de que é uma má idéia colocar parâmetros em interfaces que não são úteis no momento, mas ele continua insistindo que muito trabalho será necessário se implementarmos o uso da data "final" em algum momento depois e tem que adaptar todo o código então.

Agora, minha pergunta é: existem fontes que estão lidando com um tópico como este de gurus de codificação "respeitados" aos quais podemos vinculá-lo?

sveri
fonte
29
"A previsão é muito difícil, especialmente sobre o futuro."
Jörg W Mittag
10
Parte do problema é que não é a melhor interface para começar. Obter Foos e filtrá-los são dois problemas separados. Essa interface está forçando você a filtrar a lista de uma maneira específica; uma abordagem muito mais geral é passar uma função que decide se o foo deve ser incluído. No entanto, isso só é elegante, se você tem acesso a Java 8.
Doval
5
Contrariar é ponto. Basta fazer esse método aceitar whos tipo personalizado por agora conter apenas o que você precisa e pode, eventualmente, adicionar o endparâmetro para esse objeto e até mesmo de TI padrão, a fim de não quebrar o código
Rémi
3
Com os métodos padrão do Java 8, não há razão para adicionar argumentos desnecessários. Um método pode ser adicionado posteriormente com uma implementação padrão que chama simplesmente o definido com, digamos null,. As classes de implementação podem substituir, conforme necessário.
Boris the Spider
1
@Doval Eu não sei sobre Java, mas em .NET às vezes você vai ver coisas como para evitar expor as peculiaridades de IQueryable(só pode tomar certas expressões) para o código fora do DAL
Ben Aaronson

Respostas:

62

Convide-o a aprender sobre YAGNI . A parte Racional da página da Wikipedia pode ser particularmente interessante aqui:

De acordo com aqueles que defendem a abordagem do YAGNI, a tentação de escrever código que não é necessário no momento, mas pode ser no futuro, tem as seguintes desvantagens:

  • O tempo gasto é gasto na adição, teste ou aprimoramento da funcionalidade necessária.
  • Os novos recursos devem ser depurados, documentados e suportados.
  • Qualquer novo recurso impõe restrições sobre o que pode ser feito no futuro; portanto, um recurso desnecessário pode impedir que os recursos necessários sejam adicionados no futuro.
  • Até que o recurso seja realmente necessário, é difícil definir completamente o que ele deve fazer e testá-lo. Se o novo recurso não for definido e testado corretamente, ele pode não funcionar corretamente, mesmo que eventualmente seja necessário.
  • Isso leva ao inchaço do código; o software se torna maior e mais complicado.
  • A menos que haja especificações e algum tipo de controle de revisão, o recurso pode não ser conhecido pelos programadores que poderiam fazer uso dele.
  • Adicionar o novo recurso pode sugerir outros novos recursos. Se esses novos recursos também forem implementados, isso poderá resultar em um efeito de bola de neve para a fluência do recurso.

Outros argumentos possíveis:

  • “80% do custo vitalício de um software é destinado à manutenção” . Escrever código na hora certa reduz o custo da manutenção: é preciso manter menos código e concentrar-se no código realmente necessário.

  • O código fonte é escrito uma vez, mas é lido dezenas de vezes. Um argumento adicional, não usado em lugar algum, levaria à perda de tempo para entender por que existe um argumento que não é necessário. Dado que esta é uma interface com várias implementações possíveis, torna as coisas apenas mais difíceis.

  • O código-fonte deve ser auto-documentado. A assinatura real é enganosa, pois um leitor pensaria que isso endafeta o resultado ou a execução do método.

  • As pessoas que escrevem implementações concretas dessa interface podem não entender que o último argumento não deve ser usado, o que levaria a diferentes abordagens:

    1. Eu não preciso end, então simplesmente ignorarei seu valor,

    2. Eu não preciso end, então lançarei uma exceção, se não for null,

    3. Não preciso end, mas tentarei usá-lo de alguma forma,

    4. Escreverei muito código que poderá ser usado mais tarde, quando endnecessário.

Mas lembre-se de que seu colega pode estar certo.

Todos os pontos anteriores se baseiam no fato de que a refatoração é fácil, portanto, adicionar um argumento posteriormente não exigirá muito esforço. Mas essa é uma interface e, como interface, pode ser usada por várias equipes que contribuem para outras partes do seu produto. Isso significa que alterar uma interface pode ser particularmente doloroso; nesse caso, o YAGNI não se aplica aqui.

A resposta do hjk fornece uma boa solução: adicionar um método a uma interface já usada não é particularmente difícil, mas em alguns casos, também tem um custo substancial:

  • Algumas estruturas não suportam sobrecargas. Por exemplo, e se bem me lembro (me corrija se estiver errado), o WCF do .NET não suporta sobrecargas.

  • Se a interface tiver muitas implementações concretas, adicionar um método à interface exigiria passar por todas as implementações e adicionar o método também.

Arseni Mourzenko
fonte
4
Eu acho que também é importante considerar a probabilidade desse recurso aparecer no futuro. Se você tiver certeza, ele será adicionado, mas não agora, por qualquer motivo, então eu diria que é um bom argumento a ter em mente, pois não é apenas um recurso "no caso".
Jeroen Vannevel
3
@ JeroenVannevel: certamente, mas isso é muito especulativo. Pela minha experiência, a maioria dos recursos que eu acreditava que certamente seriam implementados foram cancelados ou atrasados ​​para sempre. Isso inclui recursos que foram destacados originalmente como muito importantes pelas partes interessadas.
Arseni Mourzenko
26

mas ele continua insistindo que muito trabalho deverá ser feito se implementarmos o uso da data "final" algum tempo depois e precisarmos adaptar todo o código.

(algum tempo depois)

public class EventGetter implements IEventGetter {

    private static final Date IMPLIED_END_DATE_ASOF_20140711 = new Date(Long.MAX_VALUE); // ???

    @Override
    public List<FooType> getFooList(String fooName, Date start) throws Exception {
        return getFooList(fooName, start, IMPLIED_END_DATE_ASOF_20140711);
    }

    @Override
    public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception {
        // Final implementation goes here
    }
}

É tudo o que você precisa, sobrecarregando o método. O método adicional no futuro pode ser introduzido de forma transparente, sem afetar as chamadas para o método existente.

hjk
fonte
6
Exceto que sua alteração significa que não é mais uma interface e não será possível usar nos casos em que um objeto já deriva de um objeto e implementa a interface. É um problema trivial se todas as classes que implementam a interface estiverem no mesmo projeto (procure erros de compilação). Se for uma biblioteca usada por vários projetos, a adição do campo causa confusão e trabalho para outras pessoas em momentos esporádicos nos próximos x meses / anos.
Kieveli 11/07
1
Se a equipe do OP (salve o colega) realmente acredita que o endparâmetro é desnecessário agora e passou a usar o endmétodo -less em todo o (s) projeto (s), a atualização da interface é a menor das preocupações, pois torna-se necessário atualizar o classes de implementação. Minha resposta está voltada especificamente para a parte "adaptar todo o código", porque parece que o colega estava pensando na mesma linha de ter que atualizar os chamadores do método original em todo o projeto para introduzir um terceiro parâmetro padrão / falso .
Hjk
18

[Existem] gurus de codificação "respeitados" aos quais podemos ligá-lo [para convencê-lo]?

Os apelos à autoridade não são particularmente convincentes; melhor apresentar um argumento válido, independentemente de quem o tenha dito.

Aqui está uma reductio ad absurdum que deve convencer ou demonstrar que seu colega de trabalho está preso em estar "certo", independentemente da sensibilidade:


O que você realmente precisa é

getFooList(String fooName, Date start, Date end, Date middle, 
           Date one_third, JulianDate start, JulianDate end,
           KlingonDate start, KlingonDate end)

Você nunca sabe quando terá que se internacionalizar para o Klingon, então é melhor cuidar dele agora, porque isso exigirá muito trabalho para se adaptar e os Klingons não são conhecidos por sua paciência.

msw
fonte
22
You never know when you'll have to internationalize for Klingon. É por isso que você deve passar a Calendare deixar o código do cliente decidir se ele deseja enviar um GregorianCalendarou a KlingonCalendar(aposto que alguns já o fizeram). Duh. :-p
SJuan76
3
Que tal StarDate sdStarte StarDate sdEnd?
Afinal de contas
Todos esses são obviamente subclasses ou conversíveis em Date!
Darkhogg
Se ao menos fosse assim tão simples .
msw
@ msw, não tenho certeza se ele estava pedindo um "apelo à autoridade" quando pediu links para "respeitados gurus da codificação". Como você diz, ele precisa de "um argumento válido, independentemente de quem o tenha dito". As pessoas do tipo "guru" tendem a conhecer muitas delas. Também você esqueceu HebrewDate starte HebrewDate end.
Julio
12

Do ponto de vista da engenharia de software, acredito que a solução adequada para esse tipo de problema está no padrão do construtor. Esse é definitivamente um link dos autores do 'guru' para o seu colega http://en.wikipedia.org/wiki/Builder_pattern .

No padrão do construtor, o usuário cria um objeto que contém os parâmetros. Este contêiner de parâmetro será passado para o método Isso cuidará de qualquer extensão e sobrecarga de parâmetros que seu colega precisaria no futuro, ao mesmo tempo em que tornará tudo muito estável quando forem necessárias alterações.

Seu exemplo se tornará:

public interface IEventGetter {
    public List<FooType> getFooList(ISearchFooRequest req) {
        throws Exception;
    ....
    }
}

public interface ISearchFooRequest {
        public String getName();
        public Date getStart();
        public Date getEnd();
        public int getOffset();
        ...
    }
}

public class SearchFooRequest implements ISearchFooRequest {

    public static SearchFooRequest buildDefaultRequest(String name, Date start) {
        ...
    }

    public String getName() {...}
    public Date getStart() {...}
    ...
    public void setEnd(Date end) {...}
    public void setOffset(int offset) {...}
    ...
}
InformedA
fonte
3
Essa é uma boa abordagem geral, mas, neste caso, parece apenas empurrar o problema de IEventGetterpara ISearchFootRequest. Em vez disso, sugiro algo como public IFooFiltercom membro public bool include(FooType item)que retorna se deve ou não incluir o item. Então implementações de interface indivíduo pode decidir como eles vão fazer isso de filtragem
Ben Aaronson
@ Ben Aaronson Eu só editar o código para mostrar como objeto de solicitação de parâmetro é criada
InformedA
O construtor é desnecessário aqui (e, francamente, um padrão usado em excesso / recomendado em geral); não há regras complexas sobre como os parâmetros precisam ser configurados; portanto, um objeto de parâmetro é perfeitamente adequado se houver uma preocupação legítima sobre os parâmetros do método serem complexos ou alterados com frequência. E eu concordo com o @BenAaronson, embora o objetivo de usar um objeto de parâmetro não seja incluir os parâmetros desnecessários logo de cara, mas torná-los muito fáceis de serem adicionados posteriormente (mesmo se houver várias implementações de interface).
Aaronaught
7

Você não precisa agora, então não adicione. Se você precisar mais tarde, estenda a interface:

public interface IEventGetter {

    public List<FooType> getFooList(String fooName, Date start)
         throws Exception;
    ....

}

public interface IBoundedEventGetter extends IEventGetter {

    public List<FooType> getFooList(String fooName, Date start, Date end)
        throws Exception;
    ....

}
ToddR
fonte
+1 por não alterar a interface existente (e provavelmente já testada / enviada).
Sebastian Godelet 13/07/2014
4

Nenhum princípio de design é absoluto; portanto, embora eu concorde com as outras respostas, pensei em interpretar o advogado do diabo e discutir algumas condições sob as quais consideraria aceitar a solução do seu colega:

  • Se essa é uma API pública e você prevê que o recurso seja útil para desenvolvedores de terceiros, mesmo que você não o utilize internamente.
  • Se possui benefícios imediatos consideráveis, não apenas futuros. Se a data final implícita for Now(), a adição de um parâmetro remove um efeito colateral, o que traz benefícios para o armazenamento em cache e o teste de unidade. Talvez isso permita uma implementação mais simples. Ou talvez seja mais consistente com outras APIs no seu código.
  • Se sua cultura de desenvolvimento tem uma história problemática. Se processos ou territorialidade tornam muito difícil alterar algo central como uma interface, o que eu já vi acontecer antes é que as pessoas implementam soluções alternativas do lado do cliente em vez de mudar a interface, então você está tentando manter uma dúzia de datas de término ad hoc filtros em vez de um. Se esse tipo de coisa acontece muito na sua empresa, faz sentido colocar um pouco mais de esforço em provas futuras. Não me interpretem mal, é melhor mudar seus processos de desenvolvimento, mas geralmente é mais fácil falar do que fazer.

Dito isto, na minha experiência, o maior corolário de YAGNI é YDKWFYN: Você não sabe de que forma precisará (sim, acabei de criar esse acrônimo). Mesmo que a necessidade de algum parâmetro de limite possa ser relativamente previsível, ele pode assumir a forma de um limite de página, ou número de dias ou um valor booleano que especifica o uso da data de término em uma tabela de preferências do usuário ou de várias outras coisas.

Como você ainda não possui o requisito, não há como saber que tipo de parâmetro deve ser. Muitas vezes, você acaba tendo uma interface estranha que não é a mais adequada para seus requisitos ou precisando alterá-la de qualquer maneira.

Karl Bielefeldt
fonte
2

Não há informações suficientes para responder a essa pergunta. Depende do que getFooListrealmente faz e como faz.

Aqui está um exemplo óbvio de um método que deve suportar um parâmetro adicional, usado ou não.

void CapitalizeSubstring (String capitalize_me, int start_index);

A implementação de um método que opera em uma coleção em que você pode especificar o início, mas não o final da coleção, costuma ser boba.

Você realmente precisa analisar o problema em si e perguntar se o parâmetro não faz sentido no contexto de toda a interface e realmente qual a carga que o parâmetro adicional impõe.

QuestionC
fonte
1

Receio que seu colega possa ter um argumento válido. Embora sua solução não seja realmente a melhor.

Pela interface proposta, fica claro que

public List<FooType> getFooList(String fooName, Date start, Date end) throws Exception;

está retornando instâncias encontradas dentro de um intervalo de tempo. Se os clientes atualmente não usam o parâmetro final, isso não altera o fato de que esperam instâncias encontradas dentro de um intervalo de tempo. Apenas significa que atualmente todos os clientes usam intervalos abertos (do início à eternidade)

Portanto, uma interface melhor seria:

public List<FooType> getFooList(String fooName, Interval interval) throws Exception;

Se você fornecer um intervalo com um método estático de fábrica:

public static Interval startingAt(Date start) { return new Interval(start, null); }

os clientes nem sentirão a necessidade de especificar um horário de término.

Ao mesmo tempo, sua interface transmite mais corretamente o que faz, pois getFooList(String, Date)não comunica que isso lida com um intervalo.

Observe que minha sugestão segue o que o método atualmente faz, não o que deveria ou poderia fazer no futuro e, como tal, o princípio YAGNI (que é muito válido de fato) não se aplica aqui.

Bowmore
fonte
0

Adicionar um parâmetro não utilizado é confuso. As pessoas podem chamar esse método, assumindo que esse recurso funcione.

Eu não adicionaria. É trivial adicioná-lo posteriormente usando uma refatoração e consertando mecanicamente os sites de chamada. Em uma linguagem de tipo estaticamente, isso é fácil de fazer. Não é necessário adicioná-lo avidamente.

Se não é uma razão para ter esse parâmetro, você pode evitar a confusão: Adicionar uma declaração na implementação dessa interface para impor que este parâmetro ser passado como o valor padrão. Se alguém acidentalmente usar esse parâmetro, pelo menos, ele notará imediatamente durante o teste que esse recurso não foi implementado. Isso elimina o risco de colocar um bug na produção.

usr
fonte