Por que a maioria dos arquitetos de sistemas insiste em programar primeiro para uma interface?

StackOverflow https://stackoverflow.com/questions/48605

Pergunta

Quase todos os livros sobre Java que li falam sobre o uso da interface como uma forma de compartilhar estado e comportamento entre objetos que, quando "construídos" pela primeira vez, não pareciam compartilhar um relacionamento.

No entanto, sempre que vejo arquitetos projetando um aplicativo, a primeira coisa que fazem é começar a programar uma interface.Por quê?Como você conhece todos os relacionamentos entre objetos que ocorrerão nessa interface?Se você já conhece esses relacionamentos, por que não estender uma classe abstrata?

Foi útil?

Solução

Programar para uma interface significa respeitar o "contrato" criado ao usar essa interface.E então se o seu IPoweredByMotor interface tem um start() método, futuras classes que implementam a interface, sejam elas MotorizedWheelChair, Automobile, ou SmoothieMaker, ao implementar os métodos dessa interface, adicione flexibilidade ao seu sistema, porque um pedaço de código pode iniciar o motor de muitos tipos diferentes de coisas, porque tudo o que um pedaço de código precisa saber é que eles respondem a start().Não importa como eles começam, apenas que eles deve começar.

Outras dicas

Ótima pergunta.vou te indicar Josh Bloch em Java Eficaz, que escreve (item 16) por que preferir o uso de interfaces em vez de classes abstratas.Aliás, se você ainda não tem esse livro, eu recomendo fortemente!Aqui está um resumo do que ele diz:

  1. As classes existentes podem ser facilmente adaptadas para implementar uma nova interface. Tudo que você precisa fazer é implementar a interface e adicionar os métodos necessários.As classes existentes não podem ser facilmente adaptadas para estender uma nova classe abstrata.
  2. As interfaces são ideais para definir mix-ins. Uma interface mix-in permite que as classes declarem comportamentos adicionais e opcionais (por exemplo, Comparable).Ele permite que a funcionalidade opcional seja combinada com a funcionalidade primária.Classes abstratas não podem definir mix-ins – uma classe não pode estender mais de um pai.
  3. As interfaces permitem estruturas não hierárquicas. Se você tiver uma classe que possua a funcionalidade de muitas interfaces, ela poderá implementar todas elas.Sem interfaces, você teria que criar uma hierarquia de classes inchada com uma classe para cada combinação de atributos, resultando em uma explosão combinatória.
  4. As interfaces permitem melhorias seguras de funcionalidade. Você pode criar classes wrapper usando o padrão Decorator, um design robusto e flexível.Uma classe wrapper implementa e contém a mesma interface, encaminhando algumas funcionalidades para métodos existentes, enquanto adiciona comportamento especializado a outros métodos.Você não pode fazer isso com métodos abstratos - você deve usar herança, que é mais frágil.

E quanto à vantagem de classes abstratas fornecerem implementação básica?Você pode fornecer uma classe abstrata de implementação esquelética com cada interface.Isso combina as virtudes das interfaces e das classes abstratas.As implementações esqueléticas fornecem assistência na implementação sem impor as restrições severas que as classes abstratas impõem quando servem como definições de tipo.Por exemplo, o Estrutura de coleções define o tipo usando interfaces e fornece uma implementação básica para cada uma.

A programação para interfaces oferece vários benefícios:

  1. Obrigatório para padrões do tipo GoF, como o padrão de visitante

  2. Permite implementações alternativas.Por exemplo, podem existir múltiplas implementações de objetos de acesso a dados para uma única interface que abstrai o mecanismo de banco de dados em uso (AccountDaoMySQL e AccountDaoOracle podem implementar AccountDao)

  3. Uma classe pode implementar múltiplas interfaces.Java não permite herança múltipla de classes concretas.

  4. Detalhes de implementação de resumos.As interfaces podem incluir apenas métodos de API públicos, ocultando detalhes de implementação.Os benefícios incluem uma API pública bem documentada e contratos bem documentados.

  5. Muito usado por estruturas modernas de injeção de dependência, como http://www.springframework.org/.

  6. Em Java, interfaces podem ser usadas para criar proxies dinâmicos - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html.Isso pode ser usado de forma muito eficaz com estruturas como Spring para realizar Programação Orientada a Aspectos.Aspectos podem adicionar funcionalidades muito úteis às classes sem adicionar código Java diretamente a essas classes.Exemplos desta funcionalidade incluem registro, auditoria, monitoramento de desempenho, demarcação de transações, etc. http://static.springframework.org/spring/docs/2.5.x/reference/aop.html.

  7. Implementações simuladas, testes unitários - Quando classes dependentes são implementações de interfaces, podem ser escritas classes simuladas que também implementam essas interfaces.As classes simuladas podem ser usadas para facilitar o teste de unidade.

Acho que uma das razões pelas quais as classes abstratas foram amplamente abandonadas pelos desenvolvedores pode ser um mal-entendido.

Quando o Bando dos Quatro escreveu:

Programa para uma interface, não para uma implementação.

não existia interface java ou C#.Eles estavam falando sobre o conceito de interface orientada a objetos, que toda classe possui.Erich Gamma menciona isso em esta entrevista.

Acho que seguir todas as regras e princípios mecanicamente, sem pensar, dificulta a leitura, navegação, compreensão e manutenção da base de código.Lembrar:A coisa mais simples que poderia funcionar.

Por quê?

Porque é isso que todos os livros dizem.Assim como os padrões GoF, muitas pessoas o consideram universalmente bom e nunca pensam se é realmente o design certo ou não.

Como você conhece todos os relacionamentos entre objetos que ocorrerão nessa interface?

Você não sabe, e isso é um problema.

Se você já conhece esses relacionamentos, por que não apenas estender uma classe abstrata?

Razões para não estender uma classe abstrata:

  1. Você tem implementações radicalmente diferentes e criar uma classe base decente é muito difícil.
  2. Você precisa gravar sua única classe base para outra coisa.

Se nenhum dos dois se aplicar, vá em frente e use uma classe abstrata.Isso economizará muito tempo.

Perguntas que você não fez:

Quais são as desvantagens de usar uma interface?

Você não pode alterá-los.Ao contrário de uma classe abstrata, uma interface é imutável.Depois de ter um em uso, estendê-lo quebrará o código, ponto final.

Eu realmente preciso de algum deles?

Na maior parte do tempo, não.Pense bem antes de construir qualquer hierarquia de objetos.Um grande problema em linguagens como Java é que torna muito fácil criar hierarquias de objetos enormes e complicadas.

Considere o exemplo clássico que LameDuck herda de Duck.Parece fácil, não é?

Bem, isso é até que você precise indicar que o pato foi ferido e agora está manco.Ou indique que o pato manco foi curado e pode voltar a andar.Java não permite alterar o tipo de objeto, portanto, usar subtipos para indicar claudicação não funciona.

Programação para uma interface significa respeitar o "contrato" criado usando essa interface

Esta é a coisa mais incompreendida sobre interfaces.

Não há como fazer cumprir tal contrato com interfaces.As interfaces, por definição, não podem especificar nenhum comportamento.As aulas são onde o comportamento acontece.

Esta crença equivocada é tão difundida que é considerada sabedoria convencional por muitas pessoas.No entanto, está errado.

Portanto, esta declaração no OP

Quase todos os livros de java que li fala sobre o uso da interface como uma maneira de compartilhar estado e comportamento entre objetos

simplesmente não é possível.As interfaces não têm estado nem comportamento.Eles podem definir propriedades que as classes de implementação devem fornecer, mas isso é o mais próximo possível.Você não pode compartilhar comportamento usando interfaces.

Você pode presumir que as pessoas implementarão uma interface para fornecer o tipo de comportamento implícito no nome de seus métodos, mas isso não é a mesma coisa.E não impõe nenhuma restrição sobre quando tais métodos são chamados (por exemplo, que Start deve ser chamado antes de Stop).

Esta afirmação

Obrigatório para padrões do tipo GoF, como o padrão de visitante

também está incorreto.O livro GoF usa exatamente zero interfaces, pois elas não eram uma característica das linguagens usadas na época.Nenhum dos padrões requer interfaces, embora alguns possam utilizá-las.IMO, o padrão Observer é aquele em que as interfaces podem desempenhar um papel mais elegante (embora o padrão seja normalmente implementado usando eventos hoje em dia).No padrão Visitor, quase sempre é necessária uma classe base Visitor que implementa o comportamento padrão para cada tipo de nó visitado, o IME.

Pessoalmente, acho que a resposta à pergunta é tripla:

  1. As interfaces são vistas por muitos como uma solução mágica (essas pessoas geralmente trabalham sob o equívoco do "contrato" ou pensam que as interfaces dissociam magicamente seu código)

  2. O pessoal de Java está muito focado no uso de frameworks, muitos dos quais (com razão) exigem classes para implementar suas interfaces

  3. As interfaces eram a melhor maneira de fazer algumas coisas antes da introdução de genéricos e anotações (atributos em C#).

As interfaces são um recurso de linguagem muito útil, mas são muito abusadas.Os sintomas incluem:

  1. Uma interface é implementada apenas por uma classe

  2. Uma classe implementa múltiplas interfaces.Muitas vezes apontado como uma vantagem das interfaces, geralmente significa que a classe em questão está violando o princípio da separação de interesses.

  3. Existe uma hierarquia de herança de interfaces (frequentemente espelhada por uma hierarquia de classes).Esta é a situação que você está tentando evitar usando interfaces em primeiro lugar.Muita herança é ruim, tanto para classes quanto para interfaces.

Todas essas coisas são cheiros de código, IMO.

É uma forma de promover a liberdade acoplamento.

Com baixo acoplamento, uma alteração em um módulo não exigirá uma alteração na implementação de outro módulo.

Um bom uso deste conceito é Padrão abstrato de fábrica.No exemplo da Wikipedia, a interface GUIFactory produz a interface Button.A fábrica concreta pode ser WinFactory (produzindo WinButton) ou OSXFactory (produzindo OSXButton).Imagine se você estivesse escrevendo uma aplicação GUI e tivesse que dar uma olhada em todas as instâncias de OldButton classe e alterá-los para WinButton.Então, no próximo ano, você precisará adicionar OSXButton versão.

Na minha opinião, você vê isso com frequência porque é uma prática muito boa, frequentemente aplicada em situações erradas.

Existem muitas vantagens nas interfaces em relação às classes abstratas:

  • Você pode alternar implementações sem reconstruir código que depende da interface.Isso é útil para:classes proxy, injeção de dependência, AOP, etc.
  • Você pode separar a API da implementação no seu código.Isso pode ser bom porque fica óbvio quando você altera o código que afetará outros módulos.
  • Ele permite que os desenvolvedores escrevam código que depende do seu código para simular facilmente sua API para fins de teste.

Você obtém a maior vantagem das interfaces ao lidar com módulos de código.No entanto, não existe uma regra fácil para determinar onde devem estar os limites dos módulos.Portanto, é fácil usar essa prática recomendada em excesso, especialmente ao projetar algum software pela primeira vez.

Eu diria (com @eed3s9n) que é para promover um acoplamento fraco.Além disso, sem interfaces, o teste de unidade se torna muito mais difícil, pois você não pode simular seus objetos.

Por que estender é mau.Este artigo é praticamente uma resposta direta à pergunta feita.Não consigo pensar em quase nenhum caso em que você realmente precisar uma classe abstrata e muitas situações em que é uma má ideia.Isso não significa que implementações que utilizam classes abstratas sejam ruins, mas você terá que tomar cuidado para não tornar o contrato de interface dependente de artefatos de alguma implementação específica (caso em questão:a classe Stack em Java).

Mais uma coisa:não é necessário, nem é uma boa prática, ter interfaces em todos os lugares.Normalmente, você deve identificar quando precisa de uma interface e quando não.Em um mundo ideal, o segundo caso deveria ser implementado como aula final na maioria das vezes.

Existem algumas respostas excelentes aqui, mas se você estiver procurando por um motivo concreto, não procure além do Teste de Unidade.

Considere que você deseja testar um método na lógica de negócios que recupera a taxa de imposto atual para a região onde ocorre uma transação.Para fazer isso, a classe de lógica de negócios precisa se comunicar com o banco de dados por meio de um Repositório:

interface IRepository<T> { T Get(string key); }

class TaxRateRepository : IRepository<TaxRate> {
    protected internal TaxRateRepository() {}
    public TaxRate Get(string key) {
    // retrieve an TaxRate (obj) from database
    return obj; }
}

Ao longo do código, use o tipo IRepository em vez de TaxRateRepository.

O repositório possui um construtor não público para incentivar os usuários (desenvolvedores) a usar a fábrica para instanciar o repositório:

public static class RepositoryFactory {

    public RepositoryFactory() {
        TaxRateRepository = new TaxRateRepository(); }

    public static IRepository TaxRateRepository { get; protected set; }
    public static void SetTaxRateRepository(IRepository rep) {
        TaxRateRepository = rep; }
}

A fábrica é o único local onde a classe TaxRateRepository é referenciada diretamente.

Então você precisa de algumas classes de apoio para este exemplo:

class TaxRate {
    public string Region { get; protected set; }
    decimal Rate { get; protected set; }
}

static class Business {
    static decimal GetRate(string region) { 
        var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
        return taxRate.Rate; }
}

E há também outra implementação do IRepository - o mock up:

class MockTaxRateRepository : IRepository<TaxRate> {
    public TaxRate ReturnValue { get; set; }
    public bool GetWasCalled { get; protected set; }
    public string KeyParamValue { get; protected set; }
    public TaxRate Get(string key) {
        GetWasCalled = true;
        KeyParamValue = key;
        return ReturnValue; }
}

Como o código ativo (Business Class) usa um Factory para obter o Repositório, no teste de unidade você conecta o MockRepository para o TaxRateRepository.Depois que a substituição for feita, você poderá codificar o valor de retorno e tornar o banco de dados desnecessário.

class MyUnitTestFixture { 
    var rep = new MockTaxRateRepository();

    [FixtureSetup]
    void ConfigureFixture() {
        RepositoryFactory.SetTaxRateRepository(rep); }

    [Test]
    void Test() {
        var region = "NY.NY.Manhattan";
        var rate = 8.5m;
        rep.ReturnValue = new TaxRate { Rate = rate };

        var r = Business.GetRate(region);
        Assert.IsNotNull(r);
        Assert.IsTrue(rep.GetWasCalled);
        Assert.AreEqual(region, rep.KeyParamValue);
        Assert.AreEqual(r.Rate, rate); }
}

Lembre-se, você deseja testar apenas o método de lógica de negócios, não o repositório, banco de dados, cadeia de conexão, etc.Existem testes diferentes para cada um deles.Fazendo dessa forma, você pode isolar completamente o código que está testando.

Um benefício colateral é que você também pode executar o teste de unidade sem uma conexão com o banco de dados, o que o torna mais rápido e portátil (pense em equipes com vários desenvolvedores em locais remotos).

Outro benefício colateral é que você pode usar o processo Test-Driven Development (TDD) para a fase de implementação do desenvolvimento.Eu não uso estritamente TDD, mas uma mistura de TDD e codificação da velha escola.

Em certo sentido, acho que sua pergunta se resume a simplesmente: "Por que usar interfaces e não classes abstratas?" Tecnicamente, você pode obter um acoplamento solto com ambos - a implementação subjacente ainda não está exposta ao código de chamada e você pode usar o padrão abstrato de fábrica para retornar uma implementação subjacente (implementação da interface vs.extensão de classe abstrata) para aumentar a flexibilidade do seu design.Na verdade, você poderia argumentar que as classes abstratas oferecem um pouco mais, pois permitem que você exija implementações para satisfazer seu código ("você DEVE implementar start()") e forneça implementações padrão ("Eu tenho um paint() padrão que você pode substituir se você quiser") - com interfaces, implementações devem ser fornecidas, o que com o tempo pode levar a problemas de herança frágeis através de mudanças de interface.

Fundamentalmente, porém, eu uso interfaces principalmente devido à restrição de herança única do Java.Se minha implementação DEVE herdar de uma classe abstrata para ser usada chamando código, isso significa que perco a flexibilidade de herdar de outra coisa, mesmo que isso faça mais sentido (por exemplo,para reutilização de código ou hierarquia de objetos).

Um dos motivos é que as interfaces permitem crescimento e extensibilidade.Digamos, por exemplo, que você tenha um método que recebe um objeto como parâmetro,

bebida pública vazia (café um rink) {

}

Agora digamos que você queira usar exatamente o mesmo método, mas passar um objeto hotTea.Bem, você não pode.Você acabou de codificar o método acima para usar apenas objetos de café.Talvez isso seja bom, talvez isso seja ruim.A desvantagem do item acima é que ele bloqueia você estritamente com um tipo de objeto quando você deseja passar todos os tipos de objetos relacionados.

Usando uma interface, digamos IHotDrink,

interface IHotDrink { }

e reescrever seu método acima para usar a interface em vez do objeto,

bebida pública vazia (ihotdrink Somedrink) {

}

Agora você pode passar todos os objetos que implementam a interface IHotDrink.Claro, você pode escrever exatamente o mesmo método que faz exatamente a mesma coisa com um parâmetro de objeto diferente, mas por quê?De repente, você está mantendo um código inchado.

É tudo uma questão de projetar antes de codificar.

Se você não conhece todos os relacionamentos entre dois objetos depois de especificar a interface, então você fez um péssimo trabalho ao definir a interface - o que é relativamente fácil de corrigir.

Se você mergulhou direto na codificação e percebeu no meio do caminho que está faltando algo, é muito mais difícil de consertar.

Você pode ver isso de uma perspectiva perl/python/ruby:

  • quando você passa um objeto como parâmetro para um método, você não passa seu tipo, você apenas sabe que ele deve responder a alguns métodos

Acho que considerar as interfaces Java como uma analogia explicaria melhor isso.Você realmente não passa um type , apenas passa algo que responde a um método (um trait , se preferir).

Acho que o principal motivo para usar interfaces em Java é a limitação à herança única.Em muitos casos, isso leva a complicações desnecessárias e duplicação de código.Dê uma olhada nas características do Scala: http://www.scala-lang.org/node/126 As características são um tipo especial de classes abstratas, mas uma classe pode estender muitas delas.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top