¿Hay una manera elegante de hacer que cada método en una clase comience con un cierto bloque de código?


143

Tengo una clase donde cada método comienza de la misma manera:

class Foo {
  public void bar() {
    if (!fooIsEnabled) return;
    //...
  }
  public void baz() {
    if (!fooIsEnabled) return;
    //...
  }
  public void bat() {
    if (!fooIsEnabled) return;
    //...
  }
}

¿Hay una buena manera de requerir (y con suerte no escribir cada vez) la fooIsEnabledparte para cada método público en la clase?


45
Examine la Programación Orientada a Aspectos (específicamente antes del consejo ).
Sotirios Delimanolis

15
¿Para cuántos métodos tiene que usar este repetitivo? Antes de seguir el camino de la introducción de AOP, es posible que desee considerar solo tener este pequeño código repetido. A veces, un poco de copiar y pegar es la solución más simple.
bhspencer

51
Sospecho que su futuro mantenedor estaría más feliz con la placa de caldera adicional que tener que aprender un marco de AOP.
bhspencer

77
Si cada método de la clase tiene que hacer lo mismo en sus primeras líneas de código, entonces tenemos un mal diseño.
Tulains Córdova

77
@ user1598390: La pregunta no está fuera de tema aquí, y no hay nada sobre el alcance de los Programadores que haga que la pregunta sea especialmente significativa allí.
Robert Harvey

Respuestas:


90

No sé acerca de elegante, pero aquí hay una implementación funcional que usa el Java incorporado java.lang.reflect.Proxyque exige que todas las invocaciones de métodos Foocomiencen por verificar el enabledestado.

main método:

public static void main(String[] args) {
    Foo foo = Foo.newFoo();
    foo.setEnabled(false);
    foo.bar(); // won't print anything.
    foo.setEnabled(true);
    foo.bar(); // prints "Executing method bar"
}

Foo interfaz:

public interface Foo {
    boolean getEnabled();
    void setEnabled(boolean enable);

    void bar();
    void baz();
    void bat();

    // Needs Java 8 to have this convenience method here.
    static Foo newFoo() {
        FooFactory fooFactory = new FooFactory();
        return fooFactory.makeFoo();
    }
}

FooFactory clase:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class FooFactory {

    public Foo makeFoo() {
        return (Foo) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{Foo.class},
                new FooInvocationHandler(new FooImpl()));
    }

    private static class FooImpl implements Foo {
        private boolean enabled = false;

        @Override
        public boolean getEnabled() {
            return this.enabled;
        }

        @Override
        public void setEnabled(boolean enable) {
            this.enabled = enable;
        }

        @Override
        public void bar() {
            System.out.println("Executing method bar");
        }

        @Override
        public void baz() {
            System.out.println("Executing method baz");
        }

        @Override
        public void bat() {
            System.out.println("Executing method bat");
        }

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

        public FooInvocationHandler(FooImpl fooImpl) {
            this.fooImpl = fooImpl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Foo.class &&
                !method.getName().equals("getEnabled") &&
                !method.getName().equals("setEnabled")) {

                if (!this.fooImpl.getEnabled()) {
                    return null;
                }
            }

            return method.invoke(this.fooImpl, args);
        }
    }
}

Como otros han señalado, parece excesivo lo que necesita si solo tiene que preocuparse por un puñado de métodos.

Dicho esto, ciertamente hay beneficios:

  • Se logra una cierta separación de preocupaciones, porque Foolas implementaciones de métodos no tienen que preocuparse por la preocupación enabledtransversal de la verificación. En cambio, el código del método solo necesita preocuparse sobre cuál es el propósito principal del método, nada más.
  • No hay forma de que un desarrollador inocente agregue un nuevo método a la Fooclase y, por error, "olvide" agregar el enabledcheque. El enabledcomportamiento de verificación se hereda automáticamente por cualquier método recién agregado.
  • Si necesita agregar otra preocupación transversal, o si necesita mejorar la enabledverificación, es muy fácil hacerlo de manera segura y en un solo lugar.
  • Es agradable que pueda obtener este comportamiento similar a AOP con la funcionalidad Java incorporada. No está obligado a tener que integrar algún otro marco como Spring, aunque definitivamente también pueden ser buenas opciones.

Para ser justos, algunos de los inconvenientes son:

  • Parte del código de implementación que maneja las invocaciones de proxy es feo. Algunos también dirían que tener clases internas para evitar la creación de instancias de la FooImplclase es feo.
  • Si desea agregar un nuevo método Foo, debe realizar un cambio en 2 puntos: la clase de implementación y la interfaz. No es un gran problema, pero sigue siendo un poco más de trabajo.
  • Las invocaciones de proxy no son gratuitas. Hay una cierta sobrecarga de rendimiento. Sin embargo, para uso general, no se notará. Ver aquí para más información.

EDITAR:

El comentario de Fabian Streitel me hizo pensar en 2 molestias con mi solución anterior que, admito, no estoy contento conmigo mismo:

  1. El controlador de invocación utiliza cadenas mágicas para omitir la "comprobación habilitada" en los métodos "getEnabled" y "setEnabled". Esto puede romperse fácilmente si se refactorizan los nombres de los métodos.
  2. Si hubo un caso en el que se deben agregar nuevos métodos que no hereden el comportamiento de "verificación habilitada", entonces puede ser bastante fácil para el desarrollador equivocarse, y al menos, significaría agregar más magia instrumentos de cuerda.

Para resolver el punto n. ° 1 y, al menos, aliviar el problema con el punto n. ° 2, crearía una anotación BypassCheck(o algo similar) que podría utilizar para marcar los métodos en la Foointerfaz para los que no quiero realizar el " verificación habilitada ". De esta manera, no necesito cadenas mágicas en absoluto, y se vuelve mucho más fácil para un desarrollador agregar correctamente un nuevo método en este caso especial.

Usando la solución de anotación, el código se vería así:

main método:

public static void main(String[] args) {
    Foo foo = Foo.newFoo();
    foo.setEnabled(false);
    foo.bar(); // won't print anything.
    foo.setEnabled(true);
    foo.bar(); // prints "Executing method bar"
}

BypassCheck anotación:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BypassCheck {
}

Foo interfaz:

public interface Foo {
    @BypassCheck boolean getEnabled();
    @BypassCheck void setEnabled(boolean enable);

    void bar();
    void baz();
    void bat();

    // Needs Java 8 to have this convenience method here.
    static Foo newFoo() {
        FooFactory fooFactory = new FooFactory();
        return fooFactory.makeFoo();
    }
}

FooFactory clase:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class FooFactory {

    public Foo makeFoo() {
        return (Foo) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                new Class[]{Foo.class},
                new FooInvocationHandler(new FooImpl()));
    }

    private static class FooImpl implements Foo {

        private boolean enabled = false;

        @Override
        public boolean getEnabled() {
            return this.enabled;
        }

        @Override
        public void setEnabled(boolean enable) {
            this.enabled = enable;
        }

        @Override
        public void bar() {
            System.out.println("Executing method bar");
        }

        @Override
        public void baz() {
            System.out.println("Executing method baz");
        }

        @Override
        public void bat() {
            System.out.println("Executing method bat");
        }

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

        public FooInvocationHandler(FooImpl fooImpl) {
            this.fooImpl = fooImpl;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Foo.class
                    && !method.isAnnotationPresent(BypassCheck.class) // no magic strings
                    && !this.fooImpl.getEnabled()) {

                return null;
            }

            return method.invoke(this.fooImpl, args);
        }
    }
}

11
Entiendo que esta es una solución inteligente, pero ¿realmente usarías esto?
bhspencer

1
es una buena solución, está utilizando el patrón de proxy dinámico para decorar el objeto con este comportamiento común que se encuentra al comienzo de cada método.
Victor

11
@bhspencer: una pregunta muy legítima. De hecho, lo he usado muchas veces para hacer el manejo de excepciones, el registro, el manejo de transacciones, etc. Admito que para clases más pequeñas, parece excesivo, y puede muy bien serlo. Pero si espero que la clase crezca mucho en complejidad y quiero garantizar un comportamiento consistente en todos los métodos a medida que lo hago, no me importa esta solución.
sstan

1
No para ser parte del 97% del mal aquí, pero ¿cuáles son las implicaciones de rendimiento de esta clase proxy?
corsiKa

55
@corsiKa: Buena pregunta. No hay duda de que usar proxys dinámicos es más lento que las llamadas a métodos directos. Sin embargo, para uso general, la sobrecarga de rendimiento será imperceptible. Tema SO relacionado si está interesado: Costo de rendimiento del proxy dinámico de Java
desde el

51

Hay muchas buenas sugerencias ... lo que puede hacer para resolver su problema es pensar en el Patrón de estado e implementarlo.

Eche un vistazo a este fragmento de código ... tal vez lo lleve a una idea. En este escenario, parece que desea modificar toda la implementación de los métodos en función del estado interno del objeto. Recuerde que la suma de los métodos en un objeto se conoce como comportamiento.

public class Foo {

      private FooBehaviour currentBehaviour = new FooEnabledBehaviour (); // or disabled, or use a static factory method for getting the default behaviour

      public void bar() {
        currentBehaviour.bar();
      }
      public void baz() {
        currentBehaviour.baz();
      }
      public void bat() {
        currentBehaviour.bat();
      }

      public void setFooEnabled (boolean fooEnabled) { // when you set fooEnabel, you are changing at runtime what implementation will be called.
        if (fooEnabled) {
          currentBehaviour = new FooEnabledBehaviour ();
        } else {
          currentBehaviour = new FooDisabledBehaviour ();
        }
      }

      private interface FooBehaviour {
        public void bar();
        public void baz();
        public void bat();
      }

      // RENEMBER THAT instance method of inner classes can refer directly to instance members defined in its enclosing class
      private class FooEnabledBehaviour implements FooBehaviour {
        public void bar() {
          // do what you want... when is enabled
        }
        public void baz() {}
        public void bat() {}

      }

      private class FooDisabledBehaviour implements FooBehaviour {
        public void bar() {
          // do what you want... when is desibled
        }
        public void baz() {}
        public void bat() {}

      }
}

¡Espero que te guste!

PD: es una implementación del patrón de estado (también conocido como estrategia según el contexto ... pero los principios son los mismos).


1
OP quiere no tener que repetir la misma línea de código al comienzo de cada método y su solución consiste en repetir la misma línea de código al comienzo de cada método.
Tulains Córdova

2
@ user1598390 no hay necesidad de repetir la evaluación, dentro de FooEnabledBehaviour está asumiendo que el cliente de este objeto ha establecido fooEnabled en verdadero, por lo que no hay necesidad de chequear. Lo mismo ocurre con la clase FooDisabledBehaviour. Verifique nuevamente, codifique.
Victor

2
Gracias @ bayou.io, esperemos hasta que el OP responda. Creo que la comunidad está haciendo un gran trabajo aquí, ¡hay muchos buenos consejos por aquí!
Victor

2
De acuerdo con @dyesdyes, no puedo imaginar implementar esto para otra cosa que no sea una clase realmente trivial. Es demasiado problemático, teniendo bar()en cuenta que in FooEnabledBehaviory bar()in FooDisabledBehaviorpodrían compartir mucho del mismo código, con quizás incluso una sola línea diferente entre los dos. Podría muy fácilmente, especialmente si este código fuera mantenido por desarrolladores junior (como yo), terminar con un desorden gigante de basura que es imposible de mantener y de verificar. Eso puede suceder con cualquier código, pero esto parece tan fácil de fastidiar realmente rápido. +1 sin embargo, porque buena sugerencia.
Chris Cirefice 01 de

1
Mmmmm ... no, chicos ... pero primero, gracias por los comentarios. Para mí, el tamaño del código no es un problema siempre que sea "limpio" y "legible". A mi favor, debería argumentar que no estoy usando ninguna clase externa ... eso debería hacer que las cosas sean más accesibles. Y si hay algún comportamiento común, encapsúlelo en CommonBehaviourClass y deleguemos donde lo necesite. En el libro GOF (no es mi libro favorito para aprender, pero tiene buenas recetas) encontró este ejemplo: en.wikipedia.org/wiki/… . Es más o menos lo mismo que hago aquí.
Victor

14

Sí, pero es un poco de trabajo, por lo que depende de lo importante que sea para ti.

Puede definir la clase como una interfaz, escribir una implementación de delegado y luego usarla java.lang.reflect.Proxypara implementar la interfaz con métodos que hacen la parte compartida y luego llaman condicionalmente al delegado.

interface Foo {
    public void bar();
    public void baz();
    public void bat();
}

class FooImpl implements Foo {
    public void bar() {
      //... <-- your logic represented by this notation above
    }

    public void baz() {
      //... <-- your logic represented by this notation above
    }

    // and so forth
}

Foo underlying = new FooImpl();
InvocationHandler handler = new MyInvocationHandler(underlying);
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
     new Class[] { Foo.class },
     handler);

Su MyInvocationHandleraspecto es posible que algo como esto (la gestión de errores y andamios clase se omite, asumiendo que fooIsEnabledse define en algún lugar accesible):

public Object invoke(Object proxy, Method method, Object[] args) {
    if (!fooIsEnabled) return null;
    return method.invoke(underlying, args);
}

No es increíblemente bonito. Pero a diferencia de varios comentaristas, lo haría, ya que creo que la repetición es un riesgo más importante que este tipo de densidad, y podrá producir la "sensación" de su clase real, con este envoltorio algo inescrutable agregado en muy localmente en solo un par de líneas de código.

Consulte la documentación de Java para obtener detalles sobre las clases de proxy dinámico.


14

Esta pregunta está estrechamente relacionada con la programación orientada a aspectos . AspectJ es una extensión AOP de Java y puede echarle un vistazo para obtener algo de inspiración.

Hasta donde yo sé, no hay soporte directo para AOP en Java. Hay algunos patrones GOF que se relacionan con él, como por ejemplo, el Método de plantilla y la Estrategia, pero en realidad no le ahorrará líneas de código.

En Java y en la mayoría de los otros lenguajes, puede definir la lógica recurrente que necesita en las funciones y adoptar el llamado enfoque de codificación disciplinada en el que los llama en el momento adecuado.

public void checkBalance() {
    checkSomePrecondition();
    ...
    checkSomePostcondition();
}

Sin embargo, esto no encajaría en su caso porque le gustaría que el código factorizado pueda regresar checkBalance. En los lenguajes que admiten macros (como C / C ++), podría definir checkSomePreconditiony checkSomePostconditioncomo macros y simplemente serían reemplazados por el preprocesador antes de que se invoque el compilador:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

Java no tiene esto fuera de la caja. Esto puede ofender a alguien, pero utilicé motores de generación automática de plantillas y plantillas para automatizar tareas de codificación repetitivas en el pasado. Si procesa sus archivos Java antes de compilarlos con un preprocesador adecuado, por ejemplo Jinja2, podría hacer algo similar a lo que es posible en C.

Posible enfoque de Java puro

Si está buscando una solución Java pura, lo que puede encontrar probablemente no sea conciso. Sin embargo, aún podría factorizar partes comunes de su programa y evitar la duplicación de código y los errores. Podrías hacer algo como esto (es una especie de patrón inspirado en la estrategia ). Tenga en cuenta que en C # y Java 8, y en otros lenguajes en los que las funciones son un poco más fáciles de manejar, este enfoque en realidad puede verse bien.

public interface Code {
    void execute();
}

...

public class Foo {
  private bool fooIsEnabled;

  private void protect(Code c) {
      if (!fooIsEnabled) return;
      c.execute();
  }

  public void bar() {
    protect(new Code {
      public void execute() {
        System.out.println("bar");
      }
    });
  }

  public void baz() {
    protect(new Code {
      public void execute() {
        System.out.println("baz");
      }
    });
  }

  public void bat() {
    protect(new Code {
      public void execute() {
        System.out.println("bat");
      }
    });
  }
}

Un poco como un escenario del mundo real

Está desarrollando una clase para enviar marcos de datos a un robot industrial. El robot toma tiempo para completar un comando. Una vez que se completa el comando, le devuelve un marco de control. El robot puede dañarse si recibe un nuevo comando mientras el anterior aún se está ejecutando. Su programa usa una DataLinkclase para enviar y recibir marcos hacia y desde el robot. Debe proteger el acceso a la DataLinkinstancia.

Las llamadas de subproceso de interfaz de usuario RobotController.left, right, upo downcuando el usuario hace clic en los botones, pero también llama BaseController.ticka intervalos regulares, con el fin de volver a activar el reenvío de comando a la privada DataLinkinstancia.

interface Code {
    void ready(DataLink dataLink);
}

class BaseController {
    private DataLink mDataLink;
    private boolean mReady = false;
    private Queue<Code> mEnqueued = new LinkedList<Code>();

    public BaseController(DataLink dl) {
        mDataLink = dl;
    }

    protected void protect(Code c) {
        if (mReady) {
            mReady = false;
            c.ready(mDataLink);
        }
        else {
            mEnqueue.add(c);
        }
    }

    public void tick() {
        byte[] frame = mDataLink.readWithTimeout(/* Not more than 50 ms */);

        if (frame != null && /* Check that it's an ACK frame */) {
          if (mEnqueued.isEmpty()) {
              mReady = true;
          }
          else {
              Code c = mEnqueued.remove();
              c.ready(mDataLink);
          }
        }
    }
}

class RobotController extends BaseController {
    public void left(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'left' by amount */);
        }});
    }

    public void right(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'right' by amount */);
        }});
    }

    public void up(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'up' by amount */);
        }});
    }

    public void down(float amount) {
        protect(new Code() { public void ready(DataLink dataLink) {
            dataLink.write(/* Create a byte[] that means 'down' by amount */);
        }});
    }
}

44
¿No es esto simplemente patear la lata por el camino? Es decir, el futuro mantenedor solía tener que recordar si (! FooIsEnabled) regresa; al comienzo de cada función y ahora deben recordar proteger (nuevo Código {... al comienzo de cada función. ¿Cómo ayuda esto?
bhspencer

Me gusta su análisis y contexto de fondo damix911 ... construiría las nuevas instancias de Código en tiempo de compilación (usando miembros estáticos privados) suponiendo que el código no cambiará con el tiempo y cambiaría el nombre del cambio a "executeIf" pasando como argumento una Condición (Al igual que la clase Predicate) y el Código. Pero eso es más sentido personal y gusto.
Victor

1
@bhspencer Parece bastante torpe en Java, y en la mayoría de los casos esta estrategia es realmente una ingeniería excesiva de un código simple. No muchos programas pueden beneficiarse de este patrón. Sin embargo, algo bueno es que creamos un nuevo símbolo protectque es más fácil de reutilizar y documentar. Si le dice al futuro mantenedor que se debe proteger el código crítico protect, ya le está diciendo qué hacer. Si las reglas de protección cambian, el nuevo código aún está protegido. Esta es exactamente la razón detrás de la definición de funciones, pero el OP necesitaba "devolver un return" que las funciones no pueden hacer.
damix911

11

Consideraría refactorizar. Este patrón está rompiendo fuertemente el patrón SECO (No se repita). Creo que esto rompe esta responsabilidad de clase. Pero esto depende de su control de código. Tu pregunta es muy abierta: ¿a dónde llamas Fooinstancia?

Supongo que tienes un código como

foo.bar(); // does nothing if !fooEnabled
foo.baz(); // does also nothing
foo.bat(); // also

tal vez deberías llamarlo así:

if (fooEnabled) {
   foo.bat();
   foo.baz();
   ...
}

Y mantenlo limpio. Por ejemplo, el registro:

this.logger.debug(createResourceExpensiveDump())

a logger no se pregunta si la depuración está habilitada. Solo registra.

En cambio, la clase de llamada debe verificar esto:

if (this.logger.isDebugEnabled()) {
   this.logger.debug(createResourceExpensiveDump())
}

Si se trata de una biblioteca y no puede controlar las llamadas de esta clase, arroje una IllegalStateExceptionque explique por qué, si esta llamada es ilegal y causa problemas.


66
Definitivamente es más simple y más fácil a la vista. Pero si el objetivo de OP es asegurarse de que, a medida que se agreguen nuevos métodos, que la lógica habilitada nunca se omita, esta refactorización no facilita su aplicación.
sstan

44
También para su ejemplo de registro, diría que esto implica mucha más repetición: cada vez que desee iniciar sesión, debe verificar que el registrador esté habilitado. Tiendo a registrar más líneas que la cantidad de métodos en cualquier clase ...
T. Kiley

44
Esto rompe la modularidad, porque ahora la persona que llama tiene que saber algo sobre las partes internas de foo (en este caso si es fooEnabled). Este es un ejemplo clásico en el que seguir las reglas de mejores prácticas no resolverá el problema porque las reglas entran en conflicto. (Todavía estoy esperando que alguien va a llegar a una respuesta que "por qué no se me había ocurrido eso?".)
Ian Goldby

2
Bueno, depende en gran medida del contexto si tiene sentido.
Ian Goldby

3
El registro es exactamente el ejemplo, donde no quiero que se repita mi código. Solo quiero escribir LOG.debug ("...."); - Y el registrador debería comprobar si realmente quiero depurar. - Otro ejemplo es el cierre / limpieza. - Si uso AutoClosable no quiero una excepción si ya está cerrado, simplemente no debería hacer nada.
Falco

6

En mi humilde opinión, la solución más elegante y de mejor rendimiento para esto es tener más de una implementación de Foo, junto con un método de fábrica para crear uno:

class Foo {
  protected Foo() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do something
  }

  public static void getFoo() {
    return fooEnabled ? new Foo() : new NopFoo();
  }
}

class NopFoo extends Foo {
  public void bar() {
    // Do nothing
  }
}

O una variación:

class Foo {
  protected Foo() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do something
  }

  public static void getFoo() {
    return fooEnabled ? new Foo() : NOP_FOO;
  }

  private static Foo NOP_FOO = new Foo() {
    public void bar() {
      // Do nothing
    }
  };
}

Como señala sstan, aún mejor sería usar una interfaz:

public interface Foo {
  void bar();

  static Foo getFoo() {
    return fooEnabled ? new FooImpl() : new NopFoo();
  }
}

class FooImpl implements Foo {
  FooImpl() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do something
  }
}

class NopFoo implements Foo {
  NopFoo() {
    // Prevent direct instantiation
  }

  public void bar() {
    // Do nothing
  }
}

Adapte esto al resto de sus circunstancias (¿está creando un nuevo Foo cada vez o reutilizando la misma instancia, etc.)


1
Esto no es lo mismo que la respuesta de Konrad. Me gusta esto, pero creo que sería más seguro si, en lugar de utilizar la herencia de clase, utilizara una interfaz como otros han sugerido en sus respuestas. La razón es simple: es demasiado fácil para un desarrollador agregar un método Fooy olvidar agregar la versión no operativa del método en la clase extendida, evitando así el comportamiento deseado.
sstan

2
@sstan Tienes razón, eso sería mejor. Lo hice así para modificar el ejemplo original de Kristina lo menos posible para evitar distracciones, pero esto es relevante. Agregaré tu sugerencia a mi respuesta.
Pepijn Schmitz

1
Creo que pierdes un punto. Usted determina si isFooEnableden la instanciación de Foo. Es demasiado temprano. En el código original, esto se hace cuando se ejecuta un método. El valor de isFooEnabledpuede cambiar mientras tanto.
Nicolas Barbulesco

1
@NicolasBarbulesco No lo sabes, fooIsEnabled podría ser una constante. O podría usarse de una manera que lo haga efectivamente constante. O podría establecerse en unos pocos lugares bien definidos para que sea fácil obtener una nueva instancia de Foo cada vez. O podría ser aceptable obtener una nueva instancia de Foo cada vez que se usa. No sabes, por eso escribí: "adapta esto al resto de tus circunstancias".
Pepijn Schmitz

@PepijnSchmitz - Por supuesto, fooIsEnabled puede ser constante. Pero nada nos dice que sea constante. Entonces considero el caso general.
Nicolas Barbulesco

5

Tengo otro enfoque: tener un

interface Foo {
  public void bar();
  public void baz();
  public void bat();
}

class FooImpl implements Foo {
  public void bar() {
    //...
  }
  public void baz() {
    //...
  }
  public void bat() {
    //...
  }
}

class NullFoo implements Foo {
  static NullFoo DEFAULT = new NullFoo();
  public void bar() {}
  public void baz() {}
  public void bat() {}
}

}

y luego puedes hacer

(isFooEnabled ? foo : NullFoo.DEFAULT).bar();

Tal vez incluso puedas reemplazar el isFooEnabledcon unFoo variable que contiene el FooImplpara ser utilizado o el NullFoo.DEFAULT. Entonces la llamada es más simple nuevamente:

Foo toBeUsed = isFooEnabled ? foo : NullFoo.DEFAULT;
toBeUsed.bar();
toBeUsed.baz();
toBeUsed.bat();

Por cierto, esto se llama el "patrón nulo".


El enfoque general es bueno, pero usar la expresión (isFooEnabled ? foo : NullFoo.DEFAULT).bar();parece un poco torpe. Tener una tercera implementación que delegue a una de las implementaciones existentes. En lugar de cambiar el valor de un campo isFooEnabled, se podría cambiar el objetivo de la delegación. Esto reduce la cantidad de ramas en el código
SpaceTrucker,

1
¡Pero estás deportando la cocina interna de la clase Fooen el código de llamada! ¿Cómo podríamos saber si isFooEnabled? Este es un campo interno en la clase Foo.
Nicolas Barbulesco

3

En un enfoque funcional similar a la respuesta de @ Colin, con las funciones lambda de Java 8 , es posible ajustar el código de activación / desactivación de la función condicional en un método de protección ( executeIfEnabled) que acepta la acción lambda, a la que se puede ejecutar el código condicionalmente pasado

Aunque en su caso, este enfoque no guardará ninguna línea de código, al SECAR esto, ahora tiene la opción de centralizar otras inquietudes de alternancia de funciones, más AOP o inquietudes de depuración como el registro, el diagnóstico, el perfil, etc.

Una ventaja de usar lambdas aquí es que los cierres se pueden usar para evitar la necesidad de sobrecargar el executeIfEnabledmétodo.

Por ejemplo:

class Foo {
    private Boolean _fooIsEnabled;

    public Foo(Boolean isEnabled) {
        _fooIsEnabled = isEnabled;
    }

    private void executeIfEnabled(java.util.function.Consumer someAction) {
        // Conditional toggle short circuit
        if (!_fooIsEnabled) return;

        // Invoke action
        someAction.accept(null);
    }

    // Wrap the conditionally executed code in a lambda
    public void bar() {
        executeIfEnabled((x) -> {
            System.out.println("Bar invoked");
        });
    }

    // Demo with closure arguments and locals
    public void baz(int y) {
        executeIfEnabled((x) -> {
            System.out.printf("Baz invoked %d \n", y);
        });
    }

    public void bat() {
        int z = 5;
        executeIfEnabled((x) -> {
            System.out.printf("Bat invoked %d \n", z);
        });
    }

Con una prueba:

public static void main(String args[]){
    Foo enabledFoo = new Foo(true);
    enabledFoo.bar();
    enabledFoo.baz(33);
    enabledFoo.bat();

    Foo disabledFoo = new Foo(false);
    disabledFoo.bar();
    disabledFoo.baz(66);
    disabledFoo.bat();
}

También es similar al enfoque de Damix, sin la necesidad de una interfaz e implementaciones de clases anónimas con anulación de método.
StuartLC

2

Como se señala en otras respuestas, el Patrón de diseño de estrategia es un patrón de diseño apropiado a seguir para simplificar este código. Lo he ilustrado aquí usando la invocación de métodos a través de la reflexión, pero hay varios mecanismos que podría usar para obtener el mismo efecto.

class Foo {

  public static void main(String[] args) {
      Foo foo = new Foo();
      foo.fooIsEnabled = false;
      foo.execute("bar");
      foo.fooIsEnabled = true;
      foo.execute("baz");
  }

  boolean fooIsEnabled;

  public void execute(String method) {
    if(!fooIsEnabled) {return;}
    try {
       this.getClass().getDeclaredMethod(method, (Class<?>[])null).invoke(this, (Object[])null);
    }
    catch(Exception e) {
       // best to handle each exception type separately
       e.printStackTrace();
    }
  }

  // Changed methods to private to reinforce usage of execute method
  private void bar() {
    System.out.println("bar called");
    // bar stuff here...
  }
  private void baz() {
    System.out.println("baz called");
    // baz stuff here...
  }
  private void bat() {
    System.out.println("bat called");
    // bat stuff here...
  }
}

Tener que lidiar con la reflexión es un poco incómodo, si ya hay clases que hacen esto por usted, como las ya mencionadas Proxy.
SpaceTrucker

¿Cómo pudiste hacer foo.fooIsEnabled ...? A priori, este es un campo interno del objeto, no podemos y no queremos verlo afuera.
Nicolas Barbulesco

2

Si tan solo Java fuera un poco mejor para ser funcional. Piensa que la solución más OOO es crear una clase que envuelva una sola función, por lo que se llama solo cuando foo está habilitado.

abstract class FunctionWrapper {
    Foo owner;

    public FunctionWrapper(Foo f){
        this.owner = f;
    }

    public final void call(){
        if (!owner.isEnabled()){
            return;
        }
        innerCall();
    }

    protected abstract void innerCall();
}

y luego implementar bar, bazy batcomo clases anónimas que se extienden FunctionWrapper.

class Foo {
    public boolean fooIsEnabled;

    public boolean isEnabled(){
        return fooIsEnabled;
    }

    public final FunctionWrapper bar = new FunctionWrapper(this){
        @Override
        protected void innerCall() {
            // do whatever
        }
    };

    public final FunctionWrapper baz = new FunctionWrapper(this){
        @Override
        protected void innerCall() {
            // do whatever
        }
    };

    // you can pass in parms like so 
    public final FunctionWrapper bat = new FunctionWrapper(this){
        // some parms:
        int x,y;
        // a way to set them
        public void setParms(int x,int y){
            this.x=x;
            this.y=y;
        }

        @Override
        protected void innerCall() {
            // do whatever using x and y
        }
    };
}

Otra idea

Uso glglgl de solución anulable pero maquillaje FooImply NullFooclases internas (con constructores privados) de la clase a continuación:

class FooGateKeeper {

    public boolean enabled;

    private Foo myFooImpl;
    private Foo myNullFoo;

    public FooGateKeeper(){
        myFooImpl= new FooImpl();
        myNullFoo= new NullFoo();
    }

    public Foo getFoo(){
        if (enabled){
            return myFooImpl;
        }
        return myNullFoo;
    }  
}

de esta manera no tiene que preocuparse por recordar usar (isFooEnabled ? foo : NullFoo.DEFAULT).


Digamos que tiene: Foo foo = new Foo()para llamarlo barescribiríafoo.bar.call()
Colin

1

Parece que la clase no hace nada cuando Foo no está habilitado, así que ¿por qué no expresar esto en un nivel superior donde creas u obtienes la instancia de Foo?

class FooFactory
{
 static public Foo getFoo()
 {
   return isFooEnabled ? new Foo() : null;
 }
}
 ...
 Foo foo = FooFactory.getFoo();
 if(foo!=null)
 {
   foo.bar();
   ....
 }     

Sin embargo, esto solo funciona si isFooEnabled es una constante. En un caso general, puede crear su propia anotación.


Konrad, ¿puedes desarrollar la anotación?
Nicolas Barbulesco

El código original determina si fooIsEnabledse llama a un método. Haces esto antes de la instanciación de Foo. Es demasiado temprano. El valor puede cambiar mientras tanto.
Nicolas Barbulesco

Creo que pierdes un punto. A priori, isFooEnabledes un campo de instancia de Fooobjetos.
Nicolas Barbulesco

1

No estoy familiarizado con la sintaxis de Java. Suposición de que en Java hay polimorfismo, propiedad estática, clase y método abstractos:

    public static void main(String[] args) {
    Foo.fooIsEnabled = true; // static property, not particular to a specific instance  

    Foo foo = new bar();
    foo.mainMethod();

    foo = new baz();
    foo.mainMethod();

    foo = new bat();
    foo.mainMethod();
}

    public abstract class Foo{
      static boolean fooIsEnabled;

      public void mainMethod()
      {
          if(!fooIsEnabled)
              return;

          baMethod();
      }     
      protected abstract void baMethod();
    }
    public class bar extends Foo {
        protected override baMethod()
        {
            // bar implementation
        }
    }
    public class bat extends Foo {
        protected override baMethod()
        {
            // bat implementation
        }
    }
    public class baz extends Foo {
        protected override baMethod()
        {
            // baz implementation
        }
    }

¿Quién dice que estar habilitado es una propiedad estática de la clase?
Nicolas Barbulesco

¿Qué se new bar()supone que significa?
Nicolas Barbulesco

En Java, escribimos una clase Namecon mayúscula.
Nicolas Barbulesco

Esto necesita cambiar demasiado el código de llamada. Llamamos a un método normalmente: bar(). Si necesitas alterar eso, estás condenado.
Nicolas Barbulesco

1

Básicamente, tiene un indicador de que, si está configurado, se debe omitir la llamada a la función. Así que creo que mi solución sería tonta, pero aquí está.

Foo foo = new Foo();

if (foo.isEnabled())
{
    foo.doSomething();
}

Aquí está la implementación de un Proxy simple, en caso de que desee ejecutar algún código antes de ejecutar cualquier función.

class Proxy<T>
{
    private T obj;
    private Method<T> proxy;

    Proxy(Method<T> proxy)
    {
        this.ojb = new T();
        this.proxy = proxy;
    }

    Proxy(T obj, Method<T> proxy)
    {
        this.obj = obj;
        this.proxy = proxy;
    }

    public T object ()
    {
        this.proxy(this.obj);
        return this.obj;
    }
}

class Test
{
    public static void func (Foo foo)
    {
        // ..
    }

    public static void main (String [] args)
    {
        Proxy<Foo> p = new Proxy(Test.func);

        // how to use
        p.object().doSomething();
    }
}

class Foo
{
    public void doSomething ()
    {
        // ..
    }
}

Para su primer bloque de código, necesita un método visible isEnabled(). A priori, estar habilitado es una cocción interna Foo, no expuesta.
Nicolas Barbulesco

El código de llamada no puede y no quiere saber si el objeto está habilitado .
Nicolas Barbulesco

0

Hay otra solución, usar delegado (puntero para funcionar). Puede tener un método único que primero realiza la validación y luego llama al método relevante de acuerdo con la función (parámetro) a llamar. Código C #:

internal delegate void InvokeBaxxxDelegate();

class Test
{
    private bool fooIsEnabled;

    public Test(bool fooIsEnabled)
    {
        this.fooIsEnabled = fooIsEnabled;
    }

    public void Bar()
    {
        InvokeBaxxx(InvokeBar);
    }

    public void Baz()
    {
        InvokeBaxxx(InvokeBaz);
    }

    public void Bat()
    {
        InvokeBaxxx(InvokeBat);
    }

    private void InvokeBaxxx(InvokeBaxxxDelegate invoker)
    {
        if (!fooIsEnabled) return;
        invoker();
    }

    private void InvokeBar()
    {
        // do Invoke bar stuff
        Console.WriteLine("I am Bar");
    }

    private void InvokeBaz()
    {
        // do Invoke bar stuff
        Console.WriteLine("I am Baz");
    }

    private void InvokeBat()
    {
        // do Invoke bar stuff
        Console.WriteLine("I am Bat");
    }
}

2
Correcto, está marcado Java y es por eso que enfatizo y escribí "Código en C #" ya que no conozco Java. Como es una pregunta de Patrón de diseño, el lenguaje no es importante.
ehh

Oh! Entiendo, lo siento, solo intenté ayudar y encontrar una solución. Gracias
ehh
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.