Java中装饰器模式的替代方案?

8
假设您有以下与统计相关的类层次结构,其结构方式类似于模板方法模式
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
}

假设现在我们想要扩展这些类,例如检查度量的收敛性。对于我的目的,我不需要在运行时进行此扩展。我可以考虑以下替代方案:
  • 创建子类S0C, S1C, S2CS3C,分别从S0, S1, S2S3继承,并拥有检查收敛性的代码副本:

    • 优点:
      • 概念上直观简单
      • 生成的对象仍然属于超类
      • 子类源代码仅包含额外的收敛检查代码
    • 缺点:
      • 大量的代码重复 - 未来会导致同步开销的增加
    • 主要缺点:
      • 如果我想要另一组例如预处理样本的类怎么办?我们在谈论指数级别的相同代码复制!
  • 使用装饰器模式

    • 优点:
      • 没有代码重复!
    • 缺点:
      • 对象不再属于原始类(可以很容易地解决这个问题)
      • 在Java中,由于使用虚方法调用而非特殊方法调用,会有一个非常小的(存在!我测量过!)性能损失。这并不是非常重要,但仍然可以注意到。
    • 主要缺点:
      • 必须保持与包装对象接口同步的数量庞大的委托方法。使用接口确保没有遗漏的方法,但即使使用自动化生成委托方法的IDE也很难维护。
      • 为了拥有适当实现的装饰器模式,所有装饰器和被包装的类都需要实现完全相同的接口。这实际上意味着我必须将例如收敛检查方法添加到S接口中,这完全破坏了任何模块化的意义。 唯一取消此要求的方法是禁止代码中的嵌套装饰器。
如果Java支持多重继承,我可能可以通过同时从统计和基本收敛检查(或其他)类继承来处理这个问题。遗憾的是,Java不支持多重继承(不,接口不算!)。
在Java中有更好的处理此问题的方法吗?也许是不同的设计模式?更技术性的解决方案?某种特殊的仪式舞蹈?
PS:如果我误解了什么,请随时(温柔地)指出...
编辑:
看来我需要稍微澄清一下我的目标:
- 我不需要运行时对象组合。我想要的是使用新方法扩展S*类的能力。如果我可以根据需要创建子类而不重复代码,我可能会这样做。如果我可以在使用位置进行操作(不太可能),那就更好了。 - 我不想一遍又一遍地编写相同的代码。注意:委托方法和构造函数是可以的,我想,实现算法的方法则不行。 - 我希望保持我的接口模块化。这是我对装饰器模式的主要问题 - 除非放置非常特定的嵌套约束,否则您最终将得到所有接口的超级接口...
回应几条评论:
  • The S* classes are structured using template methods:

    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 ...;
       }
    }
    
  • My S*C extended classes from the first solution would be like this:

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

    Note the duplication of the hasConverged() method.

  • A convergence checking decorator would be like this:

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

    The problem: If I want to combine another separator behavior besides convergence checking, I need a separate decorator e.g. NB - and in order to have access to e.g. the hasConverged() method, the new decorator needs to:

    • Implement the same interface as CC
    • Use the same interface as CC for its wrapped object type...
    • ...which forces me to use that interface for the S* methods if I want to be able to use NB with S* objects without using CC
  • My selection of the Decorator patter was only for lack of a better alternative. It's just the cleanest solution I have found thus far.

  • When extending the S* classes, I still need the originals intact. Putting e.g. the convergence functionality in a common super-class would mean that the associated behavior (and its performance impact) would now exist in all subclasses, which is definitely not what I want.


1
我为什么有一种困扰的感觉,你可以尝试一下策略模式(http://en.wikipedia.org/wiki/Strategy_pattern)?不确定它是否适合,需要更多思考,但值得一试... - PhD
也许是一个访问者模式 - Eineki
我并没有看到装饰器有太大的问题。当然,如果你向 S 添加一个新函数,需要为装饰器实现转发逻辑可能会稍微有些不便,但这种情况相对较少(如果忘记了,还会得到编译错误)。我担心我没有正确理解第二点。如果您想要不同的装饰器,它们都覆盖某些收敛函数的实现,您可以使用 interface Decorator extends S 并在其中声明必要的收敛函数。 - Voo
@thkala 好的,有道理(但在这种情况下,MI也没有帮助,因为那也是静态的?)。不过我不完全理解装饰器的作用。你想从外部代码调用hasConverged()而不仅仅是在某个S接口函数之前/之后吗?如果是这样,那么装饰器就不是正确的解决方案,因为你基本上创建了一个完全不同的数据类型。不过这并不是什么大问题,因为“Decorator extends S”解决方案仍然可以工作——它只是不再是装饰器了。 - Voo
@thkala 如果您提供更多类似于您要做的问题域,那么我们可以尝试为您解决它。模式并非“一刀切”,您需要根据情况应用它们。在某些情况下,您必须进行微调。我建议使用策略模式,因为您的问题似乎是一个算法问题。这有帮助吗? - Daryl Teo
显示剩余10条评论
2个回答

4

根据您最近的编辑。

修饰器不适合这个问题,你可能已经意识到了。这是因为它解决的是单一功能的增强,而不是整个类树的增强。

可能的方法是使用策略模式。策略模式聚焦于算法;它允许您将行为代码解耦(如果出现了一些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代替,即由几个操作组成的“超级”操作,其结果传递给下一个操作)

希望这符合您的要求 :)


策略模式并不完全符合我的需求,所以我最终选择了装饰者模式,并使用一个抽象基类来处理对包装对象的委派 - 至少我不必一遍又一遍地编写委派方法。话虽如此,这个答案是策略模式的一个很好的例子,而且确实帮助了我,所以我会接受它... - thkala

0
我有点困惑。不太清楚为什么需要第一个继承树。下面这段代码就可以完成这个任务:
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;
}

嗯,这只是一个例子。我并不认为像这样消除原始类层次结构是一个合适的解决方案。否则我就把所有东西都放在一个大类里面,然后就完成了... - thkala
思考一下,我同意Ray的观点。看起来你可能创建了比必要更多的类。你可能过度模块化,从而使问题变得复杂化。选择最简单的解决方案,而不是追求“100%理论+设计正确”的目标 :) - PhD
我并不是统计专业的,但这个解决方案难道不需要两倍于原来的方法吗? - Daryl Teo
@Nupul:实际使用的度量标准要昂贵得多,因此在性能方面进行模块化是有意义的。当我只需要均值时,没有必要计算样本的时间衰减信息内容... - thkala

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接