Qual é a melhor maneira de chamar um método que está disponível apenas para uma classe que implementa uma interface, mas não para a outra?

11

Basicamente, preciso executar ações diferentes, dada uma determinada condição. O código existente é escrito desta maneira

Interface base

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Implementação da primeira classe de trabalhadores

class DoItThisWay implements DoSomething {
  ...
}

Implementação da segunda classe de trabalhadores

class DoItThatWay implements DoSomething {
   ...
}

A classe principal

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          worker = new DoItThisWay();
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Este código funciona bem e é fácil de entender.

Agora, devido a um novo requisito, preciso passar uma nova informação que só faz sentido DoItThisWay.

Minha pergunta é: o seguinte estilo de codificação é bom para lidar com esse requisito.

Use nova variável de classe e método

// Use new class variable and method

class DoItThisWay implements DoSomething {
  private int quality;
  DoSomething() {
    quality = 0;
  }

  public void setQuality(int quality) {
    this.quality = quality;
  };

 public void letDoIt(String info) {
   if (quality > 50) { // make use of the new information
     ...
   } else {
     ...
   }
 } ;

}

Se eu fizer dessa maneira, preciso fazer a alteração correspondente no chamador:

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          DoItThisWay tmp = new DoItThisWay();
          tmp.setQuality(quality)
          worker = tmp;

        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

É um bom estilo de codificação? Ou posso apenas lançá-lo

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          worker = new DoItThisWay();
          ((DoItThisWay) worker).setQuality(quality)
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }
Anthony Kong
fonte
19
Por que não apenas passar qualitypara o construtor DoItThisWay?
David Arno
Provavelmente preciso revisar minha pergunta ... porque, por razões de desempenho, a construção de DoItThisWaye DoItThatWayé feita uma vez no construtor de Main. Mainé uma classe de vida longa e doingIté chamada várias vezes.
Anthony Kong
Sim, atualizar sua pergunta seria uma boa ideia nesse caso.
David Arno
Você quer dizer que o setQualitymétodo será chamado várias vezes durante a vida útil do DoItThisWayobjeto?
Jpmc26 07/06/19
1
Não há diferença relevante entre as duas versões que você apresentou. Do ponto de vista lógico, eles fazem a mesma coisa.
Sebastian Redl

Respostas:

6

Estou assumindo que qualityprecisa ser definido ao lado de cada letDoIt()chamada em um DoItThisWay().

A questão que vejo surgir aqui é a seguinte: Você está introduzindo um acoplamento temporal (ou seja, o que acontece se você esquecer de ligar setQuality()antes de chamar letDoIt()um DoItThisWay?). E as implementações para DoItThisWaye DoItThatWaysão divergentes (é preciso ter setQuality()chamado, o outro não).

Embora isso possa não causar problemas no momento, ele pode voltar a assombrá-lo eventualmente. Talvez valha a pena dar uma olhada letDoIt()e considerar se as informações de qualidade precisam fazer parte do que infovocê passa por elas; mas isso depende dos detalhes.

CharonX
fonte
15

É um bom estilo de codificação?

Do meu ponto de vista, nenhuma das suas versões é. Primeiro, ter que ligar setQualityantes letDoItpode ser chamado é um acoplamento temporal . Você está preso vendo DoItThisWaycomo um derivado de DoSomething, mas não é (pelo menos não funcionalmente), é algo como um

interface DoSomethingWithQuality {
   void letDoIt(String info, int quality);
}

Isso tornaria Mainalgo como

class Main {
    // omitting the creation
    private DoSomething doSomething;
    private DoSomethingWithQuality doSomethingWithQuality;

    public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
            int quality = obtainQualityInfo();
            doSomethingWithQuality.letDoIt(info, quality);
        } else {
            doSomething.letDoIt(info);
        }
    }
}

Por outro lado, você poderia passar os parâmetros diretamente para as classes (supondo que isso seja possível) e delegar a decisão sobre qual delas usar em uma fábrica (isso tornaria as instâncias intercambiáveis ​​novamente e ambas poderão ser derivadas DoSomething). Isso ficaria Mainassim

class Main {
    private DoSomethingFactory doSomethingFactory;

     public doingIt(String info) {
         int quality = obtainQualityInfo();
         DoSomething doSomethingWorker = doSomethingFactory.Create(info, quality);
         doSomethingWorker.letDoIt();
     }
}

Eu sei que você escreveu isso

porque, por motivos de desempenho, a construção de DoItThisWay e DoItThatWay é feita uma vez no construtor de Main

Mas você pode armazenar em cache as peças que são caras para criar na fábrica e passá-las ao construtor também.

Paul Kertscher
fonte
Argh, você está me espionando enquanto eu digito respostas? Fazemos esses pontos semelhantes (mas o seu é muito mais abrangente e eloquente);)
CharonX
1
@DocBrown Sim, é discutível, mas já que DoItThisWayrequer que a qualidade seja definida antes de chamá- letDoItla se comporta de maneira diferente. Eu esperaria DoSomethingque funcionasse da mesma maneira (sem definir a qualidade antes) para todos os derivados. Isso faz sentido? É claro que isso é meio embaçado, já que se chamássemos o setQualitymétodo de uma fábrica, o cliente ficaria agnóstico sobre se a qualidade deve ser definida ou não.
Paul Kertscher 7/07/19
2
@DocBrown Eu diria que o contrato letDoIt()é (sem informações adicionais) "Posso executar corretamente (fazer o que fizer) se você me fornecer String info". Exigir que setQuality()seja chamado antecipadamente reforça a pré-condição de chamada letDoIt()e, portanto, seria uma violação do LSP.
CharonX 07/06/19
2
@CharonX: se, por exemplo, houvesse um contrato implicando que " letDoIt" não lançasse nenhuma exceção, e esquecendo de chamar "setQuality" criaria letDoItuma exceção, então eu concordaria que tal implementação violaria o LSP. Mas essas parecem suposições muito artificiais para mim.
Doc Brown
1
Esta é uma boa resposta. A única coisa que gostaria de acrescentar é um comentário sobre o quão desastrosamente o acoplamento temporal é para uma base de código. É um contrato oculto entre componentes aparentemente dissociados. Com o acoplamento normal, pelo menos é explícito e facilmente identificável. Fazer isso é essencialmente cavar um poço e cobri-lo com folhas.
JimmyJames 13/06/19
10

Vamos supor qualityque não possa ser passado para o construtor, e a chamada para setQualityé necessária.

Atualmente, um trecho de código como

    int quality = obtainQualityInfo();
    worker = new DoItThisWay();
    ((DoItThisWay) worker).setQuality(quality);

é muito pequeno para investir pensamentos demais nele. IMHO parece um pouco feio, mas não é realmente difícil de entender.

Os problemas surgem quando esse snippet de código cresce e, devido à refatoração, você acaba com algo como

    int quality = obtainQualityInfo();
    worker = CreateAWorkerForThisInfo();
    ((DoItThisWay) worker).setQuality(quality);

Agora, você não vê imediatamente se esse código ainda está correto e o compilador não informará. Portanto, introduzir uma variável temporária do tipo correto e evitar o elenco é um pouco mais seguro para o tipo, sem nenhum esforço extra real.

No entanto, eu realmente daria tmpum nome melhor:

      int quality = obtainQualityInfo();
      DoItThisWay workerThisWay = new DoItThisWay();
      workerThisWay.setQuality(quality)
      worker = workerThisWay;

Essa nomeação ajuda a fazer com que o código errado pareça errado.

Doc Brown
fonte
tmp pode ser ainda melhor chamado algo como "workerThatUsesQuality"
user949300
1
@ user949300: IMHO que não é uma boa ideia: suponha que DoItThisWayobtém parâmetros mais específicos - pelo que você sugere, o nome precisa ser alterado sempre. É melhor usar nomes para variáveis ​​que tornam o tipo de objeto claro, não quais nomes de métodos estão disponíveis.
Doc Brown
Para esclarecer, seria o nome da variável, não o nome da classe. Concordo que colocar todas as capacidades no nome da classe é uma má idéia. Então, eu estou sugerindo que workerThisWayse torne algo parecido usingQuality. Nesse pequeno trecho de código, é mais seguro ser mais específico. (E, se houver uma frase melhor em seu domínio, do que "usingQuality", use-a. #
User949300 7/19/19
2

Uma interface comum em que a inicialização varia com base nos dados de tempo de execução geralmente se presta ao padrão de fábrica.

DoThingsFactory factory = new DoThingsFactory(thingThatProvidesQuality);
DoSomething doSomething = factory.getDoer(info);

doSomething.letDoIt();

Então DoThingsFactory pode se preocupar em obter e definir as informações de qualidade dentro do getDoer(info)método antes de retornar o objeto DoThingsWithQuality concreto convertido para a interface DoSomething.

Greg Burghardt
fonte