¿Cuál es la mejor manera de llamar a un método que solo está disponible para una clase que implementa una interfaz pero no la otra?

11

Básicamente necesito ejecutar diferentes acciones dada una cierta condición. El código existente se escribe de esta manera.

Interfaz base

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Implementación de la primera clase obrera.

class DoItThisWay implements DoSomething {
  ...
}

Implementación de la segunda clase obrera.

class DoItThatWay implements DoSomething {
   ...
}

La clase 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 bien y es fácil de entender.

Ahora, debido a un nuevo requisito, necesito pasar una nueva información que solo tiene sentido DoItThisWay.

Mi pregunta es: ¿es bueno el siguiente estilo de codificación para manejar este requisito?

Use una nueva variable de clase y 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 {
     ...
   }
 } ;

}

Si lo hago de esta manera, necesito hacer el cambio correspondiente a la persona que llama:

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

¿Es un buen estilo de codificación? ¿O puedo simplemente lanzarlo?

   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
fuente
19
¿Por qué no simplemente pasar qualityal constructor DoItThisWay?
David Arno
Probablemente necesito revisar mi pregunta ... porque por razones de rendimiento, la construcción de DoItThisWayy DoItThatWayse realiza una vez en el constructor de Main. Maines una clase de larga vida y doingItse llama muchas veces una y otra vez.
Anthony Kong
Sí, actualizar su pregunta sería una buena idea en ese caso.
David Arno
¿Quiere decir que el setQualitymétodo se llamará varias veces durante la vida útil del DoItThisWayobjeto?
jpmc26
1
No hay diferencia relevante entre las dos versiones que presentó. Desde un punto de vista lógico, hacen lo mismo.
Sebastian Redl

Respuestas:

6

Supongo que qualitydebe establecerse junto con cada letDoIt()llamada en a DoItThisWay().

El problema que veo surgir aquí es el siguiente: está introduciendo el acoplamiento temporal (es decir, ¿qué sucede si olvida llamar setQuality()antes de llamar letDoIt()a un DoItThisWay?). Y las implementaciones para DoItThisWayy DoItThatWayson divergentes (uno debe haber setQuality()llamado, el otro no).

Si bien esto puede no causar problemas en este momento, puede volver a perseguirte eventualmente. Puede valer la pena echarle otro vistazo letDoIt()y considerar si la información de calidad podría ser parte de la información que infousted pasa; Pero esto depende de los detalles.

CharonX
fuente
15

¿Es un buen estilo de codificación?

Desde mi punto de vista, ninguna de sus versiones es. Primero tener que llamar setQualityantes de que letDoItse pueda llamar es un acoplamiento temporal . Estás atrapado viendo DoItThisWaycomo un derivado de DoSomething, pero no lo es (al menos no funcionalmente), es más bien algo así como un

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

Esto haría Mainmás bien algo 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 otro lado, podría pasar los parámetros a las clases directamente (suponiendo que esto sea posible) y delegar la decisión de cuál usar en una fábrica (esto haría que las instancias sean intercambiables nuevamente y ambas puedan derivarse DoSomething). Esto haría que se Mainvea así

class Main {
    private DoSomethingFactory doSomethingFactory;

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

Soy consciente de que escribiste eso

porque por razones de rendimiento, la construcción de DoItThisWay y DoItThatWay se realiza una vez en el constructor de Main

Pero podría almacenar en caché las piezas que son costosas de crear en la fábrica y pasarlas también al constructor.

Paul Kertscher
fuente
Argh, ¿me estás espiando mientras escribo las respuestas? Hacemos puntos similares (pero el tuyo es mucho más completo y elocuente);)
CharonX
1
@DocBrown Sí, es discutible, pero dado que DoItThisWayrequiere que se establezca la calidad antes de llamar, letDoItse comporta de manera diferente. Esperaría que DoSomethinga funcione de la misma manera (sin establecer la calidad antes) para todos los derivados. ¿Esto tiene sentido? Por supuesto, esto es algo borroso, ya que si llamamos setQualitydesde un método de fábrica, el cliente sería agnóstico acerca de si la calidad debe establecerse o no.
Paul Kertscher
2
@DocBrown Supongo que el contrato letDoIt()es (sin información adicional) "Puedo ejecutar correctamente (hacer lo que haga) si me lo proporciona String info". Exigir que setQuality()se llame de antemano fortalece la condición previa de la llamada letDoIt()y, por lo tanto, sería una violación de LSP.
CharonX
2
@CharonX: si, por ejemplo, hubiera un contrato que implicara que " letDoIt" no arrojará ninguna excepción, y olvidarse de llamar "setQuality" haría letDoItuna excepción, entonces estaría de acuerdo, tal implementación violaría el LSP. Pero esos me parecen suposiciones muy artificiales.
Doc Brown
1
Esta es una buena respuesta. Lo único que agregaría es algún comentario sobre cuán desastrosamente malo es el acoplamiento temporal para una base de código. Es un contrato oculto entre componentes aparentemente desacoplados. Con el acoplamiento normal, al menos es explícito y fácilmente identificable. Hacer esto es esencialmente cavar un hoyo y cubrirlo con hojas.
JimmyJames
10

Supongamos que qualityno se puede pasar al constructor, y setQualityse requiere la llamada a .

Actualmente, un fragmento de código como

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

es demasiado pequeño para invertir demasiado en ello. En mi humilde opinión, se ve un poco feo, pero no es realmente difícil de entender.

Los problemas surgen cuando tal fragmento de código crece y debido a la refactorización terminas con algo como

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

Ahora, no ves de inmediato si ese código sigue siendo correcto, y el compilador no te lo dirá. Por lo tanto, introducir una variable temporal del tipo correcto y evitar el reparto es un poco más seguro, sin ningún esfuerzo adicional real.

Sin embargo, en realidad le daría tmpun mejor nombre:

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

Tales nombres ayudan a hacer que el código incorrecto parezca incorrecto.

Doc Brown
fuente
tmp podría ser incluso mejor llamado algo así como "workerThatUsesQuality"
user949300
1
@ user949300: en mi humilde opinión, no es una buena idea: suponga que DoItThisWayobtiene parámetros más específicos; según lo que sugiere, el nombre debe cambiarse cada vez. Es mejor utilizar nombres para variables que aclaren el tipo de objeto, no los nombres de métodos disponibles.
Doc Brown
Para aclarar, sería el nombre de la variable, no el nombre de la clase. Estoy de acuerdo en que poner todas las capacidades en el nombre de la clase es una mala idea. Así que sugiero que se workerThisWayconvierta en algo así usingQuality. En ese pequeño fragmento de código es más seguro ser más específico. (Y, si hay una frase mejor dentro de su dominio que "usingQuality", úsela.
user949300
2

Una interfaz común donde la inicialización varía según los datos de tiempo de ejecución generalmente se presta al patrón de fábrica.

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

doSomething.letDoIt();

Entonces DoThingsFactory puede preocuparse por obtener y establecer la información de calidad dentro del getDoer(info)método antes de devolver el objeto concreto DoThingsWithQuality emitido a la interfaz DoSomething.

Greg Burghardt
fuente