【问题标题】:Is business logic in constructors a good idea?构造函数中的业务逻辑是个好主意吗?
【发布时间】:2016-12-15 00:00:52
【问题描述】:

我目前正在重建一个专门的工单系统(主要用于支持有遥感硬件故障的人......)。无论如何,我想知道在对象的构造函数中执行大量工作流类型的活动是否是个好主意。

比如目前有这样的:

$ticket = new SupportTicket(
    $customer,
    $title,
    $start_ticket_now,
    $mail_customer
);

一旦创建了对象,它就会将一行存入数据库,去给客户发一封确认电子邮件,可能会向最近的技术人员发送一条短信等等。

构造函数应该启动所有这些工作,还是应该像下面这样?

$ticket = new SupportTicket($customer, $title);
$customer->confirmTicketMailed($ticket);
$helpdesk->alertNewTicket($ticket);

如果有帮助,对象都基于 ActiveRecord 样式。

我想这可能是一个见仁见智的问题,但您认为最好的做法是什么?

【问题讨论】:

    标签: oop


    【解决方案1】:

    如果构造函数完成了所有这些工作,那么构造函数就知道许多其他域对象。这会产生依赖性问题。 ticket真的应该知道CustomerHelpDesk吗?当添加新功能时,新的域对象是否可能会添加到工作流中,这是否意味着我们可怜的 ticket 将不得不了解不断增加的域对象数量?

    像这样的依赖蜘蛛网的问题是任何一个域对象的源代码更改都会对我们可怜的ticket产生影响。 ticket 将拥有如此多系统知识,因此无论发生什么,ticket 都会参与其中。您会发现讨厌的if 语句聚集在该构造函数中,检查配置数据库、会话状态以及许多其他事情。 ticket将成长为神级。

    我不喜欢构造函数的另一个原因是它使它们周围的对象很难测试。我喜欢写很多模拟对象。当我针对customer 编写测试时,我想通过模拟ticket。如果ticket 的构造函数控制工作流,以及customer 和其他域对象之间的舞蹈,那么我不太可能模拟它来测试customer

    我建议您阅读The SOLID Principles,这是我几年前写的一篇关于管理面向对象设计中的依赖关系的论文。

    【讨论】:

    • 上帝直接对你说话,感到有福了。 Robert C. Martin 回复了你的答案:O
    【解决方案2】:

    把事情分开。真的,您不希望票证知道它应该如何发送电子邮件等。这是类服务的工作。

    [更新] 我不认为建议的工厂模式对此有好处。如果您想创建票证的不同实现而不将此逻辑放入票证本身(例如通过重载的构造函数),工厂将很有用。

    看看领域驱动设计中提出的服务概念。

    服务:当操作在概念上不属于任何对象时。遵循问题的自然轮廓,您可以在服务中实现这些操作。服务概念在 GRASP 中称为“Pure Fabrication”。

    【讨论】:

      【解决方案3】:

      从 OO 设计的角度来看,问题不在于该功能是否应该在构造函数中实现(而不是在该类的其他方法中),而是 SupportTicket 类是否应该知道如何完成所有这些事物。

      简而言之,SupportTicket 类应该建模支持票,并且只是支持票。创建电子邮件、知道如何将该电子邮件发送给客户、排队处理票证等都是您应该从 SupportTicket 类移出并封装到其他地方的功能块。这样做的好处包括更低的耦合性、更高的内聚性和更高的可测试性。

      查看Single Responsibility Principle,它解释了这样做的好处。特别是,this 博客是阅读 SRP 和其他良好 OO 设计的关键原则的好地方。

      【讨论】:

      • 这真是一针见血。是否使用工厂(如其他帖子所述)无关紧要。
      【解决方案4】:

      实际上,我从未对任何可用的答案感到满意,但让我们看看它们。选择围绕两个评估问题:

      E1.业务逻辑的知识属于哪里?

      E2. 下一位阅读代码的人会去哪里? (Screechingly Obvious Code)

      一些选择:

      • 客户端代码(执行“新 SupportTicket”的对象)中。显然,它可能不/不应该知道业务逻辑,否则您不会想要创建那个花哨的构造函数。如果它是业务逻辑的正确位置,那么它应该说:

        $ticket = new SupportTicket($customer, $title);
        
        handleNewSupportTicket($ticket, ...other parameters...)
        

        其中,为了保护 E2,“handlenewSupportTicket”是定义业务逻辑的地方(下一个程序员可以很容易地找到它)。

      • support ticket 对象中,作为单独的业务调用。就个人而言,我对此并不满意,因为这是来自客户端代码的两次调用,其中思想是一件事。

        $ticket = new SupportTicket($customer, $title);
        
        $ticket -> handleNewSupportTicket(...other parameters...)
        
      • 支持票类中。这里预计业务逻辑位于 Support Ticket 区域,但由于新的 Support Ticket 绝对需要立即处理,而不是稍后处理,所以这个重要任务不能留给任何人的想象或错误输入,即具体不要交给客户端代码。我只知道如何在Smalltalk 中编写类方法,但我会尝试使用伪代码:

        $ticket = SupportTicket.createAndHandleNewSupportTicket(...other parameters...)
        

        假设客户端代码需要新票的句柄用于其他目的(否则“$ticket =”会消失)。

        我不太喜欢这个,因为其他程序员并不觉得在类或静态方法中寻找业务逻辑很自然。但这是第三种选择。

      • 唯一的第四个选择是,如果业务逻辑愉快地驻留在另一个地方,其他程序员自然会寻找它,在这种情况下,它会进入那里的类/静态函数。

        $ticket = SupportTicketBusinessLogic.createAndHandleNewSupportTicket(...other params...)
        

        该类/静态函数在哪里进行所需的多次调用。 (但现在我们再次有可能构建票证但无法正确处理:(。

      【讨论】:

        【解决方案5】:

        简短的回答是否定的。

        在硬件设计中,我们曾经有过这样的说法,“不要在时钟或复位线上设置门——它会掩盖逻辑。”出于同样的原因,这同样适用于此。

        更长的答案将不得不等待,但请参阅“ScreetchinglyObvious Code”。

        【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-02-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-01-04
        • 1970-01-01
        相关资源
        最近更新 更多