Чи є елегантний спосіб змусити кожен метод у класі починати з певного блоку коду?


143

У мене є клас, де кожен метод починається однаково:

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

Чи є приємний спосіб вимагати (і, сподіваюся, не писати кожного разу) fooIsEnabledчастини для кожного публічного методу в класі?


45
Перегляньте програмування, орієнтоване на аспекти (особливо перед порадою ).
Сотіріос Деліманоліс

15
Скільки методів ви повинні використовувати цю котельну плиту? Перш ніж спуститися з маршруту введення AOP, вам варто подумати про те, щоб повторити цей код. Іноді невелика копія та вставка є найпростішим рішенням.
bhspencer

51
Я підозрюю, що ваш майбутній супровідник буде щасливішим із додатковою плитою котла, ніж доводиться вивчати рамки AOP.
bhspencer

7
Якщо кожен метод класу повинен робити те саме, що в перших рядках коду, то у нас поганий дизайн.
Тулен Кордова

7
@ user1598390: Питання тут не поза темою, і нічого про сферу програмістів немає, що робить це особливо важливим.
Роберт Харві

Відповіді:


90

Я не знаю про елегантне, але ось робоча реалізація з використанням вбудованої Java, java.lang.reflect.Proxyяка примушує всі виклики методів Fooпочинатись з перевірки enabledстану.

main метод:

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 інтерфейс:

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 клас:

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

Як зазначали інші, це здається непосильним для того, що вам потрібно, якщо у вас є лише декілька методів, про які потрібно турбуватися.

Однак, безумовно, є переваги:

  • Досягається певне розмежування проблем, оскільки Fooвпровадження методів не повинно турбуватися про enabledвсебічну проблему перевірки. Натомість код методу потребує лише турбуватися про те, яка головна мета методу, не більше того.
  • Немає можливості невинного розробника додати новий метод до Fooкласу і помилково «забути» додати enabledчек. enabledПоведінка перевірки автоматично успадковуються будь-яким додається способом.
  • Якщо вам потрібно додати ще одну наскрізну проблему, або якщо вам потрібно підвищити enabledперевірку, це зробити дуже просто і безпечно в одному місці.
  • Дуже приємно, що ви можете отримати таку поведінку, як AOP, завдяки вбудованому функціоналу Java. Ви не змушені інтегрувати якісь інші рамки на кшталт Spring, хоча вони, безумовно, можуть бути і хорошими варіантами.

Справедливістю, деякі з мінусів є:

  • Деякий код реалізації, який обробляє виклики проксі, неприйнятний. Дехто також сказав, що мати внутрішні класи для запобігання інстанції FooImplкласу - це некрасиво.
  • Якщо ви хочете додати новий метод Foo, вам слід змінити два місця: клас реалізації та інтерфейс. Не велика справа, але все ж трохи більше роботи.
  • Викликання проксі-сервера не є безкоштовними. Існує певна продуктивність накладних витрат. Однак для загального використання це не помітно. Дивіться тут для отримання додаткової інформації.

Редагувати:

Коментар Фабіана Стрейтеля змусив мене замислитися над двома роздратуваннями щодо мого вищезазначеного рішення, яке, я визнаю, не задоволений собою:

  1. Обробник виклику використовує магічні рядки, щоб пропустити "увімкнено-перевірити" методами "getEnabled" та "setEnabled". Це легко зламається, якщо назви методів будуть відновлені.
  2. Якщо був випадок, коли потрібно додати нові методи, які не повинні успадковувати поведінку "включеної перевірки", тоді розробнику може бути досить просто помилитися, і, принаймні, це означатиме додати більше магії струни.

Щоб вирішити точку №1 і принаймні полегшити проблему з пунктом №2, я створив би примітку BypassCheck(або щось подібне), який я міг би використати для позначення методів в Fooінтерфейсі, для якого я не хочу виконувати " включена перевірка ". Таким чином, мені взагалі не потрібні магічні рядки, і розробнику стає набагато простіше правильно додати новий метод у цьому спеціальному випадку.

Використовуючи рішення для приміток, код виглядатиме так:

main метод:

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 примітка:

import java.lang.annotation.*;

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

Foo інтерфейс:

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 клас:

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
Я розумію, що це розумне рішення, але чи справді ви цим користуєтесь?
bhspencer

1
це хороший спосіб вирішення, ви використовуєте динамічний шаблон проксі-сервера, щоб прикрасити об'єкт із такою загальною поведінкою, знайденою на початку кожного методу.
Віктор

11
@bhspencer: дуже законне запитання. Я фактично використовував його багато разів, щоб робити обробку виключень, ведення журналів, обробку транзакцій і т. Д. Я визнаю, що для менших класів це здається завищеним, і це дуже може бути. Але якщо я очікую, що клас значно зросте у складності, і я хочу забезпечити послідовну поведінку в усіх методах, як це я роблю, я не проти цього рішення.
sstan

1
Не бути частиною 97% зла тут, але які наслідки для цього проксі-класу є ефективністю?
corsiKa

5
@corsiKa: Добре запитання. Немає сумнівів, що використання динамічних проксі-серверів відбувається повільніше, ніж прямі виклики методів. Однак для загального використання продуктивність накладних витрат буде непомітною. Пов’язана
тематика

51

Є багато хороших пропозицій. Що можна зробити, щоб вирішити свою проблему, - це продумати Державний зразок та реалізувати його.

Погляньте на цей фрагмент коду .. Можливо, це набере вас на думку. У цьому сценарії виглядає так, що ви хочете змінити всю реалізацію методів на основі внутрішнього стану об'єкта. Зверніть увагу, що сума методів у об'єкті знається як поведінка.

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

      }
}

Сподіваюся, вам сподобається!

ПД: Це реалізація Державного зразка (також відомий як Стратегія залежно від контексту .. але принципи точно такі ж).


1
OP хоче не повторювати один і той же рядок коду на початку кожного методу, і ваше рішення передбачає повторення одного і того ж рядка коду на початку кожного методу.
Тулен Кордова

2
@ user1598390 немає необхідності повторювати оцінку, всередині FooEnabledBehaviour ви припускаєте, що клієнт цього об'єкта встановив fooEnabled на істинне, тому немає необхідності в обробці. Те саме стосується класу FooDisabledBehaviour. Перевірте ще раз, введіть у ньому код.
Віктор

2
Дякуємо @ bayou.io, зачекаємо, поки відповідь ОП. Я думаю, що громада тут робить пекельну роботу, тут є багато хороших порад!
Віктор

2
За згодою з @dyesdyes, я не можу уявити, щоб реалізувати це для чого-небудь, крім справді тривіального класу. Це занадто проблематично, враховуючи, що bar()в FooEnabledBehaviorі bar()в FooDisabledBehaviorможе бути спільне багато одного і того ж коду, можливо, навіть один рядок, різний між ними. Ви могли б дуже легко, особливо якби цей код підтримували молодші розробники (такі, як я), в кінцевому підсумку з великим безладом безладу, яке неможливо домогтися і неможливо. Це може статися з будь-яким кодом, але це, здається, дуже легко викрутити дуже швидко. +1, хоча гарна пропозиція.
Chris Cirefice

1
Ммммм ... я не хлопці .. але спочатку дякую за коментарі. Для мене розмір коду не є проблемою, якщо він "чистий" і "читабельний". На мою користь я повинен стверджувати, що я не використовую жодного зовнішнього класу .. Це повинно зробити речі більш доступними. І якщо є якась загальна поведінка, давайте інкапсулюємо її у CommonBehaviourClass і делегуємо туди, де вона потрібна. У книзі GOF (не моя улюблена книга для навчання, але є хороші рецепти) ви знайшли такий приклад: en.wikipedia.org/wiki/… . Це більш-менш те саме, що я роблю тут.
Віктор

14

Так, але це трохи роботи, тому залежить, наскільки це важливо для вас.

Ви можете визначити клас як інтерфейс, написати реалізацію делегата, а потім використовувати java.lang.reflect.Proxyдля реалізації інтерфейсу методи, які виконують спільну частину, а потім умовно викликати делегата.

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

Ви MyInvocationHandlerможете виглядати приблизно так (обробка помилок та ліси класів опущені, якщо припустити, що fooIsEnabledвони визначені десь доступними):

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

Це не неймовірно красиво. Але на відміну від різних коментаторів, я б це зробив, оскільки я вважаю, що повторення є більш важливим ризиком, ніж такий тип щільності, і ви зможете створити "відчуття" вашого реального класу, завдяки цьому додається дещо непереборлива обгортка дуже локально всього за пару рядків коду.

Детальні відомості про динамічні класи проксі див. У документації Java .


14

Це питання тісно пов'язане з аспектно-орієнтованим програмуванням . AspectJ - це розширення AOP Java на AOP, і ви можете поглянути на це, щоб отримати деяку іспірацію.

Наскільки я знаю, немає прямої підтримки AOP на Java. Існують деякі шаблони GOF, які пов'язані з нею, наприклад, метод шаблону та стратегія шаблону, але він насправді не збереже рядки коду.

У Java та більшості інших мов ви можете визначити необхідну для функцій повторювану логіку та прийняти так званий дисциплінований підхід кодування, за допомогою якого ви викликаєте їх у потрібний час.

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

Однак це не відповідатиме вашому випадку, тому що ви хочете, щоб код, який вийшов із фактури, міг повернутися з нього checkBalance. На мовах, що підтримують макроси (наприклад, C / C ++), ви можете визначити checkSomePreconditionі checkSomePostconditionяк макроси, і вони будуть просто замінені препроцесором до того, як компілятор навіть буде викликаний:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

У Java цього немає. Це може образити когось, але я використовував автоматичне генерування коду та шаблонні двигуни для автоматизації повторюваних завдань кодування в минулому. Якщо ви обробляєте ваші Java-файли, перш ніж компілювати їх з відповідним препроцесором, наприклад, Jinja2, ви можете зробити щось подібне до того, що можливо в C.

Можливий чистий підхід до Java

Якщо ви шукаєте чисте рішення Java, те, що ви, можливо, знайдете, не буде стислим. Але це все ще може визначити загальні частини вашої програми та уникнути дублювання коду та помилок. Ви можете зробити щось подібне (це якась стратегія- натхненна модель). Зауважте, що в C # і Java 8, а також в інших мовах, якими функціями трохи легше керувати, такий підхід може насправді виглядати приємно.

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

Своєрідний сценарій у реальному світі

Ви розробляєте клас для надсилання кадрів даних промисловому роботу. Роботу потрібен час, щоб виконати команду. Після того, як команда буде виконана, вона поверне вам керуючий кадр назад. Робот може отримати пошкодження, якщо він отримає нову команду, поки виконується попередня. Ваша програма використовує DataLinkклас для надсилання та отримання кадрів на робот і з нього. Вам потрібно захистити доступ до DataLinkекземпляра.

Інтерфейс нитка виклики RobotController.left, right, upабо downколи користувач натискає на кнопки, а й виклики BaseController.tickчерез регулярні проміжки часу, для того , щоб експедиції повторного включення команди в приватному DataLinkвипадку.

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
Це не просто збиває банку по дорозі. Що означає, що раніше повинен був пам'ятати, якщо (! FooIsEnabled) повертається; на початку кожної функції, і тепер їм потрібно пам’ятати, щоб захистити (новий код {... на початку кожної функції. Як це допомагає?
bhspencer

Мені подобається ваш аналіз та фоновий контекст damix911 ... я буду будувати нові екземпляри коду під час компіляції (використовуючи приватні статичні члени), припускаючи, що код не зміниться з часом і перейменуватиме зміни в "ExecuteIf", передаючи як аргумент Умова (Як і клас предикатів) та Код. Але це більш особистий рубок і смак.
Віктор

1
@bhspencer На Java це виглядає досить незграбно, і в більшості випадків ця стратегія справді є завищеною інженерією простого коду. Не так багато програм можуть скористатися такою схемою. Холодна річ, що ми створили новий символ, protectякий простіше використовувати та документувати. Якщо ви скажете майбутньому технічному обслуговувачу, що критичний код повинен бути захищений protect, ви вже говорите йому, що робити. Якщо правила захисту змінюються, новий код все одно захищений. Це саме обґрунтування визначення функцій, але ОП, необхідне для "повернення return", яке функції не можуть виконувати.
damix911

11

Я б розглядав питання рефакторингу. Цей візерунок сильно порушує схему DRY (не повторюйте себе). Я вважаю, що це порушує цю класову відповідальність. Але це залежить від вашого контролю над кодом. Ваше питання дуже відкрите - куди ви називаєте Fooекземпляр?

Я гадаю, у вас є такий код

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

можливо, вам слід назвати це приблизно так:

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

І тримайте його в чистоті. Наприклад, ведення журналу:

this.logger.debug(createResourceExpensiveDump())

a logger не запитує себе , чи налагоджено налагодження. Це просто журнали.

Натомість для виклику класу потрібно перевірити це:

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

Якщо це бібліотека і ви не можете керувати викликом цього класу, киньте IllegalStateExceptionпояснення, чому пояснюється, якщо це дзвінок незаконне і спричинить проблеми.


6
Це напевно простіше і простіше на очі. Але якщо мета ОП - переконатися, що, додаючи нові методи, що увімкнена логіка ніколи не обходить стороною, тоді цей рефакторинг не спрощує її виконання.
sstan

4
Також для вашого прикладу журналу я б сказав, що це передбачає набагато більше повторень - кожен раз, коли ви хочете ввійти в систему, ви повинні перевірити, чи реєстратор включений. Я схильний записувати більше рядків, ніж кількість методів для будь-якого класу ...
Т. Кілі

4
Це порушує модульність, тому що зараз абонент повинен щось знати про внутрішні місця foo (у цьому випадку, чи є fooEnabled). Це класичний приклад, коли дотримання правил найкращої практики не вирішить проблему, оскільки правила суперечать. (Я все ще сподіваюся, що хтось придумає "чому я не придумав цього?",
Відповівши

2
Ну, це сильно залежить від контексту, наскільки це має сенс.
Ян Голдбі

3
Ведення журналу - саме той приклад, коли я не хочу повторювати свій код. Я просто хочу написати LOG.debug ("...."); - І лісоруб повинен перевірити, чи справді я хочу налагоджувати. - Ще один приклад - закриття / прибирання. - Якщо я використовую функцію автоматичного замикання, я не хочу винятку, якщо він уже закритий, він просто не повинен нічого робити.
Фалько

6

IMHO - це найелегантніше та найкраще рішення для цього - мати кілька реалізацій Foo разом із заводським методом створення одного:

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

Або варіант:

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

Як зазначає sstan, ще краще було б використовувати інтерфейс:

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

Адаптуйте це до решти своїх обставин (чи створюєте ви новий Foo кожен раз чи використовуєте той самий екземпляр тощо)


1
Це не те саме, що відповідь Конрада. Мені це подобається, але я думаю, що було б безпечніше, якби замість використання успадкування класів ви використовували інтерфейс, як пропонували інші у своїх відповідях. Причина проста: Розробнику занадто просто додати метод до Fooта забути додати неопераційну версію методу у розширений клас, обходячи таким чином бажану поведінку.
sstan

2
@sstan Ви праві, це було б краще. Я зробив це так, щоб якомога рідше змінити оригінальний приклад Крістіни, щоб уникнути відволікань, але це актуально. Я додам вашу пропозицію до своєї відповіді.
Pepijn Schmitz

1
Я думаю, ви пропустите точку. Ви визначаєте, чи isFooEnabledв момент встановлення Foo. Ще рано. У вихідному коді це робиться під час запуску методу. Значення isFooEnabledможе тим часом змінюватися.
Ніколя Барбулеско,

1
@NicolasBarbulesco Ви цього не знаєте, fooIsEnabled може бути постійною. Або його можна використовувати таким чином, що робить його ефективно постійним. Або його можна встановити в декількох чітко визначених місцях, щоб легко було щоразу отримувати новий екземпляр Foo. Або може бути прийнятним отримати кожен екземпляр Foo кожного разу, коли він використовується. Ви не знаєте, тому я написав: "адаптуйте це до решти своїх обставин".
Пепійн Шмітц

@ PepijnSchmitz - Звичайно, fooIsEnabled може бути постійним. Але ніщо не говорить про те, що воно буде постійним. Тому я вважаю загальну справу.
Ніколя Барбулеско,

5

У мене інший підхід: мати

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

}

і тоді ви можете зробити

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

Може бути , ви навіть можете замінити isFooEnabledз Fooзмінної , яка або утримує , FooImplщоб використовувати або NullFoo.DEFAULT. Тоді дзвінок знову простіший:

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

До речі, це називається "Нульова картина".


Підхід на загальному рівні хороший, але використання виразу (isFooEnabled ? foo : NullFoo.DEFAULT).bar();здається трохи незграбним. Майте третю реалізацію, яка делегує одну з існуючих реалізацій. Замість зміни значення поля isFooEnabledмішень делегування може бути змінена. Це зменшує кількість відділень у коді
SpaceTrucker

1
Але ви депортуєте внутрішнє приготування класу Fooу коду виклику! Як ми могли знати, чи isFooEnabled? Це внутрішнє поле в класі Foo.
Ніколя Барбулеско,

3

У аналогічному функціональному підході до відповіді @ Colin, за допомогою лямбда-функцій Java 8 , можна переключити умовний функцію перемикання включення / відключення коду на метод захисту (executeIfEnabled ), який приймає лямбда дії, до якого може бути умовно виконаний код пройшов.

Хоча у вашому випадку цей підхід не врятує жодних рядків коду, засушивши це, тепер у вас є можливість централізувати інші проблеми перемикання функцій, плюс AOP або проблеми налагодження, такі як реєстрація, діагностика, профілювання та ін.

Однією з переваг використання лямбдасів є те, що закриття можна використовувати, щоб уникнути необхідності перевантажувати executeIfEnabledметод.

Наприклад:

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

З тестом:

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

2

Як вказується в інших відповідях, для спрощення цього коду слід дотримуватися шаблону розробки стратегії . Я проілюстрував це тут, використовуючи виклик методу через рефлексію, але існує будь-яка кількість механізмів, які можна використати для отримання того ж ефекту.

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

Доводиться мати справу з рефлексією трохи незручно, якщо вже є класи, які роблять це для вас, як уже згадувалося Proxy.
SpaceTrucker

Як ви могли це зробити foo.fooIsEnabled ...? Апріорі, це внутрішнє поле об'єкта, ми не можемо і не хочемо бачити його зовні.
Ніколя Барбулеско,

2

Якби тільки ява трохи краще ставився до функціональності. Думається, що найважливішим рішенням OOO є створення класу, який охоплює одну функцію, тому вона викликається лише тоді, коли 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();
}

а потім реалізувати bar, bazі batяк анонімні класи , які , які проходять 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
        }
    };
}

Ще одна ідея

Використовуйте зворотне рішення glglgl, але складіть FooImplі NullFooвнутрішні класи (з приватними конструкторами) класу нижче:

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

таким чином вам не доведеться турбуватися про запам'ятовування (isFooEnabled ? foo : NullFoo.DEFAULT).


Скажіть, у вас є: Foo foo = new Foo()зателефонувати, barви б написалиfoo.bar.call()
Колін

1

Схоже, що клас нічого не робить, коли Foo не ввімкнено, то чому б не виразити це на більш високому рівні, де ви створюєте або отримуєте екземпляр Foo?

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

Це працює лише, якщо isFooEnabled є константою. У загальному випадку ви можете створити власну примітку.


Конраде, ти можеш розробити на анотації?
Ніколя Барбулеско,

Вихідний код визначає, чи fooIsEnabledвикликається метод. Ви робите це до отримання екземпляра Foo. Ще рано. Значення може тим часом змінюватися.
Ніколя Барбулеско

Я думаю, ви пропустите точку. Апріорі, isFooEnabledце поле примірника Fooоб'єктів.
Ніколя Барбулеско

1

Я не знайомий із синтаксисом Java. Припущення, що в Java існує поліморфізм, статична властивість, абстрактний клас і метод:

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

Хто каже, що включення є статичною властивістю класу?
Ніколя Барбулеско,

Що new bar()має означати?
Ніколя Барбулеско,

На Яві ми пишемо клас Nameіз великої літери.
Ніколя Барбулеско,

Для цього потрібно занадто сильно змінити спосіб виклику. Ми називаємо метод зазвичай: bar(). Якщо вам потрібно це змінити, ви приречені.
Ніколя Барбулеско

1

В основному у вас є прапор, що якщо його встановлено, виклик функції слід пропустити. Тож я думаю, що моє рішення було б нерозумним, але ось воно є.

Foo foo = new Foo();

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

Ось реалізація простого проксі, якщо ви хочете виконати якийсь код перед виконанням будь-якої функції.

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

Для першого блоку коду потрібен видимий метод isEnabled(). Апріорі, що вмикається, це внутрішнє приготування їжі Foo, не піддається впливу.
Ніколя Барбулеско,

Код, що викликає, не може і не хоче знати, чи включений об'єкт .
Ніколя Барбулеско,

0

Є ще одне рішення, використовуючи делегат (покажчик на функцію). Ви можете мати унікальний метод, який спочатку робить перевірку, а потім викликає відповідний метод відповідно до функції (параметра), яку потрібно викликати. 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
Правильно, вона позначена Java, і саме тому я наголошую і написала "Код в C #", оскільки я не знаю Java. Оскільки це питання «Шаблон дизайну», мова не є важливою.
ех

Ой! Я розумію, вибачте за це, лише намагався допомогти і знайти рішення. Дякую
ах
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.