使用静态类型检查来防止业务错误


13

我非常喜欢静态类型检查。它可以防止您犯以下愚蠢的错误:

// java code
Adult a = new Adult();
a.setAge("Roger"); //static type checker would complain
a.setName(42); //and here too

但这并不能阻止您做出如下愚蠢的错误:

Adult a = new Adult();
// obviously you've mixed up these fields, but type checker won't complain
a.setAge(150); // nobody's ever lived this old
a.setWeight(42); // a 42lb adult would have serious health issues

当您使用相同的类型表示明显不同的信息时,就会出现问题。我当时想解决这个问题的一个好的解决方案是扩展Integer类,只是为了防止业务逻辑错误,而不是增加功能。例如:

class Age extends Integer{};
class Pounds extends Integer{};

class Adult{
    ...
    public void setAge(Age age){..}
    public void setWeight(Pounds pounds){...}
}

Adult a = new Adult();
a.setAge(new Age(42));
a.setWeight(new Pounds(150));

这被认为是好的做法吗?还是这种限制性设计会在将来带来无法预料的工程问题?


5
a.SetAge( new Age(150) )还是不会编译?
John Wu,

1
Java的int类型具有固定范围。显然,您希望具有自定义范围的整数,例如Integer <18,110>。您正在寻找某些(非主流)语言确实提供的优化类型或从属类型。
Theodoros Chatzigiannakis '18

3
您在说的是做设计的好方法!可悲的是,Java具有如此原始的类型系统,很难做到正确。即使您尝试这样做,也可能会导致副作用,例如性能降低或开发人员生产率降低。
欣快的

@JohnWu是的,您的示例仍将编译,但这是该逻辑的单点故障。声明new Age(...)对象后,您将无法Weight在其他任何地方将其错误地分配给类型变量。它减少了可能发生错误的地方。
J-bob

Answers:


12

您实质上是在要求一个单位制(不,不是单位测试,如“物理单位”中的“单位”,例如电表,伏特等)。

在您的代码中,Age时间Pounds代表质量。这会导致诸如单位转换,基本单位,精度等问题。


曾经/曾经尝试将这样的事情引入Java中,例如:

后两个似乎生活在这个github东西中:https : //github.com/unitsofmeasurement


C ++通过Boost具有单位


LabView附带了一堆单元


还有其他语言的示例。(欢迎修改)


这被认为是好的做法吗?

正如您在上面看到的那样,一种语言使用单位处理值的可能性越大,它就越自然地支持单位。LabView通常用于与测量设备进行交互。因此,在语言中具有这样的功能是很有意义的,使用它肯定会被认为是一种很好的做法。

但是在任何通用的高级语言中,对于这种严格程度的需求都很低,这可能是出乎意料的。

还是这种限制性设计会在将来带来无法预料的工程问题?

我的猜测是:性能/内存。如果处理许多值,则每个值的对象开销可能会成为问题。但还是一如既往: 过早的优化是万恶之源

我认为更大的“问题”正在使人们逐渐适应它,因为单位通常是隐式定义的,如下所示:

class Adult
{
    ...
    public void setAge(int ageInYears){..}

人们int在不熟悉单位系统的情况下必须将对象作为似乎看似简单的东西的值来传递时会感到困惑。


“当人们不得不将一个对象作为似乎似乎用简单的东西难以形容的东西的价值来传递时,人们会感到困惑int。” ---这里我们以“最不惊讶”原则为指导。接得好。
格雷格·伯格哈特

1
我认为自定义范围会将其从单位库的范围移到依赖类型
jk。

6

与null的答案相反,如果整数不足以描述度量,则为“单位”定义类型可能是有益的。例如,重量通常在同一测量系统内以多个单位进行测量。考虑“磅”和“盎司”或“千克”和“克”。

如果您需要更细致的测量级别,则为单位定义类型是有益的:

public struct Weight {
    private int pounds;
    private int ounces;

    public Weight(int pounds, int ounces) {
        // Value range checks go here
        // Throw exception if ounces is greater than 16?
    }

    // Getters go here
}

对于“年龄”之类的东西,我建议在运行时根据该人的出生日期进行计算:

public class Adult {
    private Date birthDate;

    public Interval getCurrentAge() {
        return calculateAge(Date.now());
    }

    public Interval calculateAge(Date date) {
        // Return interval between birthDate and date
    }
}

2

您似乎在寻找什么被称为标记类型。他们说“这是一个代表年龄的整数”,而“这也是一个整数,但代表体重”和“不能将一个分配给另一个”。请注意,这远远超出了诸如米或千克之类的物理单位:在我的程序中,我可能具有“人的身高”和“地图上各点之间的距离”,两者均以米为单位,但由于将分配给从业务逻辑的角度来看,另一个没有意义。

某些语言(例如Scala)很容易支持标记的类型(请参见上面的链接)。在其他情况下,您可以创建自己的包装器类,但这不太方便。

另一个问题是验证,例如检查一个人的身高是否“合理”。您可以将此类代码放入您的Adult类(构造函数或设置器)中,或放入标记的类型/包装器类中。在某种程度上,内置类具有URLUUID承担这种作用(除其他外,例如,提供实用程序方法)。

使用标记类型或包装器类实际上是否将使您的代码更好,将取决于多个因素。如果您的对象很简单并且字段很少,则错误分配它们的风险就很小,使用标记类型所需的其他代码可能不值得。在具有复杂结构和大量字段的复杂系统中(特别是如果其中许多共享相同的原始类型),实际上可能会有所帮助。

在我编写的代码中,如果我绕过地图,通常会创建包装器类。诸如此类的类型Map<String, String>本身非常不透明,因此将它们包装在具有有意义名称的类中会NameToAddress很有帮助。当然,使用标记的类型,您可以编写Map<Name, Address>而不需要整个地图的包装器。

但是,对于诸如Strings或Integers之类的简单类型,我发现包装器类(在Java中)太麻烦了。常规的业务逻辑还不错,但是在将这些类型序列化为JSON,将它们映射到DB对象等方面出现了许多问题。您可以为所有大型框架(例如Jackson和Spring Data)编写映射器和挂钩,但是与此代码相关的额外工作和维护将抵消您使用这些包装程序所获得的收益。当然,YMMV和另一个系统中的余额可能会有所不同。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.