Czy istnieje elegancki sposób, aby każda metoda w klasie zaczynała się od określonego bloku kodu?

143

Mam klasę, w której każda metoda zaczyna się tak samo:

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

Czy istnieje dobry sposób na wymaganie (i miejmy nadzieję, że nie pisanie za każdym razem) fooIsEnabledczęści dla każdej metody publicznej w klasie?

Krystyna
źródło
45
Przyjrzyj się programowaniu zorientowanemu na aspekty (szczególnie przed uzyskaniem porady ).
Sotirios Delimanolis
15
Do ilu metod musisz używać tego schematu? Zanim przejdziesz na ścieżkę wprowadzania AOP, możesz rozważyć po prostu ten mały fragment powtórzonego kodu. Czasami małe kopiowanie i wklejanie jest najprostszym rozwiązaniem.
bhspencer
51
Podejrzewam, że twój przyszły opiekun byłby bardziej zadowolony z dodatkowej płyty kotłowej niż konieczność nauki frameworka AOP.
bhspencer
7
Jeśli każda metoda klasy musi robić to samo w swoich pierwszych wierszach kodu, mamy zły projekt.
Tulains Córdova
7
@ user1598390: Pytanie nie jest tutaj niezwiązane z tematem i nie ma nic w zakresie programistów, co czyni to pytanie szczególnie znaczącym.
Robert Harvey

Odpowiedzi:

90

Nie wiem o eleganckim, ale oto działająca implementacja wykorzystująca wbudowaną Javę, java.lang.reflect.Proxyktóra wymusza, że wszystkie wywołania metodFoo rozpoczynały się od sprawdzenia enabledstanu.

main metoda:

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 berło:

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

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

Jak zauważyli inni, wydaje się, że to przesada, jeśli masz tylko kilka metod, o które musisz się martwić.

To powiedziawszy, z pewnością są korzyści:

  • Osiągnięto pewien rozdział obaw, ponieważ Fooimplementacje metod nie muszą się martwić o enabledproblem przekrojowy sprawdzania. Zamiast tego kod metody musi tylko martwić się o jej podstawowy cel, nic więcej.
  • Niewinny programista nie może dodać nowej metody do Fooklasy i omyłkowo „zapomnieć” o dodaniu enabledczeku. Plikenabled sprawdzania jest automatycznie dziedziczone przez każdą nowo dodaną metodę.
  • Jeśli chcesz dodać kolejny problem przekrojowy lub jeśli chcesz ulepszyć enabled kontrolę, bardzo łatwo jest to zrobić bezpiecznie i w jednym miejscu.
  • To całkiem fajne, że możesz uzyskać takie zachowanie podobne do AOP dzięki wbudowanej funkcjonalności Java. Nie jesteś zmuszony do integracji innych frameworków Spring, chociaż z pewnością mogą one być również dobrymi opcjami.

Aby być uczciwym, niektóre z wad to:

  • Część kodu implementacji obsługującego wywołania proxy jest brzydka. Niektórzy powiedzieliby również, że posiadanie klas wewnętrznych zapobiegających tworzeniu instancji FooImplklasy jest brzydkie.
  • Jeśli chcesz dodać nową metodę do Foo , musisz dokonać zmiany w 2 miejscach: klasie implementacji i interfejsie. Nic wielkiego, ale to jeszcze trochę więcej pracy.
  • Wywołania proxy nie są bezpłatne. Istnieje pewne obciążenie związane z wydajnością. Jednak w przypadku ogólnego użytku nie będzie to zauważalne. Więcej informacji znajdziesz tutaj .

EDYTOWAĆ:

Komentarz Fabiana Streitela sprawił, że pomyślałem o 2 kłopotach z moim powyższym rozwiązaniem, które, przyznaję, nie jestem zadowolony z siebie:

  1. Procedura obsługi wywołań używa magicznych łańcuchów do pominięcia „enabled-check” w metodach „getEnabled” i „setEnabled”. Może to łatwo się zepsuć, jeśli nazwy metod zostaną refaktoryzowane.
  2. Jeśli byłby przypadek, w którym trzeba dodać nowe metody, które nie powinny dziedziczyć zachowania „enabled-check”, programista może łatwo popełnić błąd, a przynajmniej oznaczałoby to dodanie większej ilości magii smyczki.

Aby rozwiązać punkt 1 i przynajmniej złagodzić problem z punktem 2, utworzyłbym adnotację BypassCheck(lub coś podobnego), której mógłbym użyć do oznaczenia metod wFoo interfejsie, dla których nie chcę wykonywać polecenia „ włączona kontrola ”. W ten sposób nie potrzebuję w ogóle magicznych ciągów i programiście znacznie łatwiej będzie poprawnie dodać nową metodę w tym szczególnym przypadku.

Korzystając z rozwiązania adnotacji, kod wyglądałby tak:

main metoda:

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

import java.lang.annotation.*;

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

Foo berło:

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

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
źródło
11
Rozumiem, że to sprytne rozwiązanie, ale czy naprawdę z tego skorzystasz?
bhspencer
1
to fajne obejście, używasz dynamicznego wzorca proxy do ozdabiania obiektu tym typowym zachowaniem, które można znaleźć na początku każdej metody.
Victor
11
@bhspencer: Bardzo uzasadnione pytanie. W rzeczywistości używałem go wiele razy do obsługi wyjątków, rejestrowania, obsługi transakcji itp. Przyznam, że w przypadku mniejszych klas wydaje się to przesadą i może tak być. Ale jeśli oczekuję, że klasa znacznie się rozwinie pod względem złożoności i chcę zapewnić spójne zachowanie we wszystkich metodach, gdy to robię, nie przeszkadza mi to rozwiązanie.
sstan
1
Nie być częścią 97% zła tutaj, ale jakie są konsekwencje wydajności tej klasy proxy?
corsiKa
5
@corsiKa: Dobre pytanie. Nie ma wątpliwości, że używanie dynamicznych serwerów proxy jest wolniejsze niż bezpośrednie wywołania metod. Jednak w przypadku ogólnego użytku narzut wydajności będzie niezauważalny. Powiązany wątek SO, jeśli jesteś zainteresowany: Koszt wydajności dynamicznego proxy Java
sstan
51

Jest wiele dobrych sugestii ... co możesz zrobić, aby rozwiązać swój problem, to przemyśleć Wzorzec Stanu i zaimplementować go.

Spójrz na ten fragment kodu ... być może doprowadzi Cię do pomysłu. W tym scenariuszu wygląda na to, że chcesz zmodyfikować całą implementację metod na podstawie wewnętrznego stanu obiektu. Proszę pamiętać, że suma metod w obiekcie jest określana jako zachowanie.

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

      }
}

Mam nadzieję że ci się spodoba!

PD: Jest implementacją Wzorca Stanu (znany również jako Strategia w zależności od kontekstu .. ale zasady są takie same).

Zwycięzca
źródło
1
OP nie chce powtarzać tego samego wiersza kodu na początku każdej metody, a Twoje rozwiązanie polega na powtarzaniu tego samego wiersza kodu na początku każdej metody.
Tulains Córdova
2
@ user1598390 nie ma potrzeby powtarzania oceny, wewnątrz zachowania FooEnabledBehaviour zakładasz, że klient tego obiektu ustawił fooEnabled na true, więc nie ma potrzeby sprawdzania. To samo dotyczy klasy FooDisabledBehaviour. Sprawdź ponownie, wpisz w nim kod.
Victor
2
Dzięki @ bayou.io, poczekajmy na odpowiedź OP. Myślę, że społeczność wykonuje tutaj świetną robotę, jest tu wiele dobrych wskazówek!
Victor
2
Zgadzam się z @dyesdyes. Nie wyobrażam sobie implementacji tego do niczego poza naprawdę trywialną klasą. Jest to zbyt problematyczne, biorąc pod uwagę, że bar()in FooEnabledBehaviori bar()in FooDisabledBehaviormogą mieć wiele tego samego kodu, być może nawet o jedną linię różną między nimi. Mógłbyś bardzo łatwo, zwłaszcza jeśli ten kod byłby utrzymywany przez młodszych programistów (takich jak ja), skończyć z gigantycznym bałaganem, który jest nie do utrzymania i nie do przetestowania. Może się to zdarzyć w przypadku każdego kodu, ale wydaje się, że tak łatwo jest szybko zepsuć. +1 jednak, bo dobra sugestia.
Chris Cirefice
1
Mmmmm ... nie, chłopaki ... ale najpierw dzięki za komentarze. Dla mnie rozmiar kodu nie stanowi problemu, o ile jest „czysty” i „czytelny”. Na swoją korzyść powinienem argumentować, że nie używam żadnej zewnętrznej klasy ... która powinna uczynić rzeczy bardziej dostępnymi. A jeśli występuje jakieś typowe zachowanie, hermetyzujmy je w klasie CommonBehaviourClass i delegujmy tam, gdzie jest to potrzebne. W książce GOF (nie moja ulubiona książka do nauki, ale ma dobre przepisy) znalazłeś taki przykład: en.wikipedia.org/wiki/… . To mniej więcej to samo, co tutaj robię.
Victor
14

Tak, ale to trochę pracy, więc to zależy od tego, jak ważne jest to dla Ciebie.

Możesz zdefiniować klasę jako interfejs, napisać implementację delegata, a następnie użyć java.lang.reflect.Proxydo zaimplementowania interfejsu z metodami, które wykonują część udostępnioną, a następnie warunkowo wywołać delegata.

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

Twój MyInvocationHandlermoże wyglądać mniej więcej tak (pominięto obsługę błędów i tworzenie szkieletów klas, zakładając, że fooIsEnabledjest zdefiniowane w dostępnym miejscu):

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

To nie jest niesamowicie ładne. Ale w przeciwieństwie do różnych komentatorów, zrobiłbym to, ponieważ myślę, że powtarzanie jest ważniejszym ryzykiem niż tego rodzaju gęstość, i będziesz w stanie stworzyć "odczucie" swojej prawdziwej klasy, z tym nieco nieodgadnionym opakowaniem dodanym bardzo lokalnie w zaledwie kilku wierszach kodu.

Zobacz dokumentację Java aby uzyskać szczegółowe informacje na temat dynamicznych klas proxy.

David P. Caldwell
źródło
14

To pytanie jest ściśle związane z programowaniem aspektowym . AspectJ jest rozszerzeniem AOP języka Java i możesz rzucić okiem, aby uzyskać trochę ispiracji.

O ile wiem, nie ma bezpośredniego wsparcia dla AOP w Javie. Jest kilka wzorców GOF, które się z nim wiążą, jak na przykład Template Method i Strategy, ale tak naprawdę nie zaoszczędzą one linii kodu.

W Javie i większości innych języków można zdefiniować powtarzającą się logikę, której potrzebujesz w funkcjach i zastosować tak zwane zdyscyplinowane podejście do kodowania, w którym wywołujesz je we właściwym czasie.

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

Jednak to nie pasowałoby do twojego przypadku, ponieważ chciałbyś, aby kod wyodrębniony z faktury mógł powrócić z checkBalance. W językach, które obsługują makra (jak C / C ++), można zdefiniować checkSomePreconditioni checkSomePostconditionjako makra, a zostaną one po prostu zastąpione przez preprocesor, zanim jeszcze zostanie wywołany kompilator:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

Java nie ma tego po wyjęciu z pudełka. Może to kogoś urazić, ale w przeszłości korzystałem z automatycznego generowania kodu i silników szablonów, aby zautomatyzować powtarzalne zadania związane z kodowaniem. Jeśli przetwarzasz pliki Java przed skompilowaniem ich za pomocą odpowiedniego preprocesora, na przykład Jinja2, możesz zrobić coś podobnego do tego, co jest możliwe w C.

Możliwe podejście w czystej Javie

Jeśli szukasz czystego rozwiązania w języku Java, to, co możesz znaleźć, prawdopodobnie nie będzie zwięzłe. Ale nadal może uwzględniać wspólne części programu i unikać powielania kodu i błędów. Mógłbyś zrobić coś takiego (jest to jakiś wzorzec inspirowany strategią ). Zwróć uwagę, że w C # i Javie 8 oraz w innych językach, w których funkcje są nieco łatwiejsze w obsłudze, takie podejście może faktycznie wyglądać ładnie.

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

Coś w rodzaju prawdziwego scenariusza

Tworzysz klasę do wysyłania ramek danych do robota przemysłowego. Wykonanie polecenia wymaga czasu. Po wykonaniu polecenia wysyła z powrotem ramkę sterującą. Robot może ulec uszkodzeniu, jeśli otrzyma nowe polecenie, podczas gdy poprzednie jest nadal wykonywane. Twój program używa DataLinkklasy do wysyłania i odbierania ramek do i od robota. Musisz zabezpieczyć dostęp doDataLink instancji.

Połączenia gwintowe interfejs użytkownika RobotController.left, right, uplub downgdy użytkownik kliknie przycisków, ale również wywołuje BaseController.tickw regularnych odstępach czasu, w celu przekazywania poleceń ponownie włączyć do prywatnej DataLinkinstancji.

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
źródło
4
Czy to nie tylko wyrzuca puszkę w dół drogi. To znaczy, że przyszły opiekun musiał pamiętać, aby if (! FooIsEnabled) zwracać; na początku każdej funkcji, a teraz muszą pamiętać o ochronie (nowy kod {... na początku każdej funkcji. Jak to pomaga?
bhspencer
Podoba mi się analiza i kontekst w tle damix911 ... zbudowałbym nowe instancje kodu w czasie kompilacji (używając prywatnych statycznych elementów członkowskich) zakładając, że kod nie zmieni się w czasie i zmieniam nazwę zmiany na "executeIf" przekazując jako argument Warunek (Podobnie jak klasa Predicate) i kod. Ale to jest bardziej osobisty smak i smak.
Victor
1
@bhspencer W Javie wygląda to raczej niezgrabnie iw większości przypadków ta strategia polega na przepracowaniu prostego kodu. Niewiele programów może skorzystać na takim schemacie. Fajną rzeczą jest jednak to, że stworzyliśmy nowy symbol, protectktóry jest łatwiejszy do ponownego wykorzystania i dokumentowania. Jeśli powiesz przyszłemu opiekunowi, że krytyczny kod powinien być chroniony protect, to już mówisz mu, co ma robić. Jeśli zasady ochrony ulegną zmianie, nowy kod jest nadal chroniony. To jest dokładnie uzasadnienie definiowania funkcji, ale OP musiał „zwrócić return”, czego funkcje nie mogą zrobić.
damix911
11

Rozważyłbym refaktoryzację. Ten wzór mocno łamie SUCHY wzór (nie powtarzaj tego). Wierzę, że to łamie tę klasową odpowiedzialność. Ale to zależy od twojej kontroli nad kodem. Twoje pytanie jest bardzo otwarte - gdzie dzwonisz do Fooinstancji?

Przypuszczam, że masz taki kod

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

może powinieneś to nazwać mniej więcej tak:

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

I utrzymuj to w czystości. Na przykład rejestrowanie:

this.logger.debug(createResourceExpensiveDump())

a logger nie zadaje sobie pytania , czy debugowanie jest włączone. Po prostu rejestruje.

Zamiast tego dzwoniąc do klasy, musisz to sprawdzić:

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

Jeśli to jest biblioteka i nie możesz kontrolować wywoływania tej klasy, wyrzuć, IllegalStateExceptionco wyjaśnia dlaczego, jeśli to wywołanie jest nielegalne i powoduje problemy.

Gondy
źródło
6
To zdecydowanie prostsze i przyjemniejsze dla oczu. Ale jeśli celem OP jest upewnienie się, że w miarę dodawania nowych metod włączona logika nie jest nigdy omijana, to refaktoryzacja nie ułatwia egzekwowania tego.
sstan
4
Również w przypadku twojego przykładu dziennika, powiedziałbym, że wymaga to dużo więcej powtórzeń - za każdym razem, gdy chcesz się zalogować, musisz sprawdzić, czy rejestrator jest włączony. Zwykle zapisuję więcej wierszy niż liczba metod na dowolnej klasie ...
T. Kiley,
4
To łamie modularność, ponieważ teraz wywołujący musi wiedzieć coś o wewnętrznych elementach foo (w tym przypadku, czy jest to fooEnabled). To jest klasyczny przykład, w którym przestrzeganie zasad najlepszych praktyk nie rozwiąże problemu, ponieważ reguły są sprzeczne. (Nadal mam nadzieję, że ktoś
wymyśli
2
Cóż, zależy to w dużej mierze od kontekstu, czy ma to sens.
Ian Goldby
3
Rejestrowanie jest dokładnie tym przykładem, w którym nie chcę powtórzeń w moim kodzie. Chcę tylko napisać LOG.debug ("...."); - A logger powinien sprawdzić, czy naprawdę chcę debugować. - Innym przykładem jest zamknięcie / czyszczenie. - Jeśli używam AutoClosable, nie chcę wyjątku, jeśli jest już zamknięty, powinien po prostu nic nie robić.
Falco
6

IMHO najbardziej eleganckim i najskuteczniejszym rozwiązaniem tego problemu jest posiadanie więcej niż jednej implementacji Foo, wraz z fabryczną metodą jej tworzenia:

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

Lub odmiana:

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

Jak wskazuje sstan, jeszcze lepiej byłoby użyć interfejsu:

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

Dostosuj to do reszty swoich okoliczności (czy za każdym razem tworzysz nowe Foo, czy ponownie używasz tej samej instancji itp.)

Pepijn Schmitz
źródło
1
To nie to samo, co odpowiedź Konrada. Podoba mi się to, ale myślę, że byłoby bezpieczniej, gdybyś zamiast korzystać z dziedziczenia klas używał interfejsu, jak sugerowali inni w swoich odpowiedziach. Powód jest prosty: programista może zbyt łatwo dodać metodę doFoo klasy rozszerzonej i zapomnieć o dodaniu wersji tej metody, która nie działa, pomijając w ten sposób pożądane zachowanie.
sstan
2
@sstan Masz rację, tak by było lepiej. Zrobiłem to w ten sposób, aby jak najmniej zmodyfikować oryginalny przykład Kristiny, aby uniknąć rozpraszania uwagi, ale jest to istotne. Dodam twoją sugestię do mojej odpowiedzi.
Pepijn Schmitz
1
Myślę, że przegapiłeś punkt. Ty określasz, czy isFooEnabledpodczas tworzenia instancji Foo. Jest za wcześnie. W oryginalnym kodzie odbywa się to po uruchomieniu metody. WartośćisFooEnabled może się w międzyczasie zmienić.
Nicolas Barbulesco
1
@NicolasBarbulesco Nie wiesz, że fooIsEnabled może być stałą. Lub może być używany w sposób, który sprawi, że będzie skutecznie stały. Lub można go ustawić w kilku dobrze zdefiniowanych miejscach, aby za każdym razem łatwo było uzyskać nową instancję Foo. Lub może być dopuszczalne pobieranie nowej instancji Foo za każdym razem, gdy jest ona używana. Nie wiesz, dlatego napisałem: „dostosuj to do reszty swoich okoliczności”.
Pepijn Schmitz
@PepijnSchmitz - Oczywiście fooIsEnabled może być stały. Ale nic nam nie mówi, że ma być stały. Rozważam więc przypadek ogólny.
Nicolas Barbulesco
5

Mam inne podejście: mam

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

}

i wtedy możesz to zrobić

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

Być może można nawet zastąpić isFooEnabledo Foozmiennej które albo przytrzymuje FooImplbyć używany lub NullFoo.DEFAULT. Wtedy połączenie jest znowu prostsze:

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

BTW, nazywa się to „wzorcem zerowym”.

glglgl
źródło
Ogólne podejście jest dobre, ale użycie wyrażenia (isFooEnabled ? foo : NullFoo.DEFAULT).bar();wydaje się nieco niezdarne. Miej trzecią implementację, która jest delegowana do jednej z istniejących implementacji. Zamiast zmieniać wartość pola, isFooEnabledmożna zmienić cel delegacji. Zmniejsza to liczbę oddziałów w kodzie
SpaceTrucker
1
Ale deportujesz wewnętrzne gotowanie klasy Foow kodzie wywoławczym! Skąd mogliśmy wiedzieć, czy isFooEnabled? To jest wewnętrzne pole w klasie Foo.
Nicolas Barbulesco
3

W podobnym funkcjonalnym podejściu do odpowiedzi @ Colina, z funkcjami lambda Java 8 , możliwe jest zawinięcie warunkowego przełączania funkcji włączania / wyłączania kodu do metody guard ( executeIfEnabled), która akceptuje lambdę akcji, do której kod może być warunkowo wykonany przeszedł.

Chociaż w twoim przypadku to podejście nie zapisuje żadnych wierszy kodu, poprzez suszenie tego, masz teraz możliwość scentralizowania innych problemów związanych z przełączaniem funkcji, a także problemów z AOP lub debugowaniem, takich jak logowanie, diagnostyka, profilowanie i in.

Jedną korzyścią z używania tutaj lambd jest to, że można użyć zamknięć, aby uniknąć konieczności przeciążania executeIfEnabled metody.

Na przykład:

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

Z testem:

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
źródło
Również podobne do podejścia Damix, bez konieczności stosowania interfejsu i anonimowych implementacji klas z przesłonięciem metody.
StuartLC,
2

Jak wskazano w innych odpowiedziach, wzorzec projektowania strategii jest odpowiednim wzorcem projektowym, aby uprościć ten kod. Zilustrowałem to tutaj za pomocą wywołania metody poprzez odbicie, ale istnieje wiele mechanizmów, których można użyć, aby uzyskać ten sam efekt.

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
źródło
Konieczność radzenia sobie z refleksją jest trochę niezręczna, jeśli są już zajęcia, które robią to za Ciebie, jak już wspomniane Proxy.
SpaceTrucker
Jak mogłeś to zrobić foo.fooIsEnabled ...? A priori, jest to wewnętrzne pole obiektu, którego nie możemy i nie chcemy widzieć na zewnątrz.
Nicolas Barbulesco
2

Gdyby tylko java była trochę lepsza w działaniu. Uważa się, że najlepszym rozwiązaniem OOO jest utworzenie klasy, która opakowuje pojedynczą funkcję, więc jest wywoływana tylko wtedy, gdy włączone jest foo.

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

a następnie wdrożenie bar, bazi batjak anonimowych klas, które rozciągają się 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
        }
    };
}

Inny pomysł

Użyj rozwiązania glglgl dopuszczającego wartość null, ale make FooImpli NullFooklasy wewnętrzne (z prywatnymi konstruktorami) poniższej klasy:

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

w ten sposób nie musisz martwić się o pamiętanie o użyciu (isFooEnabled ? foo : NullFoo.DEFAULT).

Colin
źródło
Powiedz, że masz: Foo foo = new Foo()aby zadzwonić bar, napiszfoo.bar.call()
Colin
1

Wygląda na to, że klasa nic nie robi, gdy Foo nie jest włączone, więc dlaczego nie wyrazić tego na wyższym poziomie, na którym tworzysz lub pobierasz instancję Foo?

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

Działa to tylko wtedy, gdy isFooEnabled jest stałą. W ogólnym przypadku możesz utworzyć własną adnotację.

Konrad Höffner
źródło
Konrad, czy możesz rozwinąć adnotację?
Nicolas Barbulesco
Oryginalny kod określa, czy fooIsEnabledwywołanie metody. Robisz to przed utworzeniem instancji Foo. Jest za wcześnie. Wartość może się w międzyczasie zmienić.
Nicolas Barbulesco
Myślę, że przegapiłeś punkt. A priori isFooEnabledjest polem instancji Fooobiektów.
Nicolas Barbulesco
1

Nie znam składni języka Java. Założenie, że w Javie istnieje polimorfizm, własność statyczna, abstrakcyjna klasa i metoda:

    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
źródło
Kto powiedział, że włączenie jest statyczną właściwością klasy?
Nicolas Barbulesco
Co to new bar()ma znaczyć?
Nicolas Barbulesco
W Javie piszemy klasę Namez dużej litery.
Nicolas Barbulesco
To wymaga zbyt dużej zmiany kodu wywołującego. Nazywamy metodę normalnie: bar(). Jeśli chcesz to zmienić, jesteś skazany na zagładę.
Nicolas Barbulesco
1

Zasadniczo masz flagę, że jeśli jest ustawiona, wywołanie funkcji powinno zostać pominięte. Więc myślę, że moje rozwiązanie byłoby głupie, ale oto ono.

Foo foo = new Foo();

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

Oto implementacja prostego serwera proxy, na wypadek gdybyś chciał wykonać jakiś kod przed wykonaniem jakiejkolwiek funkcji.

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
źródło
Do pierwszego bloku kodu potrzebujesz widocznej metody isEnabled(). A priori, bycie aktywnym oznacza wewnętrzne gotowanie Foo, a nie eksponowanie.
Nicolas Barbulesco
Kod wywołujący nie może i nie chce wiedzieć, czy obiekt jest włączony .
Nicolas Barbulesco
0

Istnieje inne rozwiązanie, wykorzystujące delegata (wskaźnik do funkcji). Możesz mieć unikalną metodę, która najpierw przeprowadza walidację, a następnie wywołuje odpowiednią metodę zgodnie z wywoływaną funkcją (parametrem). Kod 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");
    }
}
ehh
źródło
2
Prawidłowo, jest oznaczony jako Java i dlatego podkreślam i napisałem „Kod w C #”, ponieważ nie znam Javy. Ponieważ jest to kwestia wzorca projektowego, język nie jest ważny.
ehh
O! Rozumiem, przepraszam, tylko starałem się pomóc i znaleźć rozwiązanie. Dziękuję
ehh