Gibt es eine elegante Möglichkeit, jede Methode in einer Klasse mit einem bestimmten Codeblock zu beginnen?

143

Ich habe eine Klasse, in der jede Methode auf die gleiche Weise beginnt:

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

Gibt es eine gute Möglichkeit, den fooIsEnabledTeil für jede öffentliche Methode in der Klasse zu fordern (und hoffentlich nicht jedes Mal zu schreiben) ?

kristina
quelle
45
Schauen Sie sich die aspektorientierte Programmierung an (speziell vor dem Rat ).
Sotirios Delimanolis
15
Für wie viele Methoden müssen Sie dieses Boilerplate verwenden? Bevor Sie sich für die Einführung von AOP entscheiden, sollten Sie in Betracht ziehen, nur dieses kleine Stück wiederholten Codes zu verwenden. Manchmal ist ein kleines Kopieren und Einfügen die einfachste Lösung.
Bhspencer
51
Ich vermute, Ihr zukünftiger Betreuer wäre mit der zusätzlichen Kesselplatte zufriedener, als ein AOP-Framework erlernen zu müssen.
Bhspencer
7
Wenn jede Methode der Klasse in ihren ersten Codezeilen genau dasselbe tun muss, haben wir ein schlechtes Design.
Tulains Córdova
7
@ user1598390: Die Frage ist hier nicht vom Thema abweichend, und nichts am Umfang der Programmierer macht die Frage dort besonders aussagekräftig.
Robert Harvey

Antworten:

90

Ich weiß ja nicht , elegant, aber hier ist eine funktionierende Implementierung mit Hilfe von Java-internen java.lang.reflect.Proxydass erzwingt , dass alle Methodenaufrufe auf Foozunächst die Überprüfung enabledZustand.

main Methode:

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

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

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

Wie andere bereits betont haben, scheint es ein Overkill für das zu sein, was Sie brauchen, wenn Sie nur eine Handvoll Methoden haben, über die Sie sich Sorgen machen müssen.

Das heißt, es gibt sicherlich Vorteile:

  • Eine gewisse Trennung der Bedenken wird erreicht, da sich Foodie Methodenimplementierungen nicht um die enabledBedenken hinsichtlich der Prüfung kümmern müssen . Stattdessen muss sich der Code der Methode nur um den Hauptzweck der Methode kümmern, nicht mehr.
  • Es gibt keine Möglichkeit für einen unschuldigen Entwickler, der FooKlasse eine neue Methode hinzuzufügen und fälschlicherweise zu vergessen, den enabledScheck hinzuzufügen . Das enabledÜberprüfungsverhalten wird automatisch von jeder neu hinzugefügten Methode übernommen.
  • Wenn Sie ein weiteres Querschnittsthema hinzufügen oder die enabledPrüfung verbessern müssen , ist dies sehr einfach, sicher und an einem Ort.
  • Es ist nett, dass Sie dieses AOP-ähnliche Verhalten mit integrierter Java-Funktionalität erhalten können. Sie sind nicht gezwungen, ein anderes Framework wie dieses zu integrieren Spring, obwohl dies definitiv auch eine gute Option sein kann.

Um fair zu sein, sind einige der Nachteile:

  • Ein Teil des Implementierungscodes, der die Proxy-Aufrufe verarbeitet, ist hässlich. Einige würden auch sagen, dass FooImples hässlich ist , innere Klassen zu haben, um die Instanziierung der Klasse zu verhindern .
  • Wenn Sie eine neue Methode hinzufügen möchten Foo, müssen Sie eine Änderung an zwei Stellen vornehmen: der Implementierungsklasse und der Schnittstelle. Keine große Sache, aber es ist noch ein bisschen mehr Arbeit.
  • Proxy-Aufrufe sind nicht kostenlos. Es gibt einen gewissen Leistungsaufwand. Für den allgemeinen Gebrauch wird es jedoch nicht bemerkt. Sehen Sie hier für weitere Informationen.

BEARBEITEN:

Fabian Streitels Kommentar brachte mich dazu, über zwei Ärger mit meiner obigen Lösung nachzudenken, die ich zugeben muss, dass ich nicht glücklich über mich selbst bin:

  1. Der Aufrufhandler verwendet magische Zeichenfolgen, um die "Aktivierungsprüfung" für die Methoden "getEnabled" und "setEnabled" zu überspringen. Dies kann leicht zu Problemen führen, wenn die Methodennamen überarbeitet werden.
  2. Wenn es einen Fall gibt, in dem neue Methoden hinzugefügt werden müssen, die das Verhalten "Aktivierte Prüfung" nicht erben sollen, kann es für den Entwickler ziemlich einfach sein, dies falsch zu verstehen, und zumindest würde dies bedeuten, mehr Magie hinzuzufügen Saiten.

Um Punkt 1 zu lösen und das Problem mit Punkt 2 zumindest zu lösen, würde ich eine Anmerkung BypassCheck(oder etwas Ähnliches) erstellen , mit der ich die Methoden in der FooSchnittstelle markieren könnte, für die ich das " aktivierte Prüfung ". Auf diese Weise benötige ich überhaupt keine magischen Zeichenfolgen, und es wird für Entwickler viel einfacher, in diesem speziellen Fall eine neue Methode korrekt hinzuzufügen.

Bei Verwendung der Anmerkungslösung würde der Code folgendermaßen aussehen:

main Methode:

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

import java.lang.annotation.*;

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

Foo Schnittstelle:

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

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);
        }
    }
}
sstan
quelle
11
Ich verstehe, dass dies eine clevere Lösung ist, aber würden Sie das wirklich nutzen?
Bhspencer
1
ist eine nette Problemumgehung. Sie verwenden das dynamische Proxy-Muster, um das Objekt mit diesem allgemeinen Verhalten zu dekorieren, das zu Beginn jeder Methode zu finden ist.
Victor
11
@bhspencer: Eine sehr legitime Frage. Ich habe es tatsächlich oft verwendet, um Ausnahmebehandlung, Protokollierung, Transaktionsbehandlung usw. durchzuführen. Ich gebe zu, dass es für kleinere Klassen wie ein Overkill erscheint und sehr gut sein kann. Wenn ich jedoch erwarte, dass die Komplexität der Klasse stark zunimmt und ich dabei ein konsistentes Verhalten über alle Methoden hinweg sicherstellen möchte, macht mir diese Lösung nichts aus.
Sstan
1
Nicht Teil der 97% des Bösen hier zu sein, aber was sind die Auswirkungen dieser Proxy-Klasse auf die Leistung?
CorsiKa
5
@corsiKa: Gute Frage. Es besteht kein Zweifel, dass die Verwendung dynamischer Proxys langsamer ist als direkte Methodenaufrufe. Für den allgemeinen Gebrauch ist der Leistungsaufwand jedoch nicht wahrnehmbar. Verwandter SO-Thread, wenn Sie interessiert sind: Leistungskosten des dynamischen Java-Proxys
Stand
51

Es gibt viele gute Vorschläge. Was Sie tun können, um Ihr Problem zu lösen, ist, im Staatsmuster zu denken und es umzusetzen.

Schauen Sie sich dieses Code-Snippet an. Vielleicht bringt es Sie auf eine Idee. In diesem Szenario möchten Sie die gesamte Methodenimplementierung basierend auf dem internen Status des Objekts ändern. Bitte denken Sie daran, dass die Summe der Methoden in einem Objekt als Verhalten bezeichnet wird.

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

      }
}

Hoffe du magst es!

PD: Ist eine Implementierung des Staatsmusters (wird je nach Kontext auch als Strategie bezeichnet. Die Prinzipien sind jedoch dieselben).

Sieger
quelle
1
OP möchte nicht zu Beginn jeder Methode dieselbe Codezeile wiederholen müssen, und Ihre Lösung besteht darin, zu Beginn jeder Methode dieselbe Codezeile zu wiederholen.
Tulains Córdova
2
@ user1598390 Es ist nicht erforderlich, die Auswertung zu wiederholen. In FooEnabledBehaviour wird davon ausgegangen, dass der Client dieses Objekts fooEnabled auf true gesetzt hat, sodass keine Überprüfung erforderlich ist. Gleiches gilt für die FooDisabledBehaviour-Klasse. Überprüfen Sie erneut, Code darin.
Victor
2
Danke @ bayou.io, lass uns bis zur OP-Antwort warten. Ich denke, dass die Community hier einen verdammt guten Job macht, hier gibt es viele gute Tipps!
Victor
2
Ich bin mit @dyesdyes einverstanden und kann mir nicht vorstellen, dies für etwas anderes als eine wirklich triviale Klasse zu implementieren. Es ist einfach zu problematisch, wenn man bedenkt, dass bar()in FooEnabledBehaviorund bar()in FooDisabledBehaviormöglicherweise viel von demselben Code verwendet wird, wobei sich möglicherweise sogar eine einzelne Zeile zwischen den beiden unterscheidet. Sie könnten sehr leicht, besonders wenn dieser Code von Junior-Entwicklern (wie mir) gepflegt würde, zu einem riesigen Durcheinander von Mist führen, der nicht zu warten und nicht zu testen ist. Das kann mit jedem Code passieren, aber es scheint so einfach zu sein, es sehr schnell zu vermasseln. +1 aber, weil guter Vorschlag.
Chris Cirefice
1
Mmmmm ... ich nicht, Leute ... aber zuerst danke für die Kommentare. Für mich ist die Größe des Codes kein Problem, solange er "sauber" und "lesbar" ist. Zu meinen Gunsten sollte ich argumentieren, dass ich keine externe Klasse benutze. Das sollte die Dinge zugänglicher machen. Und wenn es ein allgemeines Verhalten gibt, kapseln wir es in eine CommonBehaviourClass und delegieren es an den Ort, an dem es benötigt wird. Im GOF-Buch (nicht mein Lieblingsbuch zum Lernen, aber mit guten Rezepten) haben Sie dieses Beispiel gefunden: en.wikipedia.org/wiki/… . Ist mehr oder weniger das gleiche, was ich hier mache.
Victor
14

Ja, aber es ist ein bisschen Arbeit, also hängt es davon ab, wie wichtig es für Sie ist.

Sie können die Klasse als Schnittstelle definieren, eine Delegatenimplementierung schreiben und dann java.lang.reflect.Proxydie Schnittstelle mit Methoden implementieren, die den gemeinsam genutzten Teil ausführen, und dann den Delegaten bedingt aufrufen.

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

Sie MyInvocationHandlerkönnen ungefähr so ​​aussehen (Fehlerbehandlung und Klassengerüst entfallen, vorausgesetzt, sie fooIsEnabledsind an einem zugänglichen Ort definiert):

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

Es ist nicht unglaublich hübsch. Aber im Gegensatz zu verschiedenen Kommentatoren würde ich es tun, da ich denke, dass Wiederholung ein wichtigeres Risiko ist als diese Art von Dichte, und Sie werden in der Lage sein, das "Gefühl" Ihrer realen Klasse zu erzeugen, wenn dieser etwas unergründliche Wrapper hinzugefügt wird sehr lokal in nur wenigen Codezeilen.

Weitere Informationen zu dynamischen Proxy-Klassen finden Sie in der Java-Dokumentation .

David P. Caldwell
quelle
14

Diese Frage steht in engem Zusammenhang mit der aspektorientierten Programmierung . AspectJ ist eine AOP-Erweiterung von Java, und Sie können einen Blick darauf werfen, um etwas Inspiration zu erhalten.

Soweit ich weiß, gibt es in Java keine direkte Unterstützung für AOP. Es gibt einige GOF-Muster, die sich darauf beziehen, wie zum Beispiel Vorlagenmethode und Strategie, aber es werden Sie nicht wirklich Codezeilen speichern.

In Java und den meisten anderen Sprachen können Sie die wiederkehrende Logik definieren, die Sie in Funktionen benötigen, und einen sogenannten disziplinierten Codierungsansatz anwenden, bei dem Sie sie zum richtigen Zeitpunkt aufrufen.

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

Dies würde jedoch nicht zu Ihrem Fall passen, da Sie möchten, dass der ausgerechnete Code zurückgegeben werden kann checkBalance. In Sprachen, die Makros unterstützen (wie C / C ++), können Sie diese als Makros definieren checkSomePreconditionund checkSomePostconditionsie werden einfach durch den Präprozessor ersetzt, bevor der Compiler überhaupt aufgerufen wird:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

Java hat dies nicht sofort einsatzbereit. Dies mag jemanden beleidigen, aber ich habe in der Vergangenheit automatische Codegenerierungs- und Vorlagen-Engines verwendet, um sich wiederholende Codierungsaufgaben zu automatisieren. Wenn Sie Ihre Java-Dateien verarbeiten, bevor Sie sie mit einem geeigneten Präprozessor kompilieren, z. B. Jinja2, können Sie etwas Ähnliches tun, wie es in C möglich ist.

Möglicher reiner Java-Ansatz

Wenn Sie nach einer reinen Java-Lösung suchen, wird das, was Sie möglicherweise finden, wahrscheinlich nicht präzise sein. Es könnte jedoch weiterhin allgemeine Teile Ihres Programms herausrechnen und Codeduplikationen und Fehler vermeiden. Sie könnten so etwas tun (es ist eine Art Strategie- inspiriertes Muster). Beachten Sie, dass dieser Ansatz in C # und Java 8 sowie in anderen Sprachen, in denen Funktionen etwas einfacher zu handhaben sind, möglicherweise tatsächlich gut aussieht.

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

Ein bisschen wie ein reales Szenario

Sie entwickeln eine Klasse zum Senden von Datenrahmen an einen Industrieroboter. Der Roboter braucht Zeit, um einen Befehl auszuführen. Sobald der Befehl abgeschlossen ist, sendet er Ihnen einen Kontrollrahmen zurück. Der Roboter kann beschädigt werden, wenn er einen neuen Befehl erhält, während der vorherige noch ausgeführt wird. Ihr Programm verwendet eine DataLinkKlasse zum Senden und Empfangen von Frames zum und vom Roboter. Sie müssen den Zugriff auf die DataLinkInstanz schützen .

Die Benutzeroberfläche Thread Anrufe RobotController.left, right, upoder , downwenn der Benutzer klickt auf die Tasten, sondern ruft auch BaseController.tickin regelmäßigen Abständen, um zu reaktivieren Befehlsweiterleitung an die privaten DataLinkInstanz.

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 */);
        }});
    }
}
damix911
quelle
4
Tritt das nicht einfach die Dose die Straße runter? Das heißt, der zukünftige Betreuer musste sich früher daran erinnern, ob (! FooIsEnabled) zurückkehrt. zu Beginn jeder Funktion und jetzt müssen sie daran denken, zu schützen (neuer Code {... zu Beginn jeder Funktion. Wie hilft das?
bhspencer
Ich mag Sie Analyse und Hintergrundkontext damix911 ... Ich würde die neuen Code-Instanzen zur Kompilierungszeit (unter Verwendung privater statischer Mitglieder) erstellen, vorausgesetzt, dass sich der Code im Laufe der Zeit nicht ändert, und die Änderung in "executeIf" umbenennen, wobei als Argument eine Bedingung übergeben wird (Wie Prädikatklasse) und der Code. Aber das ist persönlicheres Fällen und Geschmack.
Victor
1
@bhspencer In Java sieht es ziemlich ungeschickt aus, und in den meisten Fällen ist diese Strategie wirklich eine Überentwicklung von ansonsten einfachem Code. Nicht viele Programme können von einem solchen Muster profitieren. Eine coole Sache ist jedoch, dass wir ein neues Symbol erstellt haben, protectdas einfacher wiederzuverwenden und zu dokumentieren ist. Wenn Sie dem zukünftigen Betreuer mitteilen, dass kritischer Code geschützt werden soll protect, sagen Sie ihm bereits, was zu tun ist. Wenn sich die Schutzregeln ändern, ist der neue Code weiterhin geschützt. Dies ist genau die Begründung für die Definition von Funktionen, aber das OP musste ein "zurückgeben return", was Funktionen nicht können.
Damix911
11

Ich würde ein Refactoring in Betracht ziehen. Dieses Muster bricht das DRY-Muster stark (Wiederholen Sie sich nicht). Ich glaube, das bricht diese Klassenverantwortung. Dies hängt jedoch von Ihrer Code-Kontrolle ab. Ihre Frage ist sehr offen - wo rufen Sie die FooInstanz an?

Ich nehme an, Sie haben Code wie

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

Vielleicht solltest du es so nennen:

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

Und halte es sauber. Zum Beispiel Protokollierung:

this.logger.debug(createResourceExpensiveDump())

a logger fragt sich nicht , ob das Debuggen aktiviert ist. Es protokolliert nur.

Stattdessen muss die aufrufende Klasse Folgendes überprüfen:

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

Wenn dies eine Bibliothek ist und Sie den Aufruf dieser Klasse nicht steuern können, werfen Sie eine, IllegalStateExceptiondie erklärt, warum, wenn dieser Aufruf illegal ist und Probleme verursacht.

Gondy
quelle
6
Es ist definitiv einfacher und leichter für die Augen. Wenn das Ziel von OP jedoch darin besteht, sicherzustellen, dass beim Hinzufügen neuer Methoden die aktivierte Logik niemals umgangen wird, macht es dieses Refactoring nicht einfacher, dies durchzusetzen.
Sstan
4
Auch für Ihr Protokollbeispiel würde ich sagen, dass dies viel mehr Wiederholungen beinhaltet - jedes Mal, wenn Sie protokollieren möchten, müssen Sie überprüfen, ob der Logger aktiviert ist. Ich neige dazu, mehr Zeilen als die Anzahl der Methoden in einer Klasse zu protokollieren ...
T. Kiley
4
Dies unterbricht die Modularität, da der Anrufer nun etwas über die Interna von foo wissen muss (in diesem Fall, ob es fooEnabled ist). Dies ist ein klassisches Beispiel, bei dem das Befolgen der Best-Practice-Regeln das Problem nicht löst, da die Regeln in Konflikt stehen. (Ich hoffe immer noch, dass jemand eine Antwort "Warum habe ich nicht daran gedacht?" Einbringt.)
Ian Goldby
2
Nun, es hängt stark vom Kontext ab, ob es sinnvoll ist.
Ian Goldby
3
Die Protokollierung ist genau das Beispiel, bei dem ich keine Wiederholung in meinem Code möchte. Ich möchte nur LOG.debug schreiben ("...."); - Und der Logger sollte prüfen, ob ich wirklich debuggen möchte. - Ein weiteres Beispiel ist das Schließen / Bereinigen. - Wenn ich AutoClosable verwende, möchte ich keine Ausnahme, wenn es bereits geschlossen ist. Es sollte einfach nichts tun.
Falco
6

IMHO ist die eleganteste und leistungsstärkste Lösung dafür, mehr als eine Implementierung von Foo zusammen mit einer Factory-Methode zum Erstellen einer zu haben:

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

Oder eine Variation:

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

Wie sstan betont, wäre es noch besser, eine Schnittstelle zu verwenden:

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

Passen Sie dies an den Rest Ihrer Umstände an (erstellen Sie jedes Mal ein neues Foo oder verwenden Sie dieselbe Instanz wieder usw.)

Pepijn Schmitz
quelle
1
Dies ist nicht dasselbe wie Konrads Antwort. Ich mag das, aber ich denke, es wäre sicherer, wenn Sie anstelle der Klassenvererbung eine Schnittstelle verwenden würden, wie andere in ihren Antworten vorgeschlagen haben. Der Grund ist einfach: Es ist für einen Entwickler zu einfach, eine Methode zu Fooder erweiterten Klasse hinzuzufügen und zu vergessen, die No-Op-Version der Methode hinzuzufügen, wodurch das gewünschte Verhalten umgangen wird.
Sstan
2
@sstan Du hast recht, das wäre besser. Ich habe es so gemacht, um Kristinas ursprüngliches Beispiel so wenig wie möglich zu modifizieren, um Ablenkungen zu vermeiden, aber das ist relevant. Ich werde Ihren Vorschlag zu meiner Antwort hinzufügen.
Pepijn Schmitz
1
Ich denke, Sie verpassen einen Punkt. Sie bestimmen, ob isFooEnabledbei der Instanziierung von Foo. Es ist zu früh. Im ursprünglichen Code erfolgt dies, wenn eine Methode ausgeführt wird. Der Wert von isFooEnabledkann sich in der Zwischenzeit ändern.
Nicolas Barbulesco
1
@NicolasBarbulesco Das weißt du nicht, fooIsEnabled könnte eine Konstante sein. Oder es könnte so verwendet werden, dass es effektiv konstant bleibt. Oder es könnte an einigen gut definierten Stellen eingestellt werden, so dass es einfach ist, jedes Mal eine neue Instanz von Foo zu erhalten. Oder es könnte akzeptabel sein, jedes Mal, wenn es verwendet wird, eine neue Instanz von Foo zu erhalten. Sie wissen nicht, deshalb habe ich geschrieben: "Passen Sie dies an den Rest Ihrer Umstände an."
Pepijn Schmitz
@PepijnSchmitz - Natürlich fooIsEnabled kann konstant sein. Aber nichts sagt uns, dass es konstant ist. Ich betrachte also den allgemeinen Fall.
Nicolas Barbulesco
5

Ich habe einen anderen Ansatz: habe eine

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

}}

und dann kannst du tun

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

Vielleicht können Sie das sogar durch isFooEnabledeine FooVariable ersetzen , die entweder das FooImplzu verwendende oder das enthält NullFoo.DEFAULT. Dann ist der Anruf wieder einfacher:

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

Übrigens wird dies als "Nullmuster" bezeichnet.

glglgl
quelle
Der allgemeine Ansatz ist gut, aber die Verwendung des Ausdrucks (isFooEnabled ? foo : NullFoo.DEFAULT).bar();scheint etwas ungeschickt. Haben Sie eine dritte Implementierung, die an eine der vorhandenen Implementierungen delegiert. Anstatt den Wert eines Feldes isFooEnabledzu ändern, könnte das Delegierungsziel geändert werden. Dies reduziert die Anzahl der Zweige im Code
SpaceTrucker
1
Aber Sie deportieren das interne Kochen der Klasse Fooim Aufrufcode! Wie können wir wissen, ob isFooEnabled? Dies ist ein internes Feld in der Klasse Foo.
Nicolas Barbulesco
3

In einem ähnlichen funktionalen Ansatz wie bei @ Colins Antwort ist es mit den Lambda-Funktionen von Java 8 möglich, den Code zum Aktivieren / Deaktivieren der bedingten Funktion in eine Guard-Methode ( executeIfEnabled) zu packen, die die Aktion Lambda akzeptiert, für die der bedingt auszuführende Code verwendet werden kann bestanden.

Obwohl in diesem Fall bei diesem Ansatz keine Codezeilen gespeichert werden, haben Sie jetzt die Möglichkeit, andere Probleme beim Umschalten von Funktionen sowie AOP- oder Debugging-Probleme wie Protokollierung, Diagnose, Profilerstellung usw. zu zentralisieren.

Ein Vorteil der Verwendung von Lambdas besteht darin, dass Verschlüsse verwendet werden können, um zu vermeiden, dass die executeIfEnabledMethode überlastet werden muss.

Beispielsweise:

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

Mit einem 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();
}
StuartLC
quelle
Ähnlich wie bei Damix, ohne dass eine Schnittstelle und anonyme Klassenimplementierungen mit Methodenüberschreibung erforderlich sind.
StuartLC
2

Wie in anderen Antworten ausgeführt, ist das Strategie-Entwurfsmuster ein geeignetes Entwurfsmuster, um diesen Code zu vereinfachen. Ich habe es hier mithilfe des Methodenaufrufs durch Reflexion veranschaulicht, aber es gibt eine beliebige Anzahl von Mechanismen, mit denen Sie den gleichen Effekt erzielen können.

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...
  }
}
LJ2
quelle
Sich mit Reflexion auseinandersetzen zu müssen, ist etwas umständlich, wenn es bereits Klassen gibt, die dies für Sie tun, wie die bereits erwähnten Proxy.
SpaceTrucker
Wie kannst du das machen foo.fooIsEnabled ...? A priori ist dies ein internes Feld des Objekts, wir können und wollen es nicht draußen sehen.
Nicolas Barbulesco
2

Wenn nur Java ein bisschen besser darin wäre, funktionsfähig zu sein. Es wird davon ausgegangen, dass die OOO-Lösung darin besteht, eine Klasse zu erstellen, die eine einzelne Funktion umschließt, sodass sie nur aufgerufen wird, wenn foo aktiviert ist.

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

und dann implementieren bar, bazund batals anonyme Klassen, dass erweitern 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
        }
    };
}

Eine andere Idee

Verwenden Sie die nullbare Lösung von glglgl, aber machen Sie FooImplund NullFooinnere Klassen (mit privaten Konstruktoren) der folgenden Klasse:

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

Auf diese Weise müssen Sie sich keine Gedanken mehr über die Verwendung machen (isFooEnabled ? foo : NullFoo.DEFAULT).

Colin
quelle
Sagen Sie, Sie haben: Foo foo = new Foo()zu rufen bar, würden Sie schreibenfoo.bar.call()
Colin
1

Es scheint, als würde die Klasse nichts tun, wenn Foo nicht aktiviert ist. Warum also nicht auf einer höheren Ebene ausdrücken, auf der Sie die Foo-Instanz erstellen oder abrufen?

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

Dies funktioniert jedoch nur, wenn isFooEnabled eine Konstante ist. Im Allgemeinen können Sie Ihre eigene Anmerkung erstellen.

Konrad Höffner
quelle
Konrad, können Sie die Annotation weiterentwickeln?
Nicolas Barbulesco
Der ursprüngliche Code bestimmt, ob fooIsEnabledbeim Aufrufen einer Methode. Sie tun dies vor der Instanziierung von Foo. Es ist zu früh. Der Wert kann sich in der Zwischenzeit ändern.
Nicolas Barbulesco
Ich denke, Sie verpassen einen Punkt. A priori isFooEnabledist ein Instanzfeld von FooObjekten.
Nicolas Barbulesco
1

Ich bin mit der Java-Syntax nicht vertraut. Annahme, dass es in Java Polymorphismus, statische Eigenschaften, abstrakte Klasse und Methode gibt:

    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
        }
    }
ehh
quelle
Wer sagt, dass das Aktivieren eine statische Eigenschaft der Klasse ist?
Nicolas Barbulesco
Was new bar()soll das heißen?
Nicolas Barbulesco
In Java schreiben wir eine Klasse Namemit einem Großbuchstaben.
Nicolas Barbulesco
Dies erfordert eine zu starke Änderung des aufrufenden Codes. Wir nennen eine Methode normalerweise : bar(). Wenn Sie das ändern müssen, sind Sie zum Scheitern verurteilt.
Nicolas Barbulesco
1

Grundsätzlich haben Sie ein Flag, dass der Funktionsaufruf übersprungen werden sollte, wenn er gesetzt ist. Ich denke, meine Lösung wäre dumm, aber hier ist sie.

Foo foo = new Foo();

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

Hier ist die Implementierung eines einfachen Proxys, falls Sie vor dem Ausführen einer Funktion Code ausführen möchten.

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 ()
    {
        // ..
    }
}
Khaled.K
quelle
Für Ihren ersten Codeblock benötigen Sie eine sichtbare Methode isEnabled(). A priori, aktiviert zu sein, ist internes Kochen von Foo, nicht ausgesetzt.
Nicolas Barbulesco
Der aufrufende Code kann und will nicht wissen, ob das Objekt aktiviert ist .
Nicolas Barbulesco
0

Es gibt eine andere Lösung, die delegate (Zeiger auf Funktion) verwendet. Sie können eine eindeutige Methode haben, die zuerst die Validierung durchführt und dann die entsprechende Methode entsprechend der aufzurufenden Funktion (Parameter) aufruft. C # -Code:

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");
    }
}
ehh
quelle
2
Richtig, es ist als Java markiert und deshalb betone und schreibe ich "Code in C #", da ich Java nicht kenne. Da es sich um eine Entwurfsmusterfrage handelt, ist die Sprache nicht wichtig.
ehh
Oh! Ich verstehe, tut mir leid, ich habe nur versucht zu helfen und eine Lösung zu finden. Vielen Dank
ehh