假设我们有一个带有以下构造函数的 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.
}
如果我错了,请纠正我,但我认为在这个例子中,我通过简单地指定可能的输入值的(相当静态的)约束并且如果不满足那些引发通用的,未经检查的异常,则按照 Contract 范式进行设计 .
现在,有一个后端类管理学生列表,按其入学编号索引 . 它保存 Map<Integer, Student>
以保存此映射并通过 addStudent
方法提供对它的访问:
public void addStudent(Student student) {
students.put(student.getMatrNr(), student);
}
现在让我们假设这个方法有一个约束,如“ a student with the same matriculation number must not already exist in the database ” .
我看到了如何实现这两个选项:
选项A.
如果学生具有相同的母校,则定义由 addStudent
引发的自定义 UniquenessException
课程 . 号码已经存在 . 调用代码将看起来像这样:
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,这似乎描述了确切的问题)
因此我想知道:
-
还有第三种选择更好吗?
-
选项B是否具有我没想到的优势?
-
从 Contract 的角度来看,实际上是否允许使用选项B并将唯一性定义为
addStudent
方法的前提条件? -
在定义前置条件时是否有经验法则,只需提高
IAE
以及何时使用"proper"例外?我认为"make it a pre-condition unless it depends on the current state of the system"可能是这样的规则 . 还有更好的吗?
UPDATE: 似乎还有另一个不错的选择,即提供一个 public boolean tryAddStudent(...)
方法,它不会抛出异常,而是使用返回值来表示错误/失败 .
2 回答
(这个评论太长了)
在你的选项B中,我不会使用Map <Integer,Student>然后执行:
Map 抽象并没有提到并发问题),而是使用ConcurrentMap <Integer,Student>并执行以下操作:
我不相信后端类管理学生列表的方式与 Contract 相关 - 也就是说,它持有
Map<Integer, Student>
不会成为 Contract 的一部分 . 因此,在hasStudent(int matrNr)
中将预科编号纳入 Contract 似乎也有点邪恶 .我建议校园可能应该有一个方法
Boolean hasStudent(Student student)
,它会根据条件检查校园是否有学生 . 如果 Contract 要求具有唯一性,并且确实非常特殊,那么您将使用 Contract 检查:抛出的异常应该与参数和参数以及返回值相关
UPDATE
如果添加应该只是失败,如果不满足唯一性并且不是例外,那么不要抛出异常 . 相反,使添加返回值成功(如在java.util.HashSet.add()中) . 这样,如果实际添加了学生,
campus.add(Student)
将返回 true .