首页 > 解决方案 > 如何保证方法调用中的可交换性?

问题描述

我正在使用Ward Cunningham 的 Exceptional Value 和 Meaningless Behavior patterns的组合来尝试无例外地编程的想法。

假设我有一个接口 Fraction,旨在表示可以通过将 2 个其他量除以一组操作来表示的任何量:

interface Fraction {
    
    Fraction add(Fraction f);
    Fraction subtract(Fraction f);
    Fraction multiply(Fraction f);
    Fraction divide(Fraction f);
    
}

现在,该接口将有 2 个主要实现: a) SimpleFraction,其中分母不为 0;b) UndefinedFraction,其中分母为 0。两者都将由工厂创建:

public final class SimpleFraction implements Fraction {

    private final int numerator, denominator;

    private SimpleFraction(int numerator, int denominator) {
    
        this.numerator = numerator;
        this.denominator = denominator;
        
    }
    
    // interface method implementations
    
}

public final class UndefinedFraction implements Fraction {

    private final int numerator;
    
    private UndefinedFraction(int numerator) {
        
        this.numerator = numerator;
        
    }

    // interface method implementations
    
}

UndefinedFraction 将是一个具有无意义行为的异常值:它的每个操作都应该返回一个 UndefinedFraction。更重要的是,如果它在任何其他分数的运算中用作参数,结果也应该是一个 UndefinedFraction :

FractionFactory factory = new FractionFactory();
Fraction simple = factory.fromNumeratorAndDenominator(420, 10);
Fraction undefined = factory.fromNumeratorAndDenominator(42, 0);

assertEquals(simple.add(undefined), undefined);
assertEquals(undefined.add(simple), undefined);
assertEquals(undefined.add(undefined), undefined);

如何在涉及 UndefinedFraction 的操作中强制执行这种交换性?我正在寻找基于多态性的选项(例如,它们不应该依赖于布尔检查)。

标签: javaoop

解决方案


问题是关联性。

术语:给定'a / b',我们称a为LHS(左手边),b为RHS,'/'为运算符,'divide'运算。LHS 和 RHS 都是“操作数”。

问题:假设我们创建了两种未定义的风格。我们有“红色未定义”和“绿色未定义”,我们坚持相同的定义:LHS 和/或 RHS 为“红色未定义”的任何操作都应生成“红色未定义”。

相同的规则适用于“绿色未定义”:LHS 和/或 RHS 为“绿色未定义”的任何操作都应为“绿色未定义”,而不管其他操作数的值和操作如何。

不幸的是,这个定义是模棱两可的:

Fraction red = factory.newRedUndefined();
Fraction green = factory.newGreenUndefined();
Fraction whatColorIsThis = red.add(green);

那应该是红色还是绿色?谁有优先权?

这个问题应该强调你的问题没有简单/明显的答案!

回到'a / b' - 你所做的(这在面向对象的语言中很难避免)是你定义了操作数上可用的操作。它是具有“除法”方法的“a”。并不是说“除法”这个概念是一个对象,它有一个带有 2 个参数的方法。换句话说,你有这个:

a.divide(b);

而不是这个:

fractionMathSystem.divide(a, b);

使a.divide(b)工作本质上关联“左”的基本设计(例如,“a”决定了除法的实现,b 只与右关联),并且不关联“右”,因为这就是 java 的定义方式。

问题是,您不希望在这里留下关联性:如果 'b' 未定义而 'a' 未定义,则您希望 'b' “覆盖”并控制操作。

但是你不能这样做,并且由于前面提到的红色/绿色未定义问题,没有简单的语言建议没有任何警告可以做到这一点。

根本问题是您已经定义了一个运算(分数的除法),它应该根据运算本身而不是根据任一操作数来定义。因此,fractionNumberSystem.divide(a, b)更有意义;您可以在此处编写“如果任一操作数未定义,则结果未定义”的代码,甚至可以引入一个系统,询问每个操作数是否要负责操作,并为每个操作数提供优先级层次结构,其中数字系统将选择报告它具有最高优先级的操作数,并要求它执行操作。

幸运的是,您可以a.divide(b)归结为:

public class Fraction {
    public void divide(Fraction rhs) {
        return numberSystem.divide(this, rhs);
    }
}

现在 numberSystem.divide 可以做任何你想做的事情。“除法操作首先关联到未定义,无论未定义在哪一边”的概念然后可以在 numberSystem 的除法方法中编程。或者,你可以让它更“可插拔”,想办法解决红/绿问题(当双方都想控制操作时,你如何决定哪个“赢”?是 red.add(green ) 红色还是绿色?),随心所欲。也许通过检查所涉及的 Fraction 是否是“OperationControllingFraction”(您要创建的子类型)的实例,并且使 OperationControllingFraction 类型具有radd方法(右关联添加)以及int priority()方法。

注意:提示:add是一个坏名字。plus效果更好。a.add(b)建议这修改a. a.plus(b)建议 a 保持不变,并返回一个新值。是的,BigInteger而且朋友的名字很糟糕。它发生了;这些 API 已经很老了,考虑到设计错误已经足够严重,我认为延续明显的设计错误以保持一致并不是一个好主意。


推荐阅读