Esiste un modo elegante per far iniziare ogni metodo in una classe con un certo blocco di codice?


143

Ho una classe in cui ogni metodo inizia allo stesso modo:

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

C'è un buon modo per richiedere (e si spera non scrivere ogni volta) la fooIsEnabledparte per ogni metodo pubblico nella classe?


45
Esaminare la programmazione orientata agli aspetti (in particolare prima dei consigli ).
Sotirios Delimanolis,

15
Per quanti metodi devi usare questa caldaia? Prima di intraprendere la strada dell'introduzione di AOP, potresti prendere in considerazione l'idea di avere questo pezzetto di codice ripetuto. A volte un po 'di copia e incolla è la soluzione più semplice.
bhspencer,

51
Ho il sospetto che il tuo futuro manutentore sarebbe più felice con la piastra aggiuntiva della caldaia piuttosto che dover imparare un framework AOP.
bhspencer,

7
Se ogni metodo della classe deve fare la stessa cosa nelle prime righe di codice, allora abbiamo una cattiva progettazione.
Tulains Córdova,

7
@ user1598390: La domanda non è fuori tema qui, e non c'è nulla nel campo di applicazione dei programmatori che rende la domanda particolarmente significativa lì.
Robert Harvey,

Risposte:


90

Non conosco l'elegante, ma ecco un'implementazione funzionante che utilizza Java integrato java.lang.reflect.Proxyche impone che tutte le invocazioni del metodo Fooinizino controllando lo enabledstato.

main metodo:

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

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

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

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

FooFactory classe:

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

public class FooFactory {

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

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

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

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

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

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

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

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

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

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

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

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

Come altri hanno sottolineato, sembra eccessivo per quello che ti serve se hai solo una manciata di metodi di cui preoccuparti.

Detto questo, ci sono sicuramente dei vantaggi:

  • Si ottiene una certa separazione delle preoccupazioni, poiché Foole implementazioni del metodo non devono preoccuparsi della preoccupazione enabledtrasversale del controllo. Invece, il codice del metodo deve solo preoccuparsi di quale sia lo scopo principale del metodo, niente di più.
  • Non è possibile per uno sviluppatore innocente aggiungere un nuovo metodo alla Fooclasse e "dimenticare" erroneamente di aggiungere il enabledcontrollo. Il enabledcomportamento del controllo viene ereditato automaticamente da qualsiasi metodo appena aggiunto.
  • Se è necessario aggiungere un'altra preoccupazione trasversale o se è necessario migliorare il enabledcontrollo, è molto facile farlo in modo sicuro e in un unico posto.
  • È un po 'bello poter ottenere questo comportamento simile ad AOP con funzionalità Java integrata. Non sei obbligato a dover integrare qualche altro framework come Spring, anche se possono sicuramente essere buone opzioni.

Ad essere onesti, alcuni degli aspetti negativi sono:

  • Parte del codice di implementazione che gestisce le chiamate proxy è brutto. Alcuni direbbero anche che avere classi interne per prevenire l'istanza della FooImplclasse è brutto.
  • Se si desidera aggiungere un nuovo metodo a Foo, è necessario apportare una modifica in 2 punti: la classe di implementazione e l'interfaccia. Non è un grosso problema, ma è ancora un po 'più di lavoro.
  • Le chiamate proxy non sono gratuite. C'è un certo sovraccarico di prestazioni. Per uso generale, tuttavia, non sarà evidente. Vedi qui per maggiori informazioni.

MODIFICARE:

Il commento di Fabian Streitel mi ha fatto pensare a 2 fastidi con la mia soluzione di cui sopra, lo ammetto, non sono contento di me stesso:

  1. Il gestore di invocazione utilizza stringhe magiche per saltare il "abilitato-controllo" sui metodi "getEnabled" e "setEnabled". Questo può facilmente rompersi se i nomi dei metodi vengono refactored.
  2. Se si verificasse un caso in cui è necessario aggiungere nuovi metodi che non dovrebbero ereditare il comportamento di "controllo abilitato", allora può essere abbastanza facile per lo sviluppatore sbagliare questo, e almeno significherebbe aggiungere più magia stringhe.

Per risolvere il punto n. 1 e almeno per alleviare il problema con il punto n. 2, creerei un'annotazione BypassCheck(o qualcosa di simile) che potrei usare per contrassegnare i metodi Foonell'interfaccia per i quali non voglio eseguire il " controllo abilitato ". In questo modo, non ho affatto bisogno di stringhe magiche e diventa molto più facile per uno sviluppatore aggiungere correttamente un nuovo metodo in questo caso speciale.

Utilizzando la soluzione di annotazione, il codice sarebbe simile al seguente:

main metodo:

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

import java.lang.annotation.*;

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

Foo interfaccia:

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

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

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

FooFactory classe:

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

public class FooFactory {

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

    private static class FooImpl implements Foo {

        private boolean enabled = false;

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

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

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

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

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

    }

    private static class FooInvocationHandler implements InvocationHandler {

        private FooImpl fooImpl;

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

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

                return null;
            }

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

11
Capisco che questa sia una soluzione intelligente ma la useresti davvero?
bhspencer,

1
è una bella soluzione, stai usando il modello di proxy dinamico per decorare l'oggetto con questo comportamento comune trovato all'inizio di ogni metodo.
Victor,

11
@bhspencer: una domanda molto legittima. In realtà l'ho usato molte volte per gestire le eccezioni, la registrazione, la gestione delle transazioni, ecc. Devo ammettere che per le classi più piccole, sembra eccessivo, e potrebbe benissimo esserlo. Ma se mi aspetto che la classe cresca molto in complessità e voglio garantire un comportamento coerente in tutti i metodi mentre lo faccio, non mi dispiace questa soluzione.
sstan

1
Non far parte del 97% del male qui, ma quali sono le conseguenze sulle prestazioni di questa classe proxy?
corsiKa

5
@corsiKa: bella domanda. Non vi è dubbio che l'utilizzo di proxy dinamici sia più lento delle chiamate di metodo dirette. Per un uso generale, tuttavia, il sovraccarico delle prestazioni sarà impercettibile. Discussione SO correlata se sei interessato: Costo delle prestazioni del proxy dinamico Java
sstan

51

Ci sono molti buoni suggerimenti .. quello che puoi fare per colmare il tuo problema è pensare al modello di stato e implementarlo.

Dai un'occhiata a questo frammento di codice .. forse ti farà venire un'idea. In questo scenario sembra che tu voglia modificare l'intera implementazione dei metodi in base allo stato interno dell'oggetto. Ricorda che la somma dei metodi in un oggetto è nota come comportamento.

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

      }
}

Spero ti piaccia!

PD: è un'implementazione del modello di stato (noto anche come strategia a seconda del contesto .. ma i principi sono gli stessi).


1
OP desidera non dover ripetere la stessa riga di codice all'inizio di ogni metodo e la soluzione prevede la ripetizione della stessa riga di codice all'inizio di ogni metodo.
Tulains Córdova,

2
@ user1598390 non è necessario ripetere la valutazione, all'interno di FooEnabledBehaviour si presuppone che il client di questo oggetto abbia impostato fooEnabled su true, quindi non è necessario cheking. Lo stesso vale per la classe FooDisabledBehaviour. Ricontrolla, inserisci il codice.
Victor,

2
Grazie @ bayou.io, aspettiamo fino alla risposta dell'OP. Penso che la comunità stia facendo un ottimo lavoro qui, ci sono molti buoni consigli da queste parti!
Victor,

2
D'accordo con @dyesdyes non riesco a immaginare di implementarlo per qualcosa che non sia una classe davvero banale. È troppo problematico, considerando che bar()in FooEnabledBehaviore bar()in FooDisabledBehaviorpotrebbero condividere molto dello stesso codice, con forse anche una sola riga diversa tra i due. Potresti facilmente, specialmente se questo codice dovesse essere mantenuto da sviluppatori minori (come me), finendo con un gigantesco casino di merda che è irrinunciabile e non verificabile. Questo può accadere con qualsiasi codice, ma sembra così facile da rovinare molto velocemente. +1 però, perché buon suggerimento.
Chris Cirefice,

1
Mmmmm ... non ragazzi ... ma prima, grazie per i commenti. Per me la dimensione del codice non è un problema purché sia ​​"pulita" e "leggibile". A mio favore, dovrei sostenere che non sto usando alcuna classe esterna ... che dovrebbe rendere le cose più accessibili. E se c'è qualche comportamento comune, incapsuliamolo in una CommonBehaviourClass e delegiamo dove serve. Nel libro GOF (non il mio libro preferito per l'apprendimento, ma contiene buone ricette) hai trovato questo esempio: en.wikipedia.org/wiki/… . È più o meno la stessa cosa che faccio qui.
Victor,

14

Sì, ma è un po 'di lavoro, quindi dipende da quanto sia importante per te.

È possibile definire la classe come interfaccia, scrivere un'implementazione delegata e quindi utilizzare java.lang.reflect.Proxyper implementare l'interfaccia con metodi che eseguono la parte condivisa e quindi chiamare condizionalmente il delegato.

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

Il tuo MyInvocationHandlerpuò essere simile a questa (la gestione degli errori e la classe ponteggi omesso, supponendo che fooIsEnabledè definita da qualche parte accessibile):

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

Non è incredibilmente carino. Ma a differenza di vari commentatori, lo farei, poiché penso che la ripetizione sia un rischio più importante di questo tipo di densità e sarai in grado di produrre il "feeling" della tua vera classe, con questo involucro un po 'imperscrutabile aggiunto su molto localmente in solo un paio di righe di codice.

Consultare la documentazione Java per i dettagli sulle classi proxy dinamiche.


14

Questa domanda è strettamente correlata alla programmazione orientata all'aspetto . AspectJ è un'estensione AOP di Java e potresti dargli un'occhiata per ottenere qualche ispirazione.

Per quanto ne so, non esiste un supporto diretto per AOP in Java. Esistono alcuni modelli GOF correlati ad esso, come ad esempio Metodo e strategia modello, ma non ti salveranno davvero le righe di codice.

In Java e nella maggior parte delle altre lingue è possibile definire la logica ricorrente necessaria nelle funzioni e adottare un cosiddetto approccio di codifica disciplinato in cui le si chiama al momento giusto.

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

Tuttavia, questo non si adatterà al tuo caso perché desideri che il codice fattorizzato possa tornare da checkBalance. Nei linguaggi che supportano le macro (come C / C ++) potresti definire checkSomePreconditione checkSomePostconditioncome macro e verrebbero semplicemente sostituiti dal preprocessore prima ancora che il compilatore venga invocato:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;

Java non ha questo fuori dalla scatola. Questo potrebbe offendere qualcuno, ma in passato ho usato la generazione automatica di codice e motori di template per automatizzare attività di codifica ripetitive. Se si elaborano i file Java prima di compilarli con un preprocessore adatto, ad esempio Jinja2, è possibile fare qualcosa di simile a ciò che è possibile in C.

Possibile approccio Java puro

Se stai cercando una soluzione Java pura, ciò che potresti trovare non sarà probabilmente conciso. Tuttavia, potrebbe ancora escludere parti comuni del programma ed evitare la duplicazione del codice e i bug. Potresti fare qualcosa del genere (è una sorta di modello ispirato alla strategia ). Si noti che in C # e Java 8, e in altre lingue in cui le funzioni sono un po 'più facili da gestire, questo approccio può effettivamente sembrare gradevole.

public interface Code {
    void execute();
}

...

public class Foo {
  private bool fooIsEnabled;

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

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

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

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

Un po 'di uno scenario del mondo reale

Stai sviluppando una classe per inviare frame di dati a un robot industriale. Il robot impiega tempo per completare un comando. Una volta completato il comando, ti restituisce un frame di controllo. Il robot può essere danneggiato se riceve un nuovo comando mentre il precedente è ancora in esecuzione. Il programma utilizza una DataLinkclasse per inviare e ricevere frame da e verso il robot. Devi proteggere l'accesso DataLinkall'istanza.

Le chiamate filo interfaccia utente RobotController.left, right, upo downquando l'utente fa clic sui pulsanti, ma richiede anche BaseController.tickperiodicamente, al fine di inoltro comando riattivare ai privati DataLinkistanza.

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
Questo non calcia solo la lattina lungo la strada. Vale a dire che il futuro manutentore doveva ricordare se (! FooIsEnabled) ritorna; all'inizio di ogni funzione e ora devono ricordare di proteggere (nuovo codice {... all'inizio di ogni funzione. In che modo aiuta?
bhspencer,

Mi piace la tua analisi e il contesto di sfondo damix911 ... Costruirò le nuove istanze di codice in fase di compilazione (usando membri statici privati) supponendo che il codice non cambierà nel tempo e rinominerei la modifica in "executeIf" passando come argomento una condizione (Come la classe Predicate) e il Codice. Ma questo è abbattimento e gusto più personali.
Victor,

1
@bhspencer Sembra piuttosto goffo in Java, e nella maggior parte dei casi questa strategia è davvero una riprogettazione di codice altrimenti semplice. Non molti programmi possono beneficiare di tale schema. Una cosa interessante è che abbiamo creato un nuovo simbolo protectche è più facile da riutilizzare e documentare. Se dici al futuro manutentore che il codice critico dovrebbe essere protetto protect, gli stai già dicendo cosa fare. Se le regole di protezione cambiano, il nuovo codice è ancora protetto. Questa è esattamente la logica alla base della definizione delle funzioni, ma l'OP doveva "restituire un return" che le funzioni non possono svolgere.
damix911,

11

Vorrei prendere in considerazione il refactoring. Questo modello sta spezzando fortemente il modello DRY (non ripetere te stesso). Credo che questa rottura rompa questa responsabilità di classe. Ma questo dipende dal tuo controllo del codice. La tua domanda è molto aperta: dove stai chiamando Fooistanza?

Suppongo che tu abbia un codice simile

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

forse dovresti chiamarlo in questo modo:

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

E tienilo pulito. Ad esempio, la registrazione:

this.logger.debug(createResourceExpensiveDump())

a logger non si sta chiedendo se il debug è abilitato. Registra solo.

Invece, la classe chiamante deve controllare questo:

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

Se questa è una libreria e non puoi controllare la chiamata di questa classe, lancia una IllegalStateExceptionche spiega perché, se questa chiamata è illegale e causa problemi.


6
È decisamente più semplice e più facile per gli occhi. Ma se l'obiettivo di OP è quello di assicurarsi che, man mano che vengono aggiunti nuovi metodi, la logica abilitata non venga mai ignorata, questo refactoring non semplifica l'applicazione.
sstan,

4
Anche per il tuo esempio di registro, direi che ciò comporta molte più ripetizioni: ogni volta che vuoi accedere devi controllare che il logger sia abilitato. Tendo a registrare più righe rispetto al numero di metodi su qualsiasi classe ...
T. Kiley

4
Questo rompe la modularità, perché ora il chiamante deve sapere qualcosa sugli interni di foo (in questo caso se è fooEnabled). Questo è un classico esempio in cui seguire le regole delle migliori pratiche non risolverà il problema perché le regole sono in conflitto. (Spero ancora che qualcuno troverà un "perché non ci ho pensato?" Rispondi.)
Ian Goldby

2
Bene, dipende fortemente dal contesto se ha senso.
Ian Goldby,

3
La registrazione è esattamente l'esempio, in cui non desidero ripetere il mio codice. Voglio solo scrivere LOG.debug ("...."); - E il logger dovrebbe verificare se voglio davvero eseguire il debug. - Un altro esempio è la chiusura / pulizia. - Se uso AutoClosable non voglio un'eccezione se è già chiuso, non dovrebbe fare nulla.
Falco,

6

IMHO la soluzione più elegante e dalle migliori prestazioni è quella di avere più di un'implementazione di Foo, insieme a un metodo di fabbrica per crearne una:

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

  public void bar() {
    // Do something
  }

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

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

O una variazione:

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

Come sottolinea sstan, sarebbe ancora meglio usare un'interfaccia:

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

Adattalo al resto delle tue circostanze (stai creando un nuovo Foo ogni volta o riutilizzando la stessa istanza, ecc.)


1
Questa non è la stessa della risposta di Konrad. Mi piace questo, ma penso che sarebbe più sicuro se, invece di usare l'ereditarietà di classe, usassi un'interfaccia come altri hanno suggerito nelle loro risposte. Il motivo è semplice: è troppo facile per uno sviluppatore aggiungere un metodo Fooe dimenticare di aggiungere la versione no-op del metodo nella classe estesa, aggirando così il comportamento desiderato.
sstan,

2
@sstan Hai ragione, sarebbe meglio. L'ho fatto in questo modo per modificare l'esempio originale di Kristina il meno possibile per evitare distrazioni, ma questo è rilevante. Aggiungerò il tuo suggerimento alla mia risposta.
Pepijn Schmitz,

1
Penso che ti manchi un punto. Determinate se isFooEnabledall'istanza di Foo. È troppo presto. Nel codice originale, questo viene fatto quando viene eseguito un metodo. Il valore di isFooEnabledpuò cambiare nel frattempo.
Nicolas Barbulesco,

1
@NicolasBarbulesco Non lo sai, fooIsEnabled potrebbe essere una costante. Oppure potrebbe essere utilizzato in un modo che lo rende effettivamente costante. Oppure potrebbe essere impostato in alcuni luoghi ben definiti in modo che sia facile ottenere una nuova istanza di Foo ogni volta. Oppure potrebbe essere accettabile ottenere una nuova istanza di Foo ogni volta che viene utilizzata. Non sai, ecco perché ho scritto: "Adatta questo al resto delle tue circostanze".
Pepijn Schmitz,

@PepijnSchmitz - Certo, fooIsEnabled può essere costante. Ma nulla ci dice che sia costante. Quindi considero il caso generale.
Nicolas Barbulesco,

5

Ho un altro approccio: avere a

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

}

e poi puoi farlo

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

Forse puoi persino sostituire il isFooEnabledcon una Foovariabile che contiene il FooImplda usare o il NullFoo.DEFAULT. Quindi la chiamata è di nuovo più semplice:

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

A proposito, questo si chiama "Null pattern".


L'approccio generale è buono, ma l'uso dell'espressione (isFooEnabled ? foo : NullFoo.DEFAULT).bar();sembra un po 'goffo. Avere una terza implementazione che delega a una delle implementazioni esistenti. Invece di modificare il valore di un campo, isFooEnabledl'obiettivo della delega potrebbe essere modificato. Ciò riduce il numero di rami nel codice
SpaceTrucker,

1
Ma stai deportando la cottura interna della classe Foonel codice chiamante! Come possiamo sapere se isFooEnabled? Questo è un campo interno nella classe Foo.
Nicolas Barbulesco,

3

In un approccio funzionale simile alla risposta di @ Colin, con le funzioni lambda di Java 8 , è possibile racchiudere la funzione condizionale abilitando / disabilitando il codice in un metodo guard ( executeIfEnabled) che accetta l'azione lambda, a cui può essere eseguito il codice da eseguire in modo condizionale passato.

Anche se nel tuo caso, questo approccio non salverà alcuna riga di codice, ESSICCANDO questo, ora hai la possibilità di centralizzare altre preoccupazioni di attivazione delle funzionalità, oltre a AOP o problemi di debug come registrazione, diagnostica, profiling et al.

Uno dei vantaggi dell'utilizzo di lambda qui è che le chiusure possono essere utilizzate per evitare la necessità di sovraccaricare il executeIfEnabledmetodo.

Per esempio:

class Foo {
    private Boolean _fooIsEnabled;

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

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

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

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

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

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

Con un test:

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

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

Anche simile all'approccio di Damix, senza la necessità di un'interfaccia e implementazioni di classi anonime con metodo override.
StuartLC,

2

Come sottolineato in altre risposte, il modello di progettazione strategica è un modello di progettazione appropriato da seguire per semplificare questo codice. L'ho illustrato qui usando l'invocazione del metodo attraverso la riflessione, ma ci sono un numero qualsiasi di meccanismi che potresti usare per ottenere lo stesso effetto.

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

Dover affrontare la riflessione è un po 'imbarazzante, se ci sono già classi che lo fanno per te, come il già citato Proxy.
SpaceTrucker,

Come hai potuto fare foo.fooIsEnabled ...? A priori, questo è un campo interno dell'oggetto, non possiamo e non vogliamo vederlo all'esterno.
Nicolas Barbulesco,

2

Se solo Java fosse un po 'più bravo a essere funzionale. Pensa che la soluzione più OOO sia quella di creare una classe che avvolga una singola funzione, così viene chiamata solo quando foo è abilitato.

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

e quindi implementare bar, baze batcome classi anonime che che si estendono 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
        }
    };
}

Un'altra idea

Usa la soluzione nullable di glglgl ma crea FooImple le NullFooclassi interne (con costruttori privati) della classe sottostante:

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

in questo modo non devi preoccuparti di ricordarti di usare (isFooEnabled ? foo : NullFoo.DEFAULT).


Di 'che hai: Foo foo = new Foo()per chiamarti barscriverestifoo.bar.call()
Colin il

1

Sembra che la classe non faccia nulla quando Foo non è abilitato, quindi perché non esprimerlo a un livello superiore in cui si crea o si ottiene l'istanza di Foo?

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

Funziona solo se isFooEnabled è una costante però. In un caso generale, è possibile creare la propria annotazione.


Konrad, puoi sviluppare l'annotazione?
Nicolas Barbulesco,

Il codice originale determina se fooIsEnabledviene chiamato un metodo. Lo fai prima dell'istanza di Foo. È troppo presto. Nel frattempo il valore può cambiare.
Nicolas Barbulesco,

Penso che ti manchi un punto. A priori, isFooEnabledè un campo di istanza di Foooggetti.
Nicolas Barbulesco,

1

Non ho familiarità con la sintassi Java. Supponiamo che in Java vi siano polimorfismi, proprietà statiche, classe e metodo astratti:

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

Chi dice che l' abilitazione è proprietà statica della classe?
Nicolas Barbulesco,

Cosa new bar()dovrebbe significare?
Nicolas Barbulesco,

In Java, scriviamo una classe Namecon una lettera maiuscola.
Nicolas Barbulesco,

Questo ha bisogno di cambiare troppo il codice chiamante. Chiamiamo un metodo normalmente: bar(). Se hai bisogno di modificarlo, sei condannato.
Nicolas Barbulesco,

1

Fondamentalmente hai un flag che se è impostato, la chiamata di funzione dovrebbe essere saltata. Quindi penso che la mia soluzione sarebbe sciocca, ma eccola qui.

Foo foo = new Foo();

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

Ecco l'implementazione di un semplice proxy, nel caso in cui si desideri eseguire del codice prima di eseguire qualsiasi funzione.

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

Per il tuo primo blocco di codice, hai bisogno di un metodo visibile isEnabled(). A priori, l' abilitazione è la cottura interna di Foo, non esposta.
Nicolas Barbulesco,

Il codice chiamante non può e non vuole sapere se l'oggetto è abilitato .
Nicolas Barbulesco,

0

C'è un'altra soluzione, usando delegate (puntatore alla funzione). È possibile disporre di un metodo univoco che per primo sta eseguendo la convalida e quindi chiama il metodo pertinente in base alla funzione (parametro) da chiamare. Codice 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
Corretto, è contrassegnato con Java e per questo sottolineo e scrivo "Codice in C #" poiché non conosco Java. Dal momento che è una domanda di Design Pattern, quindi la lingua non è importante.
eh

Oh! Capisco, mi dispiace per quello, ho solo cercato di aiutare e trovare una soluzione. Grazie
ehh
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.