是否有一种优雅的方法可以使类中的每个方法都以某个代码块开头?

kri*_*ina 141 java design-patterns

我有一个类,每个方法都以相同的方式启动:

class Foo {
  public void bar() {
    if (!fooIsEnabled) return;
    //...
  }
  public void baz() {
    if (!fooIsEnabled) return;
    //...
  }
  public void bat() {
    if (!fooIsEnabled) return;
    //...
  }
}
Run Code Online (Sandbox Code Playgroud)

fooIsEnabled对于类中的每个公共方法,是否有一种很好的方法要求(并且希望不是每次都写入)该部分?

sst*_*tan 89

我不知道优雅,但这里是使用工作的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"
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

正如其他人所指出的那样,如果你只需要担心一些方法,那么你所需要的东西似乎有些过分.

那说,肯定有好处:

  • 实现了一定程度的关注,因为Foo方法实现不必担心enabled检查交叉问题.相反,该方法的代码只需要担心方法的主要目的是什么,仅此而已.
  • 无辜的开发人员无法向Foo班级添加新方法,并错误地"忘记"添加enabled支票.该enabled检查的行为由新添加的方法自动继承.
  • 如果您需要增加另一个跨领域的问题,或者您需要加强enabled检查,那么在一个地方安全地完成这项工作非常容易.
  • 通过内置的Java功能,您可以获得类似AOP的行为,这很好.你不必被迫集成其他类似的框架Spring,尽管它们也绝对是不错的选择.

公平地说,一些缺点是:

  • 一些处理代理调用的实现代码很难看.有些人还会说,有内部类来防止类的实例化FooImpl是丑陋的.
  • 如果要添加新方法Foo,则必须在2个位置进行更改:实现类和接口.没什么大不了的,但它仍然需要更多的工作.
  • 代理调用不是免费的.存在一定的性能开销.但是对于一般用途,它不会引人注意.有关更多信息,请参见此处

编辑:

Fabian Streitel的评论让我想到了上述解决方案的2个烦恼,我承认,我对自己并不满意:

  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"
}
Run Code Online (Sandbox Code Playgroud)

BypassCheck 注解:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BypassCheck {
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @bhspencer:一个非常合理的问题.我实际上已经多次使用它来进行异常处理,日志记录,事务处理等.我会承认,对于较小的类,它看起来有点矫枉过正,而且可能很好.但是,如果我希望这个类在复杂性方面增长很多,并且希望确保在我这样做的所有方法中保持一致的行为,我不介意这个解决方案. (11认同)
  • 我知道这是一个聪明的解决方案,但你真的会用到这个吗? (10认同)
  • @corsiKa:好问题.毫无疑问,使用动态代理比直接方法调用慢.但是,对于一般用途,性能开销将是难以察觉的.相关的SO线程,如果您感兴趣:[Java动态代理的性能成本](http://stackoverflow.com/questions/1856242/performance-cost-of-java-dynamic-proxy) (5认同)

Vic*_*tor 50

有很多好的建议......你可以做些什么来解决你的问题是在State Pattern中思考并实现它.

看看这段代码片段......也许它会让你有个想法.在这种情况下,您希望根据对象的内部状态修改整个方法实现.请回想一下,对象中方法的总和被称为行为.

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

      }
}
Run Code Online (Sandbox Code Playgroud)

希望你喜欢!

PD:是状态模式的实现(根据上下文也称为策略......但原则是相同的).

  • OP 希望不必在每个方法的开头重复同一行代码,而您的解决方案涉及在每个方法的开头重复同一行代码。 (2认同)
  • @ user1598390不需要重复评估,在FooEnabledBehaviour中你假设这个对象的客户端已经将fooEnabled设置为true,所以不需要cheking.FooDisabledBehaviour类也是如此.再次检查,代码. (2认同)
  • 谢谢@ bayou.io,让我们等到OP回答.我认为社区在这里工作很糟糕,这里有很多好的建议! (2认同)
  • 同意@dyesdyes我无法想象实现这个除了一个非常微不足道的课程.考虑到`FooEnabledBehavior`中的`bar()`和`FooDisabledBehavior`中的`bar()`可能共享很多相同的代码,甚至可能只有一行在两者之间有所不同,这太有问题了.你很可能很容易,特别是如果这个代码是由初级开发者(比如我自己)维护的那样,最终会得到一堆无法维护和无法控制的垃圾.任何代码都可能发生这种情况,但这似乎很容易搞砸了.虽然+1,因为很好的建议. (2认同)

Dav*_*ell 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);
Run Code Online (Sandbox Code Playgroud)

MyInvocationHandler可以看起来像这样(错误处理和类脚手架省略,假设fooIsEnabled在某处可访问):

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

这不是非常漂亮.但是与各种评论者不同,我会这样做,因为我认为重复是比这种密度更重要的风险,并且你将能够产生真实类的"感觉",添加了这个有点不可思议的包装器非常本地只有几行代码.

有关动态代理类的详细信息,请参阅Java文档.


dam*_*911 14

这个问题与面向方面的编程密切相关.AspectJ是Java的AOP扩展,您可以试一试获得一些愿望.

据我所知,Java中没有对AOP的直接支持.有一些与它相关的GOF模式,例如模板方法策略,但它不会真正保存你的代码行.

在Java和大多数其他语言中,您可以定义函数中所需的循环逻辑,并采用所谓的规范编码方法,您可以在适当的时间调用它们.

public void checkBalance() {
    checkSomePrecondition();
    ...
    checkSomePostcondition();
}
Run Code Online (Sandbox Code Playgroud)

但是,这不适合您的情况,因为您希望能够从中返回的因子代码checkBalance.在支持宏的语言(如C/C++)中,您可以定义checkSomePreconditioncheckSomePostcondition作为宏,在调用甚至调用编译器之前,它们将简单地被预处理器替换:

#define checkSomePrecondition \
    if (!fooIsEnabled) return;
Run Code Online (Sandbox Code Playgroud)

Java没有开箱即用的功能.这可能会冒犯别人,但我确实使用自动代码生成和模板引擎来自动执行过去的重复编码任务.如果您在使用合适的预处理器(例如Jinja2)编译它们之前处理Java文件,您可以执行与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");
      }
    });
  }
}
Run Code Online (Sandbox Code Playgroud)

有点真实的场景

您正在开发一个类来将数据帧发送到工业机器人.机器人需要时间来完成命令.命令完成后,它会向您发送一个控制帧.如果机器人在前一个仍在执行时收到新命令,则可能会损坏.您的程序使用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 */);
        }});
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 难道这不会在路上踢.也就是说,未来的维护者过去必须记住if(!fooIsEnabled)返回; 在每个函数的开头,现在他们需要记住保护(新代码{...在每个函数的开头.这有什么帮助? (4认同)

Gon*_*ndy 11

我会考虑重构.这种模式严重破坏了DRY模式(不要重复自己).我相信这打破了这个阶级的责任.但这取决于您对代码的控制.你的问题非常开放 - 你在哪里调用Foo实例?

我想你的代码就像

foo.bar(); // does nothing if !fooEnabled
foo.baz(); // does also nothing
foo.bat(); // also
Run Code Online (Sandbox Code Playgroud)

也许你应该这样称呼它:

if (fooEnabled) {
   foo.bat();
   foo.baz();
   ...
}
Run Code Online (Sandbox Code Playgroud)

并保持清洁.例如,记录:

this.logger.debug(createResourceExpensiveDump())
Run Code Online (Sandbox Code Playgroud)

a 如果启用了调试,则logger 不会问自己.它只是记录.

相反,调用类需要检查:

if (this.logger.isDebugEnabled()) {
   this.logger.debug(createResourceExpensiveDump())
}
Run Code Online (Sandbox Code Playgroud)

如果这是一个库并且您无法控制此类的调用,请抛出一个IllegalStateException解释原因,如果此调用是非法的并导致麻烦.

  • 它的眼睛肯定更简单,更容易.但是,如果OP的目标是确保在添加新方法时,永远不会绕过启用的逻辑,那么这种重构不会使执行它变得更容易. (6认同)
  • 同样对于你的日志示例,我会说这涉及更多的重复 - 每次你想要记录时,你必须检查记录器是否已启用.我倾向于记录比任何类的方法数更多的行... (4认同)
  • 这打破了模块化,因为现在调用者必须知道关于foo内部的一些东西(在这种情况下是否是fooEnabled).这是一个典型的例子,遵循最佳实践规则将无法解决问题,因为规则冲突.(我仍然希望有人会想出"我为什么不这么想?"回答.) (4认同)
  • 记录就是一个例子,我希望代码中没有重复.我只想写LOG.debug("...."); - 记录器应该检查我是否真的想要调试. - 另一个例子是关闭/清理. - 如果我使用AutoClosable,我不想要一个例外,如果它已经关闭,它应该什么都不做. (3认同)
  • 那么,它在很大程度上取决于它是否有意义. (2认同)
  • 这将采用Foo在Foo类之外的行为方式的逻辑.这似乎都错了...... (2认同)

Pep*_*itz 6

恕我直言,最优雅,性能最佳的解决方案是拥有多个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
  }
}
Run Code Online (Sandbox Code Playgroud)

或者变化:

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
    }
  };
}
Run Code Online (Sandbox Code Playgroud)

正如斯坦斯指出的那样,更好的方法是使用界面:

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
  }
}
Run Code Online (Sandbox Code Playgroud)

根据您的其他情况进行调整(您是每次创建一个新的Foo还是重复使用相同的实例,等等)

  • @sstan你是对的,那会更好.我这样做是为了尽可能少地修改克里斯蒂娜的原始例子,以避免分心,但这是相关的.我会在你的回答中加入你的建议. (2认同)

glg*_*lgl 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() {}
}
Run Code Online (Sandbox Code Playgroud)

}

然后你就可以做到

(isFooEnabled ? foo : NullFoo.DEFAULT).bar();
Run Code Online (Sandbox Code Playgroud)

也许你甚至可以isFooEnabled用一个Foo变量来替换它,这个变量既FooImpl可以使用也可以使用NullFoo.DEFAULT.然后再次呼叫更简单:

Foo toBeUsed = isFooEnabled ? foo : NullFoo.DEFAULT;
toBeUsed.bar();
toBeUsed.baz();
toBeUsed.bat();
Run Code Online (Sandbox Code Playgroud)

顺便说一句,这被称为"空模式".