Existe-t-il une manière élégante de faire démarrer chaque méthode d'une classe avec un certain bloc de code?


143

J'ai une classe où chaque méthode commence de la même manière:

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

Existe-t-il un moyen agréable d'exiger (et, espérons-le, de ne pas écrire à chaque fois) la fooIsEnabledpartie pour chaque méthode publique de la classe?


45
Examinez la programmation orientée aspect (en particulier avant l'avis ).
Sotirios Delimanolis

15
Pour combien de méthodes devez-vous utiliser ce passe-partout? Avant de vous lancer dans l'introduction de l'AOP, vous voudrez peut-être envisager d'avoir ce petit bout de code répété. Parfois, un petit copier-coller est la solution la plus simple.
bhspencer

51
Je soupçonne que votre futur responsable serait plus heureux avec la plaque chauffante supplémentaire que d'avoir à apprendre un framework AOP.
bhspencer

7
Si chaque méthode de la classe doit faire exactement la même chose dans ses premières lignes de code, alors nous avons une mauvaise conception.
Tulains Córdova le

7
@ user1598390: La question n'est pas hors sujet ici, et il n'y a rien dans la portée des programmeurs qui rend la question particulièrement significative là-bas.
Robert Harvey le

Réponses:


90

Je ne sais pas à propos d'élégant, mais voici une implémentation fonctionnelle utilisant la fonction intégrée de Java java.lang.reflect.Proxyqui impose que toutes les invocations de méthode Foocommencent par vérifier l' enabledétat.

main méthode:

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 interface:

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 classe:

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

Comme d'autres l'ont souligné, il semble exagéré pour ce dont vous avez besoin si vous n'avez que quelques méthodes à craindre.

Cela dit, il y a certainement des avantages:

  • Une certaine séparation des préoccupations est obtenue, car Fooles implémentations de méthodes n'ont pas à se soucier de la enabledpréoccupation transversale de vérification. Au lieu de cela, le code de la méthode n'a qu'à se soucier de l'objectif principal de la méthode, rien de plus.
  • Il n'y a aucun moyen pour un développeur innocent d'ajouter une nouvelle méthode à la Fooclasse et par erreur "oublier" d'ajouter la enabledvérification. Le enabledcomportement de vérification est automatiquement hérité par toute méthode nouvellement ajoutée.
  • Si vous avez besoin d'ajouter une autre préoccupation transversale, ou si vous avez besoin d'améliorer le enabledcontrôle, il est très facile de le faire en toute sécurité et en un seul endroit.
  • C'est plutôt bien que vous puissiez obtenir ce comportement de type AOP avec la fonctionnalité Java intégrée. Vous n'êtes pas obligé d'avoir à intégrer un autre framework comme Spring, bien qu'ils puissent certainement être de bonnes options aussi.

Pour être juste, certains des inconvénients sont:

  • Une partie du code d'implémentation qui gère les invocations de proxy est laide. Certains diront aussi qu'avoir des classes internes pour empêcher l'instanciation de la FooImplclasse est moche.
  • Si vous souhaitez ajouter une nouvelle méthode à Foo, vous devez effectuer un changement en 2 points: la classe d'implémentation et l'interface. Pas grand-chose, mais c'est encore un peu plus de travail.
  • Les invocations de proxy ne sont pas gratuites. Il y a une certaine surcharge de performance. Pour un usage général cependant, cela ne sera pas perceptible. Voir ici pour plus d' informations.

ÉDITER:

Le commentaire de Fabian Streitel m'a fait réfléchir à 2 ennuis avec ma solution ci-dessus qui, je l'admets, je ne suis pas content de moi:

  1. Le gestionnaire d'invocation utilise des chaînes magiques pour ignorer le «contrôle activé» sur les méthodes «getEnabled» et «setEnabled». Cela peut facilement casser si les noms de méthode sont refactorisés.
  2. S'il y avait un cas où de nouvelles méthodes devaient être ajoutées qui ne devraient pas hériter du comportement "enabled-check", alors il peut être assez facile pour le développeur de se tromper, et à tout le moins, cela signifierait ajouter plus de magie cordes.

Pour résoudre le point n ° 1, et pour au moins atténuer le problème avec le point n ° 2, je créerais une annotation BypassCheck(ou quelque chose de similaire) que je pourrais utiliser pour marquer les méthodes de l' Foointerface pour lesquelles je ne veux pas exécuter le " vérification activée ". De cette façon, je n'ai pas du tout besoin de chaînes magiques, et il devient beaucoup plus facile pour un développeur d'ajouter correctement une nouvelle méthode dans ce cas particulier.

En utilisant la solution d'annotation, le code ressemblerait à ceci:

main méthode:

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 annotation:

import java.lang.annotation.*;

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

Foo interface:

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 classe:

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
Je comprends que c'est une solution intelligente, mais l'utiliseriez-vous vraiment?
bhspencer le

1
est une bonne solution de contournement, vous utilisez le modèle de proxy dynamique pour décorer l'objet avec ce comportement courant trouvé au début de chaque méthode.
Victor

11
@bhspencer: Une question très légitime. Je l'ai en fait utilisé plusieurs fois pour gérer les exceptions, la journalisation, la gestion des transactions, etc. J'admets que pour les petites classes, cela semble excessif, et peut très bien l'être. Mais si je m'attends à ce que la classe devienne beaucoup plus complexe et que je souhaite garantir un comportement cohérent dans toutes les méthodes, cela ne me dérange pas.
sstan

1
Ne pas faire partie des 97% du mal ici, mais quelles sont les implications de performance de cette classe proxy?
corsiKa

5
@corsiKa: Bonne question. Il ne fait aucun doute que l'utilisation de proxys dynamiques est plus lente que les appels de méthode directs. Pour une utilisation générale cependant, la surcharge de performance sera imperceptible. Fil SO connexe si vous êtes intéressé: Coût des performances du proxy dynamique Java
sstan

51

Il y a beaucoup de bonnes suggestions ... ce que vous pouvez faire pour résoudre votre problème est de penser au modèle d'état et de l'implémenter.

Jetez un œil à cet extrait de code ... peut-être qu'il vous donnera une idée. Dans ce scénario, il semble que vous souhaitiez modifier l'ensemble de l'implémentation des méthodes en fonction de l'état interne de l'objet. Veuillez vous rappeler que la somme des méthodes dans un objet est connue sous le nom de comportement.

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

      }
}

J'espère que vous aimez!

PD: est une mise en œuvre du modèle d'état (également connu sous le nom de stratégie en fonction du contexte ... mais les principes sont les mêmes).


1
OP veut ne pas avoir à répéter la même ligne de code au début de chaque méthode et votre solution consiste à répéter la même ligne de code au début de chaque méthode.
Tulains Córdova

2
@ user1598390 il n'est pas nécessaire de répéter l'évaluation, à l'intérieur du FooEnabledBehaviour, vous supposez que le client de cet objet a mis fooEnabled à true, il n'est donc pas nécessaire de vérifier. Il en va de même pour la classe FooDisabledBehaviour. Vérifiez à nouveau, codez-y.
Victor

2
Merci @ bayou.io, attendons la réponse OP. Je pense que la communauté fait un sacré boulot ici, il y a beaucoup de bons conseils ici!
Victor

2
D'accord avec @dyesdyes, je ne peux pas imaginer l'implémenter pour autre chose qu'une classe vraiment triviale. C'est tout simplement trop problématique, étant donné que bar()in FooEnabledBehavioret bar()in FooDisabledBehaviorpourraient partager une grande partie du même code, avec peut-être même une seule ligne différente entre les deux. Vous pourriez très facilement, surtout si ce code devait être maintenu par des développeurs juniors (comme moi), vous retrouver avec un énorme bordel de merde qui est impossible à maintenir et à tester. Cela peut arriver avec n'importe quel code, mais cela semble tellement facile à foutre très vite. +1 cependant, car bonne suggestion.
Chris Cirefice

1
Mmmmm ... je ne sais pas les gars ... mais d'abord, merci pour vos commentaires. Pour moi, la taille du code n'est pas un problème tant qu'il est "propre" et "lisible". En ma faveur, je devrais faire valoir que je n'utilise aucune classe externe .. qui devrait rendre les choses plus accessibles. Et s'il y a un comportement commun, encapsulons-le dans une CommonBehaviourClass et déléguons là où il en a besoin. Dans le livre GOF (pas mon livre préféré pour apprendre, mais qui contient de bonnes recettes), vous avez trouvé cet exemple: en.wikipedia.org/wiki/… . C'est plus ou moins la même chose que je fais ici.
Victor

14

Oui, mais c'est un peu de travail, cela dépend donc de son importance pour vous.

Vous pouvez définir la classe en tant qu'interface, écrire une implémentation de délégué, puis l'utiliser java.lang.reflect.Proxypour implémenter l'interface avec des méthodes qui effectuent la partie partagée, puis appellent conditionnellement le délégué.

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

Votre MyInvocationHandlerpeut ressembler à quelque chose comme ceci (gestion des erreurs et échafaudage de classe omis, en supposant qu'il fooIsEnabledsoit défini dans un endroit accessible):

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

Ce n'est pas incroyablement joli. Mais contrairement à divers commentateurs, je le ferais, car je pense que la répétition est un risque plus important que ce type de densité, et vous serez en mesure de produire la "sensation" de votre vraie classe, avec ce wrapper quelque peu impénétrable ajouté. très localement en seulement quelques lignes de code.

Consultez la documentation Java pour plus de détails sur les classes proxy dynamiques.


14

Cette question est étroitement liée à la programmation orientée aspect . AspectJ est une extension AOP de Java et vous pouvez lui donner un coup d'oeil pour obtenir une certaine inspiration.

Autant que je sache, il n'y a pas de support direct pour AOP en Java. Il y a quelques modèles GOF qui s'y rapportent, comme par exemple la méthode de modèle et la stratégie, mais cela ne vous sauvera pas vraiment des lignes de code.

En Java et dans la plupart des autres langages, vous pouvez définir la logique récurrente dont vous avez besoin dans les fonctions et adopter une approche de codage dite disciplinée dans laquelle vous les appelez au bon moment.

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

Cependant, cela ne conviendrait pas à votre cas car vous souhaitez que le code factorisé puisse revenir checkBalance. Dans les langages qui prennent en charge les macros (comme C / C ++), vous pouvez définir checkSomePreconditionet en checkSomePostconditiontant que macros et elles seraient simplement remplacées par le préprocesseur avant même que le compilateur ne soit appelé:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

Java ne l'a pas prêt à l'emploi. Cela peut offenser quelqu'un, mais j'ai utilisé la génération automatique de code et des moteurs de modèles pour automatiser les tâches de codage répétitives dans le passé. Si vous traitez vos fichiers Java avant de les compiler avec un préprocesseur approprié, par exemple Jinja2, vous pouvez faire quelque chose de similaire à ce qui est possible en C.

Approche Java pure possible

Si vous recherchez une solution Java pure, ce que vous pouvez trouver ne sera probablement pas concis. Mais cela pourrait toujours éliminer les parties communes de votre programme et éviter la duplication de code et les bogues. Vous pouvez faire quelque chose comme ça (c'est une sorte de modèle inspiré de la stratégie ). Notez qu'en C # et Java 8, et dans d'autres langages dans lesquels les fonctions sont un peu plus faciles à gérer, cette approche peut en fait sembler agréable.

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 peu d'un scénario du monde réel

Vous développez une classe pour envoyer des trames de données à un robot industriel. Le robot prend du temps pour exécuter une commande. Une fois la commande terminée, elle vous renvoie une trame de contrôle. Le robot peut être endommagé s'il reçoit une nouvelle commande alors que la précédente est toujours en cours d'exécution. Votre programme utilise une DataLinkclasse pour envoyer et recevoir des trames vers et depuis le robot. Vous devez protéger l'accès à l' DataLinkinstance.

Les appels de fil d'interface utilisateur RobotController.left, right, upou downlorsque l'utilisateur clique sur les boutons, mais appelle également BaseController.tickà intervalles réguliers, afin de la transmission de commande réactivez au secteur privé par DataLinkexemple.

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

4
N'est-ce pas juste un coup de pied sur la route. C'est-à-dire que le futur responsable devait se souvenir de if (! FooIsEnabled) return; au début de chaque fonction et maintenant ils doivent se rappeler de protéger (nouveau code {... au début de chaque fonction. Comment cela aide-t
bhspencer

1
Je vous aime analyse et contexte de fond damix911 ... je construirais les nouvelles instances de Code au moment de la compilation (en utilisant des membres statiques privés) en supposant que le code ne changera pas avec le temps et renommer le changement en "executeIf" en passant comme argument une condition (Comme la classe Predicate) et le Code. Mais c'est une coupe et un goût plus personnels.
Victor

1
@bhspencer Cela semble plutôt maladroit en Java, et dans la plupart des cas, cette stratégie est en réalité une suringénierie de code autrement simple. Peu de programmes peuvent bénéficier d'un tel schéma. Une chose intéressante cependant est que nous avons créé un nouveau symbole protectqui est plus facile à réutiliser et à documenter. Si vous dites au futur responsable avec lequel le code critique doit être protégé protect, vous lui dites déjà quoi faire. Si les règles de protection changent, le nouveau code est toujours protégé. C'est exactement la raison d'être de la définition des fonctions, mais l'OP avait besoin de «renvoyer un return», ce que les fonctions ne peuvent pas faire.
damix911

11

J'envisagerais de refactoriser. Ce modèle rompt fortement le modèle DRY (ne vous répétez pas). Je crois que cela brise cette responsabilité de classe. Mais cela dépend de votre maîtrise du code. Votre question est très ouverte - où appelez-vous Fooinstance?

Je suppose que vous avez du code comme

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

peut-être devriez-vous l'appeler de cette façon:

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

Et gardez-le propre. Par exemple, la journalisation:

this.logger.debug(createResourceExpensiveDump())

a logger ne se demande pas si le débogage est activé. Il enregistre simplement.

Au lieu de cela, la classe appelante doit vérifier ceci:

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

S'il s'agit d'une bibliothèque et que vous ne pouvez pas contrôler l'appel de cette classe, lancez un IllegalStateExceptionqui explique pourquoi, si cet appel est illégal et cause des problèmes.


6
C'est définitivement plus simple et plus facile pour les yeux. Mais si le but d'OP est de s'assurer que, à mesure que de nouvelles méthodes sont ajoutées, que la logique activée n'est jamais contournée, alors cette refactorisation ne facilite pas l'application de cela.
sstan

4
Aussi pour votre exemple de journal, je dirais que cela implique beaucoup plus de répétitions - chaque fois que vous voulez vous connecter, vous devez vérifier que l'enregistreur est activé. J'ai tendance à enregistrer plus de lignes que le nombre de méthodes sur n'importe quelle classe ...
T.Kiley

4
Cela rompt la modularité, car maintenant l'appelant doit savoir quelque chose sur les éléments internes de foo (dans ce cas, s'il est fooEnabled). Il s'agit d'un exemple classique où le respect des règles des meilleures pratiques ne résoudra pas le problème car les règles sont en conflit. (J'espère toujours que quelqu'un trouvera une réponse "pourquoi n'y ai-je pas pensé?".)
Ian Gold d'ici le

2
Eh bien, cela dépend fortement du contexte pour savoir si cela a du sens.
Ian Gold du

3
La journalisation est exactement l'exemple, où je ne veux pas de répétition dans mon code. Je veux juste écrire LOG.debug ("...."); - Et le logger devrait vérifier si je veux vraiment déboguer. - Un autre exemple est la fermeture / le nettoyage. - Si j'utilise AutoClosable, je ne veux pas d'exception s'il est déjà fermé, il ne devrait rien faire.
Falco

6

À mon humble avis, la solution la plus élégante et la plus performante à cela consiste à avoir plus d'une implémentation de Foo, ainsi qu'une méthode d'usine pour en créer une:

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

Ou une variante:

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

Comme le souligne sstan, il serait encore mieux d'utiliser une interface:

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

Adaptez cela au reste de vos circonstances (créez-vous un nouveau Foo à chaque fois ou réutilisez-vous la même instance, etc.)


1
Ce n'est pas la même chose que la réponse de Konrad. J'aime ça, mais je pense que ce serait plus sûr si, au lieu d'utiliser l'héritage de classe, vous utilisiez une interface comme d'autres l'ont suggéré dans leurs réponses. La raison est simple: il est trop facile pour un développeur d'ajouter une méthode Fooet d'oublier d'ajouter la version no-op de la méthode dans la classe étendue, contournant ainsi le comportement souhaité.
sstan

2
@sstan Vous avez raison, ce serait mieux. Je l'ai fait comme ça pour modifier le moins possible l'exemple original de Kristina afin d'éviter les distractions, mais c'est pertinent. J'ajouterai votre suggestion à ma réponse.
Pepijn Schmitz

1
Je pense que vous manquez un point. Vous déterminez si isFooEnabledlors de l'instanciation de Foo. C'est trop tôt. Dans le code d'origine, cela se fait lorsqu'une méthode est exécutée. La valeur de isFooEnabledpeut changer entre-temps.
Nicolas Barbulesco

1
@NicolasBarbulesco Vous ne savez pas que, fooIsEnabled pourrait être une constante. Ou il pourrait être utilisé d'une manière qui le rend effectivement constant. Ou il pourrait être placé dans quelques endroits bien définis afin qu'il soit facile d'obtenir une nouvelle instance de Foo à chaque fois. Ou il pourrait être acceptable d'obtenir une nouvelle instance de Foo à chaque fois qu'il est utilisé. Vous ne savez pas, c'est pourquoi j'ai écrit: "adaptez ceci au reste de votre situation".
Pepijn Schmitz le

@PepijnSchmitz - Bien sûr, cela fooIsEnabled peut être constant. Mais rien ne nous dit que ce soit constant. Je considère donc le cas général.
Nicolas Barbulesco le

5

J'ai une autre approche: avoir 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() {}
}

}

et alors tu peux faire

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

Peut-être que vous pouvez même remplacer le isFooEnabledpar une Foovariable qui contient le FooImplà utiliser ou le NullFoo.DEFAULT. Ensuite, l'appel est à nouveau plus simple:

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

BTW, cela s'appelle le "modèle nul".


L'approche globale est bonne, mais l'utilisation de l'expression (isFooEnabled ? foo : NullFoo.DEFAULT).bar();semble un peu maladroite. Avoir une troisième implémentation qui délègue à l'une des implémentations existantes. Au lieu de changer la valeur d'un champ, isFooEnabledla cible de la délégation pourrait être modifiée. Cela réduit le nombre de branches dans le code
SpaceTrucker

1
Mais vous déportez la cuisson interne de la classe Foodans le code d'appel! Comment pourrions-nous savoir si isFooEnabled? Ceci est un champ interne de la classe Foo.
Nicolas Barbulesco

3

Dans une approche fonctionnelle similaire à la réponse de @ Colin, avec les fonctions lambda de Java 8 , il est possible d'envelopper le code d'activation / désactivation de la fonction conditionnelle dans une méthode de garde ( executeIfEnabled) qui accepte l'action lambda, à laquelle le code à exécuter conditionnellement peut être passé.

Bien que dans votre cas, cette approche ne sauvera aucune ligne de code, en séchant cela, vous avez maintenant la possibilité de centraliser d'autres problèmes de basculement de fonctionnalités, ainsi que des problèmes AOP ou de débogage tels que la journalisation, les diagnostics, le profilage, etc.

Un avantage de l'utilisation de lambdas ici est que les fermetures peuvent être utilisées pour éviter d'avoir à surcharger la executeIfEnabledméthode.

Par exemple:

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

Avec un test:

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

Également similaire à l'approche de Damix, sans avoir besoin d'une interface et d'implémentations de classes anonymes avec remplacement de méthode.
StuartLC

2

Comme indiqué dans d'autres réponses, le modèle de conception de stratégie est un modèle de conception approprié à suivre pour simplifier ce code. Je l'ai illustré ici en utilisant l'invocation de méthode par réflexion, mais il existe un certain nombre de mécanismes que vous pouvez utiliser pour obtenir le même effet.

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

Avoir à faire face à la réflexion est un peu gênant, s'il y a déjà des classes qui le font pour vous, comme déjà mentionné Proxy.
SpaceTrucker

Comment peux-tu faire foo.fooIsEnabled ...? A priori, c'est un champ interne de l'objet, on ne peut pas, et on ne veut pas, le voir à l'extérieur.
Nicolas Barbulesco

2

Si seulement Java était un peu meilleur pour être fonctionnel. Il pense que la solution la plus OOO est de créer une classe qui encapsule une seule fonction, elle n'est donc appelée que lorsque foo est activé.

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

puis implémenter bar, bazet en battant que classes anonymes qui s'étendent 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
        }
    };
}

Une autre idée

Utilisez la solution nullable de glglgl mais les classes make FooImplet NullFoointernes (avec des constructeurs privés) de la classe ci-dessous:

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 cette façon, vous n'avez pas à vous soucier de vous souvenir d'utiliser (isFooEnabled ? foo : NullFoo.DEFAULT).


Dites que vous avez: Foo foo = new Foo()pour appeler barvous écririezfoo.bar.call()
Colin

1

Il semble que la classe ne fasse rien lorsque Foo n'est pas activé, alors pourquoi ne pas l'exprimer à un niveau supérieur où vous créez ou obtenez l'instance Foo?

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

Cela ne fonctionne que si isFooEnabled est une constante. Dans un cas général, vous pouvez créer votre propre annotation.


Konrad, pouvez-vous développer l'annotation?
Nicolas Barbulesco

Le code d'origine détermine si fooIsEnabledune méthode est appelée. Vous faites cela avant l'instanciation de Foo. C'est trop tôt. La valeur peut changer entre-temps.
Nicolas Barbulesco

Je pense que vous manquez un point. A priori, isFooEnabledest un champ d'instance d' Fooobjets.
Nicolas Barbulesco

1

Je ne connais pas la syntaxe Java. Supposons qu'en Java, il y a polymorphisme, propriété statique, classe abstraite et méthode:

    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 a dit que l' activation était une propriété statique de la classe?
Nicolas Barbulesco

Que veut new bar()dire?
Nicolas Barbulesco

En Java, nous écrivons une classe Nameavec une majuscule.
Nicolas Barbulesco

Cela nécessite de trop changer le code d'appel. Nous appelons une méthode normalement: bar(). Si vous avez besoin de modifier cela, vous êtes condamné.
Nicolas Barbulesco

1

Fondamentalement, vous avez un indicateur qui, s'il est défini, l'appel de fonction doit être ignoré. Je pense donc que ma solution serait idiote, mais la voici.

Foo foo = new Foo();

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

Voici l'implémentation d'un proxy simple, au cas où vous voudriez exécuter du code avant d'exécuter une fonction.

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 ()
    {
        // ..
    }
}

Pour votre premier bloc de code, vous avez besoin d'une méthode visible isEnabled(). A priori, l' activation est une cuisson interne Foo, non exposée.
Nicolas Barbulesco

Le code appelant ne peut pas et ne veut pas savoir si l'objet est activé .
Nicolas Barbulesco

0

Il existe une autre solution, en utilisant le délégué (pointeur sur la fonction). Vous pouvez avoir une méthode unique qui effectue d'abord la validation, puis appelle la méthode appropriée en fonction de la fonction (paramètre) à appeler. Code 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
Correct, il est marqué Java et c'est pour cela que je souligne et écrit "Code en C #" puisque je ne connais pas Java. Puisqu'il s'agit d'une question de Design Pattern, la langue n'est pas importante.
ehh

Oh! Je comprends, désolé pour cela, seulement essayé d'aider et de trouver une solution. Merci
ehh
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.