Qual è il modo migliore per chiamare un metodo disponibile solo per una classe che implementa un'interfaccia ma non l'altra?


11

Fondamentalmente ho bisogno di eseguire diverse azioni dato una certa condizione. Il codice esistente è scritto in questo modo

Interfaccia di base

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Implementazione della prima classe operaia

class DoItThisWay implements DoSomething {
  ...
}

Implementazione della seconda classe operaia

class DoItThatWay implements DoSomething {
   ...
}

La classe principale

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

Questo codice funziona bene ed è facile da capire.

Ora, a causa di un nuovo requisito, ho bisogno di passare una nuova informazione che ha senso solo DoItThisWay.

La mia domanda è: il seguente stile di codifica è buono per gestire questo requisito.

Usa nuova variabile di classe e metodo

// 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 lo faccio in questo modo, devo apportare la modifica corrispondente al chiamante:

   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)
     }

È un buon stile di programmazione? O posso semplicemente lanciarlo

   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)
     }

19
Perché non passare semplicemente qualitynel costruttore per DoItThisWay?
David Arno,

Probabilmente ho bisogno di rivedere la mia domanda ... perché per motivi di prestazioni la costruzione di DoItThisWaye DoItThatWayviene eseguita una volta nel costruttore di Main. Mainè una classe di lunga vita e doingItviene chiamata più volte.
Anthony Kong,

Sì, aggiornare la tua domanda sarebbe una buona idea in quel caso.
David Arno,

Intendi dire che il setQualitymetodo verrà chiamato più volte durante la vita DoItThisWaydell'oggetto?
jpmc26,

1
Non vi è alcuna differenza rilevante tra le due versioni presentate. Da un punto di vista logico, fanno la stessa cosa.
Sebastian Redl,

Risposte:


6

Presumo che qualitydebba essere impostato insieme a ciascuna letDoIt()chiamata su a DoItThisWay().

Il problema che vedo sorgere qui è questo: stai introducendo un accoppiamento temporale (cioè cosa succede se ti dimentichi di chiamare setQuality()prima di chiamare letDoIt()un DoItThisWay?). E le implementazioni per DoItThisWaye DoItThatWaystanno divergendo (una deve aver setQuality()chiamato, l'altra no).

Anche se questo potrebbe non causare problemi in questo momento, potrebbe tornare a perseguitarti alla fine. Potrebbe valere la pena dare un'altra occhiata letDoIt()e valutare se le informazioni sulla qualità potrebbero aver bisogno di far parte di ciò che le infoattraversi; ma questo dipende dai dettagli.


15

È un buon stile di programmazione?

Dal mio punto di vista nessuna delle tue versioni lo è. Il primo a dover chiamare setQualityprima di letDoItpoter essere chiamato è un accoppiamento temporale . Sei bloccato a visualizzare DoItThisWaycome un derivato di DoSomething, ma non lo è (almeno non funzionalmente), è piuttosto qualcosa di simile a

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

Questo renderebbe Mainpiuttosto qualcosa di simile

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);
        }
    }
}

D'altra parte, è possibile passare direttamente i parametri alle classi (supponendo che ciò sia possibile) e delegare la decisione quale utilizzare per una fabbrica (ciò renderebbe nuovamente intercambiabili le istanze ed entrambe le derivazioni DoSomething). Questo Mainapparirebbe così

class Main {
    private DoSomethingFactory doSomethingFactory;

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

Sono consapevole che l'hai scritto tu

perché per motivi prestazionali la costruzione di DoItThisWay e DoItThatWay viene eseguita una volta nel costruttore di Main

Ma potresti mettere in cache le parti che sono costose da creare in fabbrica e passarle anche al costruttore.


Argh, mi stai spiando mentre scrivo le risposte? Facciamo simili punti simili (ma il tuo è molto più completo ed eloquente);)
CharonX

1
@DocBrown Sì, è discutibile, ma poiché DoItThisWayrichiede che la qualità sia impostata prima di chiamare, letDoItsi comporta diversamente. Mi aspetto che DoSomethinga funzioni allo stesso modo (non impostando la qualità prima) per tutti i derivati. ha senso? Naturalmente questo è un po 'sfocato, poiché se chiamassimo il setQualitymetodo da una fabbrica il cliente sarebbe agnostico sul fatto che la qualità debba essere impostata o meno.
Paul Kertscher,

2
@DocBrown Suppongo che il contratto letDoIt()sia (privo di ulteriori informazioni) "Posso eseguire correttamente (fare tutto ciò che faccio) se mi fornisci String info". Il setQuality()requisito che viene chiamato in anticipo rafforza il presupposto della chiamata letDoIt()e quindi sarebbe una violazione di LSP.
CharonX,

2
@CharonX: se, ad esempio, ci fosse un contratto che implica che " letDoIt" non genererà alcuna eccezione, e dimenticando di chiamare "setQuality" farebbe letDoItquindi un'eccezione, quindi sarei d'accordo, tale implementazione violerebbe l'LSP. Ma quelli mi sembrano ipotesi molto artificiali.
Doc Brown,

1
Questa è una buona risposta L'unica cosa che aggiungerei è un commento su quanto disastrosamente cattivo accoppiamento temporale sia per una base di codice. È un contratto nascosto tra componenti apparentemente disaccoppiati. Con l'accoppiamento normale, almeno è esplicito e facilmente identificabile. Fare questo è essenzialmente scavare una fossa e coprirla con foglie.
JimmyJames,

10

Supponiamo che qualitynon sia possibile passare al costruttore e che setQualitysia richiesta la chiamata a .

Attualmente, un frammento di codice piace

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

è troppo piccolo per investire troppi pensieri in esso. IMHO sembra un po 'brutto, ma non è davvero difficile da capire.

I problemi sorgono quando un tale frammento di codice cresce e, a causa del refactoring, si finisce con qualcosa di simile

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

Ora, non vedi immediatamente se quel codice è ancora corretto e il compilatore non te lo dirà. Quindi introdurre una variabile temporanea del tipo corretto ed evitare il cast è un po 'più sicuro per il tipo, senza alcuno sforzo aggiuntivo.

Tuttavia, in realtà darei tmpun nome migliore:

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

Tale denominazione aiuta a far sembrare sbagliato il codice sbagliato.


tmp potrebbe essere anche meglio chiamato qualcosa del tipo "workerThatUsesQuality"
user949300

1
@ user949300: IMHO non è una buona idea: supponiamo che DoItThisWayottenga parametri più specifici - da quello che stai suggerendo, il nome deve essere cambiato ogni volta. È meglio usare i nomi delle variabili vor che chiariscono il tipo di oggetto, non i nomi dei metodi disponibili.
Doc Brown,

Per chiarire, sarebbe il nome della variabile, non il nome della classe. Sono d'accordo che mettere tutte le funzionalità nel nome della classe sia una cattiva idea. Quindi sto suggerendo che workerThisWaydiventi qualcosa di simile usingQuality. In quel piccolo frammento di codice è più sicuro essere più specifici. (E, se c'è una frase migliore nel loro dominio rispetto a "usingQuality", usala.
user949300

2

Un'interfaccia comune in cui l'inizializzazione varia in base ai dati di runtime di solito si presta al modello di fabbrica.

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

doSomething.letDoIt();

Quindi DoThingsFactory può preoccuparsi di ottenere e impostare le informazioni sulla qualità all'interno del getDoer(info)metodo prima di restituire l'oggetto DoThingsWithQuality concreto all'interfaccia DoSomething.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.