合同设计中哪些前提条件是合理的?

时间:2012-02-13 16:30:50

标签: java design-by-contract

假设我们有一个具有以下构造函数的类Student

/** Initializes a student instance.
 * @param matrNr    matriculation number (allowed range: 10000 to 99999)
 * @param firstName first name (at least 3 characters, no whitespace) 
 */
public Student(int matrNr, String firstName) {
    if (matrNr < 10000 || matrNr > 99999 || !firstName.matches("[^\\s]{3,}"))
        throw new IllegalArgumentException("Pre-conditions not fulfilled");
    // we're safe at this point.
}

如果我错了,请纠正我,但我认为在这个例子中,我通过简单地指定可能的输入值的(相当静态的)约束并提出一个通用的,未经检查的异常(如果那些不是)来遵循契约范式的设计满足。

现在,有一个后端课程管理学生列表,按其入学编号索引。它包含一个Map<Integer, Student>来保存此映射,并通过addStudent方法提供对它的访问:

public void addStudent(Student student) {
    students.put(student.getMatrNr(), student);
}

现在让我们假设这个方法有一个限制,例如“数据库中必须存在具有相同入学编号的学生”。

我看到了两种可以实现这一目标的方法:

选项A

如果学生具有相同的母校,则定义UniquenessException提升的自定义addStudent课程。号码已经存在。调用代码将看起来像这样:

try {
    campus.addStudent(new Student(...));
catch (UniquenessError) {
    printError("student already existing.");
}

选项B

将要求说明为先决条件,如果不成立则只提出IAE。此外,提供方法canAddStudent(Student stud),提前检查addStudent是否会失败。调用代码看起来像这样:

Student stud = new Student(...);
if (campus.canAddStudent(stud))
    campus.addStudent(stud);
else
    printError("student already existing.");

从软件工程的角度来看,我认为选项A更清晰,至少出于以下原因:

  • 无需修改调用代码即可轻松实现线程安全(感谢Voo指向TOCTTOU,这似乎描述了确切的问题)

因此我想知道:

  1. 还有第三种选择更好吗?
  2. 选项B是否具有我没想到的优势?
  3. 从合同的角度来看,设计是否允许使用选项B并将唯一性定义为addStudent方法的前提条件?
  4. 在定义前置条件时,是否有经验法则,只需提升IAE以及何时使用“正确”例外?我认为“使它成为一个先决条件,除非它取决于系统的当前状态”可能是这样的规则。还有更好的吗?
  5. UPDATE:似乎还有另一个不错的选择,即提供一个public boolean tryAddStudent(...)方法,该方法不会抛出异常,而是使用返回值来表示错误/失败。

2 个答案:

答案 0 :(得分:2)

(这对评论来说太长了)

在你的选项B中,我不会使用 Map&lt; Integer,Student&gt; 然后执行:

if (campus.hasStudent(12000)) 
    printError("student already existing.");
else
    campus.addStudent(new Student(...));

Map 抽象对你的用例来说不够实用(你提到并发问题),而是使用 ConcurrentMap&lt; Integer,Student&gt; 并做这样的事情:

final Student candidate = new Student(...);
final Student res = putIfAbsent(student.getMatrNr(), candidate)
if ( res != null ) {
    throw new IllegalStateException("Class contract violation: \"student already exists!\", please read the doc");
}

答案 1 :(得分:2)

我不相信后端类管理学生列表的方式与合同相关 - 也就是说,它持有Map<Integer, Student>不会成为合同的一部分。因此,在hasStudent(int matrNr)中将预科编号纳入合同中似乎也有点恶意。

我建议校园可能应该有一个方法Boolean hasStudent(Student student),它会根据条件检查校园是否有学生。如果合同要求具有唯一性,并且确实非常特殊,那么您将使用合同检查:

   Student student= new Student(int matrNbr, String name);
   if (campus.hasStudent(student) {
      throw new UniquenessException();
   }
   else {
      campus.add(student);
   }

抛出的异常应该与参数和参数以及返回值

相关

<强>更新

如果添加应该只是失败,如果不满足唯一性并且不是例外,那么不要抛出异常。相反,使添加返回值成功(如在java.util.HashSet.add()中)。这样,如果实际添加了学生,campus.add(Student)将返回 true