【问题标题】:An alternative for the Decorator pattern in Java?Java 中装饰器模式的替代方案?
【发布时间】:2011-10-09 23:48:51
【问题描述】:

假设您有以下统计相关类的层次结构,其结构类似于Template method pattern:

interface S {
   // Method definitions up-to and including the S3 class
}

class S0 implements S {
   // Code that counts samples
}

class S1 extends S0 {
   // Code that calls the superclass methods and also computes the mean
}

class S2 extends S1 {
   // Code that calls the superclass methods and also computes the variance
}

class S3 extends S2 {
   // Code that calls the superclass methods and also computes the skewness
}

现在假设我们想将这些类中的每一个扩展为例如检查度量的收敛性。出于我的目的,我不需要在运行时进行此扩展。我可以想到以下替代方案:

  • 分别从S0、S1、S2和S2和S3创建子类S1C、S1C、S2C和S3C,每个都有一个检查收敛的代码副本:

    • 优点:
      • 概念上直截​​了当
      • 生成的对象仍然属于超类
      • 子类源代码只包含额外的收敛检查代码
    • 缺点:
      • 大量重复代码 - 未来会产生更改同步开销
    • 主要缺点:
      • 如果我想要另一组类,例如预处理样品?我们谈论的是相同代码的指数复制!
  • 使用Decorator pattern:

    • 优点:
      • 没有代码重复!
    • 缺点:
      • 对象不再属于原始类(很容易解决)
      • 由于使用了虚拟方法调用,而不是特殊的方法调用,Java 的性能非常 轻微(它存在!我测量过!)。这不是很重要,但仍然很明显。
    • 主要缺点:
      • 大约有无数的委托方法必须与封装的对象接口保持同步。使用接口确实可以确保不会遗漏任何方法,但仍然难以维护,即使使用自动生成委托方法的 IDE。
      • 要正确实现装饰器模式,所有装饰器和包装类都需要实现完全相同的接口。这实质上意味着我必须添加例如S 接口的收敛检查方法,这完全破坏了任何模块化感。 解除此要求的唯一方法是在我的代码中禁止嵌套装饰器。

如果 Java 支持多重继承,我可能能够通过从统计信息和基本收敛检查(或其他)类继承来处理这个问题。唉,Java 不支持多重继承(不,接口不算在内!)。

有没有更好的方法在 Java 中处理这个问题?也许是不同的设计模式?更技术性的解决方案?某种特殊的仪式舞蹈?

PS:如果我误解了什么,请随时(温和地)指出......

编辑:

看来我需要澄清一下我的目标:

  • 我不需要运行时对象组合。我想要的是用新方法扩展S* 类的功能。如果我可以根据需要创建子类而无需重复代码,我可能会这样做。如果我能在使用地点(不太可能)做到这一点,那就更好了。

  • 我不想一遍又一遍地编写相同的代码。注意:委托方法和构造函数很好,我想,实现算法的方法不是。

  • 我想保持我的接口模块化。这是装饰器模式的主要问题 - 除非放置非常具体的嵌套约束,否则您最终会得到所有接口的超级接口......

编辑 2:

针对一些 cmets:

  • S* 类是使用模板方法构建的:

    class S0 {
       int addSample(double x) {
          ...;
       }
    
      double getMean() {
          return Double.NaN;
      }
    }
    
    class S1 extends S0 {
    
    
       int addSample(double x) {
          super.addSample(x);
          ...;
       }
    
       double getMean() {
          return ...;
       }
    }
    
  • 我在第一个解决方案中的S*C 扩展类是这样的:

    interface S {
        int addSample(double x);
        double getMean();
    }    
    
    class S0C extends S0 implements S {
       int addSample(double x) {
          super.addSample(x);
          ...;
       }
    
       boolean hasConverged() {
          return ...;
       }
    }
    
    class S1C extends S1 {
       int addSample(double x) {
          super.addSample(x);
          ...;
       }
    
       boolean hasConverged() {
          return ...;
       }
    }
    

    注意hasConverged() 方法的重复。

  • 收敛检查装饰器应该是这样的:

    class CC<T extends S> implements S {
       T o = ...;
    
       int addSample(double x) {
          o.addSample(x);
          ...;
       }
    
       double getMean() {
          return o.getMean();
       }    
    
       boolean hasConverged() {
          return ...;
       }
    }
    

    问题:如果我想组合除收敛检查之外的另一个分隔符行为,我需要一个单独的装饰器,例如NB - 为了能够访问例如hasConverged() 方法,新的装饰器需要:

    • 实现与CC相同的接口
    • 使用与CC 相同的接口作为其包装对象类型...
    • ...如果我希望能够在不使用CC 的情况下将NB 与S* 对象一起使用,这将迫使我将该接口用于S* 方法
  • 我选择 Decorator 模式只是因为没有更好的选择。这是迄今为止我找到的最干净的解决方案。

  • 在扩展 S* 类时,我仍然需要完整的原件。放例如通用超类中的收敛功能意味着相关行为(及其性能影响)现在将存在于所有子类中,这绝对不是我想要的。

【问题讨论】:

  • 为什么我总觉得你可以试试策略模式 (en.wikipedia.org/wiki/Strategy_pattern)?不确定它是否合适,将不得不考虑更多但值得一试......
  • 我也倾向于这方面的策略。策略 + 复合可能会奏效。
  • 可能是visitor?
  • @Eineki 不是更适合遍历数据结构的访问者吗?
  • 我真的没有看到装饰器有什么问题。当然,如果您向 S 添加一个新函数,则必须为装饰器实现转发逻辑会带来一些不便,但这种情况很少见(如果您忘记了它,您会遇到编译错误)。我不正确理解我担心的第二点。如果您想要在收敛函数的某些实现中使用不同的装饰器,您可以执行 interface Decorator extends S 并在其中声明必要的收敛函数。

标签: java oop


【解决方案1】:

根据您最近的编辑。

您可能已经意识到,Decorator 不适合这种情况。这是因为它解决的是单个功能的扩充,而不是整个类树的扩充。

实现这一点的一种可能方法是使用策略。策略以算法为中心;它允许你解耦行为代码(抱歉,如果这里和那里有一点 C# 滑倒)


示例类

public class S {
   private List<Integer> Samples = new List<Integer>(); 

   public void addSample(int x){
      Samples.Add(new Integer(x));
   }

   public void Process(IOp[] operations){
      for (Op operation : operations){
          Process(operation);
      }
   }
   public void Process(ICollection<IOp> operations){
      for (Op operation : operations){
          Process(operation);
      }
   }
   public void Process(IOp operation){
      operation.Compute(this.Samples);
   }
}

操作

public interface IOp { 
   // Interface is optional. Just for flexibility. 
   public void Compute(List<Integer> data);
}
public class Op<T> implements IOp{ 
   // Generics is also optional. I use this to standardise data type of Result, so that it can be polymorphically accessed.
   // You can also put in some checks to make sure Result is initialised before it is accessed.
   public T Result;

   public void Compute(List<Integer> data);
}
class ComputeMeanOperation extends Op<double>{
   public void Compute(List<Integer> data){
       /* sum and divide to get mean */
       this.Result = /* ... */
   }
}
class CheckConvergenceOperation extends Op<boolean>{
   public void Compute(List<Integer> data){
       /* check convergence */
       this.Result = /* ... */
   }
}

用法

public static void main(String args[]) {
    S s = new S();
    s.addSample(1);
    /* ... */

    ComputeMeanOperation op1 = new ComputeMeanOperation();
    CheckConvergenceOperation op2 = new CheckConvergenceOperation ();        

    // Anonymous Operation
    Op<Integer> op3 = new Op<Integer>(){
       public void Compute(List<Integer> samples){
           this.Result = samples[0]; // Gets first value of samples
       }
    }

    s.Process(op1); // Or use overloaded methods
    s.Process(op2);
    s.Process(op3);

    System.out.println("Op1 result: " + op1.Result); 
    System.out.println("Op2 result: " + op2.Result);
    System.out.println("Op3 result: " + op3.Result);
}

优点:

  • 您可以根据需要任意添加和删除操作。
  • 样本类没有额外的变化。
  • 示例类是内聚的数据结构。
  • 模块化:每个操作都是独立的。接口仅公开所需的内容。与每个操作交互的通用流程。
  • 如果您出于某种原因需要重复执行此操作,您可以将所有操作存储在一个数组中,并在循环中重复使用。比调用 4-5 个方法和存储结果要干净得多。

缺点/限制:

  • 如果您的操作需要大量数据,那么您必须将这些数据公开给您的操作,从而增加耦合度(如果您需要,我可以编辑帖子)。在我的示例中,我只是传递了一个样本列表。如果需要,您可能必须改为传入整个数据结构。
  • 如果您有任何依赖于另一个操作的结果的操作,这将无法开箱即用。 (这可以使用 Composite 来完成 - 一个由多个 Ops 组成的巨型 Ops,其结果将传递给下一个。)

希望这符合您的要求:)

【讨论】:

  • 策略模式并不是我所需要的,所以我最终选择了装饰器模式,使用抽象基类来处理对包装对象的委托——至少我不必编写委托方法一遍又一遍。也就是说,这个答案是策略模式的一个很好的例子,它确实帮助了我,所以我会接受它......
【解决方案2】:

我很困惑。目前尚不清楚为什么需要第一个继承树。类似下面的代码就可以做到这一点:

public class Statistics 
{
    void add(final double x) 
    {
        sum  += x;
        sum2 += x * x;
        sum3 += x * x * x;
        n++;
    }
    double mean() 
    {
        return n != 0 ? sum / n : 0;
    }
    double variance() 
    {
        return n != 0 ? ( sum2 - sum * sum / n) / (n - 1) : 0;
    }

    // add method for skewness
    double sum, sum2, sum3;
    int n;
}

【讨论】:

  • 呃,这是一个例子。我真的不认为像这样消除原始的类层次结构是一个合适的解决方案。否则我只会把 everything 放在一个大类中并完成它......
  • 考虑到这一点,我同意雷的观点。看来您可能创建了不必要的类。您可能过度模块化,从而使问题复杂化。选择最简单的可行解决方案,而不是“理论上 100% + 以设计为导向的正确”:)
  • 无论如何我都不是统计学专业的,但是这个解决方案不是需要两倍的方法来处理所有事情吗?
  • @Nupul:实际使用的指标要昂贵得多,因此模块化在性能方面是有意义的。当我只需要平均值时,无需计算样本的时间衰减信息内容......
猜你喜欢
  • 1970-01-01
  • 2012-01-28
  • 1970-01-01
  • 1970-01-01
  • 2011-08-10
  • 2020-05-12
  • 1970-01-01
  • 1970-01-01
  • 2020-10-20
相关资源
最近更新 更多