用DDD(或有意义)建立模型关系?


9

这是一个简化的要求:

用户创建一个Question带有多个Answer的。Question必须至少有一个Answer

澄清:思考QuestionAnswer测试一样:有一个问题,但是有几个答案,其中几个可能是正确的。用户是准备此测试的演员,因此他创建了问题和答案。

我正在尝试对这个简单的示例进行建模,以便1)匹配现实生活模型2)用代码进行表达,从而最大程度地减少潜在的误用和错误,并向开发人员提示如何使用该模型。

问题是实体,答案是价值对象。问题包含答案。到目前为止,我已经有了这些可能的解决方案。

[A]工厂内Question

除了Answer手动创建,我们可以调用:

Answer answer = question.createAnswer()
answer.setText("");
...

这将创建一个答案并将其添加到问题中。然后,我们可以通过设置其属性来操纵答案。这样,只有问题才能创造答案。此外,我们会避免没有问题的答案。但是,我们无法控制创建答案的方式,因为答案已在中进行了硬编码Question

上述代码的“语言”也存在一个问题。用户是创建答案而不是问题的人。就我个人而言,我不喜欢我们创建值对象并依靠开发人员将其填充值-他如何确定需要添加什么?

[B]工厂内部问题,参加#2

有人说我们应该在以下方法中使用这种方法Question

question.addAnswer(String answer, boolean correct, int level....);

与上述解决方案类似,此方法将强制性数据用于答案并创建一个也将添加到问题中的答案。

这里的问题是我们无缘无故地复制的构造函数Answer。另外,问题真的会产生答案吗?

[C]构造函数依赖

让我们自由地自己创建两个对象。我们还要在构造函数中表达依赖权:

Question q = new Question(...);
Answer a = new Answer(q, ...);   // answer can't exist without a question

这为开发人员提供了提示,因为不能毫无疑问地创建答案。但是,我们看不到“语言”表明答案已“添加”到问题。另一方面,我们真的需要看到它吗?

[D]构造函数依赖项,采用#2

我们可以做相反的事情:

Answer a1 = new Answer("",...);
Answer a2 = new Answer("",...);
Question q = new Question("", a1, a2);

这与上述情况相反。在这里,答案可以不存在任何问题(没有意义)而存在,但是问题也可以不存在任何问题(没有意义)而存在。此外,“语言”这里是对这个问题更加清晰将答案。

[E]常用方式

这就是我所说的通用方式,这是ppl通常要做的第一件事:

Question q = new Question("",...);
Answer a = new Answer("",...);
q.addAnswer(a);

这是以上两个答案的“宽松”版本,因为答案和问题都可能不存在。没有特别提示,您必须将它们绑定在一起。

[F]合并

还是应该结合使用C,D,E-涵盖如何建立关系的所有方式,从而帮助开发人员使用最适合他们的东西。

我知道人们可能会基于“预感”选择上述答案之一。但是我不知道上述任何一种变种是否比其他变种更好,并且有充分的理由。另外,请不要在上面的问题内思考,我想在此介绍一些可用于大多数情况的最佳实践-如果您同意,创建某些实体的大多数用例是相似的。同样,在这里让我们对技术不可知。我不想考虑是否要使用ORM。只想要好的表达方式。

任何智慧吗?

编辑

请忽略的其他属性QuestionAnswer,他们是不相关的问题。我在上面的文本中进行了编辑,并更改了大多数构造函数(在需要的地方):现在它们接受任何需要的必要属性值。那可能只是一个问题字符串,或者是使用不同语言,状态等形式的字符串映射-不管传递什么属性,它们都不是重点;)因此,只要假设我们上面传递了必要的参数即可,除非另有说明。谢谢!

Answers:


6

更新。考虑了澄清。

看起来这是一个选择域,通常具有以下要求

  1. 一个问题必须至少有两个选择,以便您可以选择
  2. 必须至少有一个正确的选择
  3. 毫无疑问应该没有选择

基于以上

[A]无法确保第1点的不变性,您可能会毫无疑问地遇到一个问题

[B][A]具有相同的缺点

[C][A][B]具有相同的缺点

[D]是有效的方法,但最好将选择作为列表传递,而不要单独传递

[E]具有与[A][B][C]相同的缺点

因此,我会选择[D],因为它可以确保遵循点1、2和3的域规则。即使您说一个问题很长一段时间都没有选择余留的可能性很小,但通过代码传达域要求始终是一个好主意。

我也将重命名为AnswerChoice因为在该领域对我来说更有意义。

public class Choice implements ValueObject {

    private Question q;
    private final String txt;
    private final boolean isCorrect;
    private boolean isSelected = false;

    public Choice(String txt, boolean isCorrect) {
        // validate and assign
    }

    public void assignToQuestion(Question q) {
        this.q = q;
    }

    public void select() {
        isSelected = true;
    }

    public void unselect() {
        isSelected = false;
    }

    public boolean isSelected() {
        return isSelected;
    }
}

public class Question implements Entity {

    private final String txt;
    private final List<Choice> choices;

    public Question(String txt, List<Choice> choices) {
        // ensure requirements are met
        // 1. make sure there are more than 2 choices
        // 2. make sure at least 1 of the choices is correct
        // 3. assign each choice to this question
    }
}

Choice ch1 = new Choice("The sky", false);
Choice ch2 = new Choice("Ceiling", true);
List<Choice> choices = Arrays.asList(ch1, ch2);
Question q = new Question("What's up?", choices);

一张纸条。如果您将Question实体设为集合根,而Choice值对象则成为同一集合的一部分,那么即使Choice没有将其分配给a ,也没有机会存储a Question(即使您没有将对Questionas的直接引用作为参数传递给Choice的构造函数),因为存储库只能使用根目录,并且一旦构建Question就可以在构造函数中分配所有选择。

希望这可以帮助。

更新

如果确实困扰您如何在提出问题之前创建选择,那么可能会发现一些有用的技巧

1)重新排列代码,使其看起来像在问题之后或至少同时创建

Question q = new Question(
    "What's up?",
    Arrays.asList(
        new Choice("The sky", false),
        new Choice("Ceiling", true)
    )
);

2)隐藏构造函数并使用静态工厂方法

public class Question implements Entity {
    ...

    private Question(String txt) { ... }

    public static Question newInstance(String txt, List<Choice> choices) {
        Question q = new Question(txt);
        for (Choice ch : choices) {
            q.assignChoice(ch);
        }
    }

    public void assignChoice(Choice ch) { ... }
    ...
}

3)使用构建器模式

Question q = new Question.Builder("What's up?")
    .assignChoice(new Choice("The sky", false))
    .assignChoice(new Choice("Ceiling", true))
    .build();

但是,一切都取决于您的域。从问题域的角度来看,大多数情况下,对象创建的顺序并不重要。更重要的是,一旦获得类的实例,它在逻辑上就已经完整并且可以使用了。


过时的。经过澄清,以下所有内容均与该问题无关。

首先,根据DDD域模型应该在现实世界中有意义。因此,几点

  1. 一个问题可能没有答案
  2. 没有问题就没有答案
  3. 一个答案应该恰好对应一个问题
  4. “空”答案不回答问题

基于以上

[A]可能与第4点矛盾,因为它很容易被滥用并且忘记设置文本。

[B]是有效的方法,但需要可选的参数

[C]可能与第4点矛盾,因为它允许没有文字的答案

[D]与第1点矛盾,可能与第2点和第3点矛盾

[E]可能与第2、3和4点矛盾

其次,我们可以利用OOP功能来强制域逻辑。也就是说,我们可以将构造函数用于必需的参数,将setter用于可选的参数。

第三,我将使用普遍适用的语言,该语言对于该领域而言更自然。

最后,我们可以使用DDD模式(如聚合根,实体和值对象)来设计所有功能。我们可以将问题作为其汇总的根,而将答案作为其一部分。这是一个合理的决定,因为答案在问题的上下文之外没有任何意义。

因此,以上所有内容都归结为以下设计

class Answer implements ValueObject {

    private final Question q;
    private String txt;
    private boolean isCorrect = false;

    Answer(Question q, String txt) {
        // validate and assign
    }

    public void markAsCorrect() {
        isCorrect = true;
    }

    public boolean isCorrect() {
        return isCorrect;
    }
}

public class Question implements Entity {

    private String txt;
    private final List<Answer> answers = new ArrayList<>();

    public Question(String txt) {
        // validate and assign
    }

    // Ubiquitous Language: answer() instead of addAnswer()
    public void answer(String txt) {
        answers.add(new Answer(this, txt));
    }
}

Question q = new Question("What's up?");
q.answer("The sky");

PS回答您的问题时,我对您的域做出了一些不正确的假设,因此请根据您的具体情况随意调整以上内容。


1
总结:这是B和C的混合。请参阅我对要求的说明。您的观点1.在提出问题时可能只存在“很短”的时间;但不在数据库中。从这个意义上讲,4.应该永远不会发生。我希望现在的要求是明确的;)
lawpert 2014年

顺便说一句,在澄清之后,对我来说,似乎addAnswer还是assignAnswer比不上更好的语言answer,我希望您对此表示同意。无论如何,我的问题是-您是否仍会求B,例如在answer方法中拥有大多数参数的副本?那不是重复吗?
lawpert 2014年

抱歉,要求不清楚,您是否愿意更新答案?
Lawpert 2014年

1
原来我的假设是不正确的。我将您的质量检查域视为stackexchange网站的示例,但它看起来更像是多项选择测试。当然,我会更新我的答案。
zafarkhaja 2014年

1
@lawpert Answer是一个值对象,它将使用其聚合的聚合根存储。您不会直接存储值对象,也不会保存不属于其集合根的实体。
zafarkhaja 2014年

1

如果要求是如此简单,以至于存在多种可能的解决方案,则应遵循KISS原则。在您的情况下,这将是选项E。

在某些情况下,也会创建表示某些内容的代码,而不应该表达某些内容。例如,将对问题的答案的创建(A和B)或对问题的答案的引用(C和D)绑定在一起,会添加一些不必要的行为,这对于域来说可能是令人困惑的。同样,在您的情况下,Question很可能与Answer合并,而Answer将是一个值类型。


1
为什么[C]是不必要的行为?如我所见,[C]表示没有问题,答案就无法生存,这就是事实。此外,想象一下“答案”是否需要更多的标志(例如答案类型,类别等)。进入KISS,我们失去了对强制性知识的了解,开发人员必须首先知道他需要为答案添加/设置的内容以使其正确。我相信这里的问题不是要对这个非常简单的示例进行建模,而是要找到一种更好的做法来使用OO编写无处不在的语言。
igor 2014年

@igor E已经通过强制将答案分配给要存储的问题来告知答案是问题的一部分。如果有一种方法可以只保存Answer而不加载问题,那么C会更好。但这从您写的内容来看并不明显。
2014年

@igor另外,如果要将答案的创建与问题联系在一起,则A会更好,因为如果与C一起使用,则在将答案分配给问题时它会隐藏。同样,在A中阅读文本时,您应区分“模型行为”和发起此行为的人。当问题需要以某种方式初始化答案时,问题可能负责创建答案。它与“用户创建答案”无关。
2014年

出于记录目的,我在C&E之间陷入了困境:)现在,这是:“ ...通过强制为问题分配答案以将其保存为存储库。” 这意味着“强制性”部分在我们进入存储库时才出现。因此,强制性连接在编译时对开发人员不可见,并且业务规则在存储库中泄漏。这就是为什么我在这里测试[C]。也许这个演讲可以提供有关我认为C选项有关的更多信息。
igor 2014年

这是:“ ...希望将答案的创建与问题联系起来...”。我不想捆绑_creation本身。只是想表达强制性关系。(个人希望能够自己创建模型对象)。因此,我认为这与创造无关,这就是为什么我很快放弃A和B。我看不到Question是负责创建答案的原因。
igor 2014年

1

我会选择[C]或[E]。

首先,为什么不选择A和B?我不希望我的问题负责创造任何相关的价值。想象一下,如果Question有许多其他值对象-您会create为每个对象放置方法吗?或者,如果存在一些复杂的聚合,则情况相同。

为什么不[D]?因为它与我们自然界的相反。我们首先创建一个问题。您可以想象在一个网页上创建所有这些内容-用户首先会创建一个问题,对吗?因此,不是D。

[E]是KISS,就像@Euphoric说的那样。但是最近我也开始喜欢[C]。这并没有看起来那么混乱。此外,想象一下Question是否需要更多东西-开发人员必须知道他需要放入Question中以使其正确初始化的内容。尽管您说对了,但没有“视觉”语言来说明答案实际上已添加到问题中。

补充阅读

这样的问题使我想知道我们的计算机语言是否太通用而无法建模。(我知道他们必须通用才能回答所有编程要求)。最近,我试图找到一种更好的方法来使用流畅的界面来表达业务语言。这样的东西(使用sudo语言):

use(question).addAnswer(answer).storeToRepo();

即试图从任何大的* Services和* Repository类转移到较小的业务逻辑块。只是一个主意。


您是在插件中谈论特定于域的语言吗?
Lawpert 2014年

现在,当您提到时,它看起来像这样:)购买我没有任何丰富的经验。
igor 2014年

2
我认为,到目前为止,已经有一个共识,即IO是正交的可重复性,因此不应由实体(storeToRepo)处理
Esben Skov Pedersen

我同意@Esben Skov Pedersen的观点,即实体本身不应在内部调用回购协议(这就是您所说的,对吧?);但是作为AFAIU,我们后面有某种构建器模式可以调用命令;因此此处的IO未在实体中完成。至少我是这样理解的;)
Lawpert 2014年

@lawpert是正确的。我看不到它应该如何工作,但会很有趣。
Esben Skov Pedersen 2014年

1

我相信您在这里遗漏了一点,您的聚合根应该是您的测试实体。

如果确实如此,我相信TestFactory将最适合回答您的问题。

您可以将“问题与答案”构建委托给Factory,因此基本上可以使用您想到的任何解决方案而不会破坏您的模型,因为您以实例化子实体的方式向客户端隐藏。

只要TestFactory是用于实例化Test的唯一接口,就可以做到这一点。

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.