合同设计中什么样的先决条件是合理的?

发布于 2025-01-05 06:16:03 字数 2118 浏览 1 评论 0原文

假设我们有一个带有以下构造函数的类 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 来保存此映射,并通过 addStudent 方法提供对其的访问:

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

现在我们假设此方法有一个约束,例如“< strong>数据库中不得存在具有相同学号的学生”。

我看到了如何实现这一点的两种选择:

选项 A

定义一个自定义 UniquenessException 类,如果学生具有相同的 matr.addStudent 属性,则由 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 以及何时使用“适当的”异常,是否有经验法则?我认为“将其作为先决条件,除非它取决于系统的当前状态”可能是这样的规则。有更好的吗?

更新:似乎还有另一个不错的选择,那就是提供一个 public boolean tryAddStudent(...) 方法,该方法不会抛出异常,而是发出错误信号/失败使用返回值。

Let's assume we have a class Student with the following constructor:

/** 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.
}

Correct me if I'm wrong, but I think in this example, I followed the design by contract paradigm by simply specifiying the (rather static) constraints on the possible input values and raising a generic, unchecked exception if those are not fulfilled.

Now, there is a backend class that manages a list of students, indexed by their matriculation number. It holds a Map<Integer, Student> to save this mapping and provides access to it through an addStudent method:

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

Now let's assume there is a constraint on this method like "a student with the same matriculation number must not already exist in the database".

I see two options of how this could be realized:

Option A

Define a custom UniquenessException class that is raise by addStudent if a student with the same matr. number already exists. Calling code will then look something like this:

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

Option B

State the requirement as a pre-condition and simply raise an IAE if it doesn't hold. Additionally, provide a method canAddStudent(Student stud) that checks in advance whether addStudent will fail. Calling code would then look something like this:

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

I feel that option A is much cleaner from a software-engineering point of view, for at least the following reason:

  • It can easily be made thread-safe without modifying the calling code (Thanks to Voo for pointing me to TOCTTOU, which seems to describe that exact issue)

Thus I wonder:

  1. Is there a third option which is even better?
  2. Does option B have an advantage that I didn't think of?
  3. Would it actually be allowed from a design by contract point of view to use option B and define the uniqueness as a pre-condition of the addStudent method?
  4. Is there a rule of thumb when to define pre-conditions and simply raise IAE and when to use "proper" exceptions? I think "make it a pre-condition unless it depends on the current state of the system" could be such a rule. Is there a better?

UPDATE: It seems like there is another good option, which is to provide a public boolean tryAddStudent(...) method that doesn't throw an exception but instead signals error/failure using the return value.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

双手揣兜 2025-01-12 06:16:03

(对于评论来说太长了)

在您的选项 B 中,我不会使用 Map然后执行:

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

Map< /em> 抽象对于您的用例来说不够实用(您提到了并发问题),我会使用 ConcurrentMap并执行以下操作:

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");
}

(this is too long for a comment)

In your option B, I wouldn't use a Map<Integer,Student> and then do:

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

The Map abstraction isn't practical enough for your use case (you're mentionning concurrency issues), I'd use instead a ConcurrentMap<Integer,Student> and do something like this:

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");
}
夕嗳→ 2025-01-12 06:16:03

我不认为后端类管理学生列表的方式与合同相关 - 也就是说,它持有 Map 不会成为合同的一部分合同。因此,将学号带入 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

I don't believe the way that the backend class manages a list of students would be relevant to the contract--that is, that it holds a Map<Integer, Student> would not be part of the contract. Thus bringing the matriculation number into the contract in hasStudent(int matrNr) seems a little evil too.

I'd suggest the the campus probably should have a method Boolean hasStudent(Student student), which would check to see if the campus has the student based on whatever the condition. If uniqueness is required by contract, and is truly exceptional, you would then use the contractual check:

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

The exceptions thrown should be as relevant to the contract as the arguments and return values

UPDATE

If the add should simply fail if uniqueness is not met and is not exceptional, then don't throw the exception. Instead, make the success of the add a return value (as in java.util.HashSet.add()). This way, campus.add(Student) would return true if the student was actually added.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文