在构造函数中编写业务逻辑是一个好的想法吗?

25

我目前正在重建工作中的一个专业票务系统(主要用于支持远程传感器硬件故障处理...)。无论如何,我想知道在对象的构造函数中进行大量工作流类型的活动是否是一个好主意。

例如,当前有这样的代码:

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

对象一旦被创建,它会将一行数据插入数据库,发送确认电子邮件给客户,可能会向最近的技术人员发送文本消息等等。

构造函数是否应该触发所有这些工作,或者更像以下的方式?

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

如果有帮助的话,这些对象都是基于ActiveRecord风格的。

我猜这可能是一个主观问题,但你认为最好做什么?

5个回答

57
如果构造函数完成所有工作,那么构造函数就会了解许多其他领域对象。这会创建一个依赖性问题。票务是否真的需要知道客户和HelpDesk?当添加新功能时,新的领域对象是否可能被添加到工作流程中,这不意味着我们可怜的票务将不得不了解越来越多的领域对象吗?
这种依赖关系的蜘蛛网问题在于,对任何一个领域对象进行源代码更改都会影响到我们可怜的票务。无论发生什么事情,票务都将拥有如此多的系统知识,您将发现构造函数内部聚集了许多恶劣的if语句,检查配置数据库、会话状态和许多其他内容。票务将成长为一个神类。
我不喜欢构造函数执行操作的另一个原因是它使周围的对象非常难以测试。我喜欢编写大量模拟对象。当我针对客户编写测试时,我希望传递一个模拟的票务。如果票务的构造函数控制工作流程和客户与其他领域对象之间的交互,则很难将其模拟出来以测试客户端。

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


7
神直接对你说话了,感到受祝福。Robert C. Martin 回复了你的回答 :O - Diego Palomar

4

将事情分开。实际上,你不希望一个票知道它应该如何发送电子邮件等等。这是类似于服务的工作。

[更新] 我认为建议的工厂模式对此并不好。如果您想要创建不同的票务实现而不是通过重载构造函数(例如),则可以使用工厂。

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

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


4
从面向对象设计的角度来看,问题并不在于该功能是否应该在构造函数中实现(而不是在该类的其他方法中实现),而在于SupportTicket类是否应该知道如何执行所有这些操作。
简而言之,SupportTicket类应该建模支持票据,只是支持票据。创建电子邮件,知道如何将该电子邮件发送给客户,将票据排队进行处理等,都是您应该从SupportTicket类中移动并封装到其他地方的功能块。这样做的好处包括更低的耦合性,更高的内聚性和更好的可测试性。
请查看单一职责原则,它解释了这样做的好处。特别是,this博客是阅读SRP和良好的面向对象设计的其他基石原则的好地方。

这正中要害。无论你是否使用工厂(如其他帖子所提到的)都是无关紧要的。 - Egwor

2

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

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

E2. 下一个代码读者会在哪里寻找?(显而易见的代码)

一些选择:

  • In the client code (the object that does "new SupportTicket"). It probably doesn't / shouldn't know the business logic, evidently, or else you wouldn't be wanting to create that fancy constructor. If it IS the right place for the business logic, then it should say:

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

    where, in order to protect E2, "handlenewSupportTicket" is the place where that business logic is defined (and the next programmer can easily find it).

  • In the support ticket object, as a separate business call. Personally, I'm not really happy with this, because it's two calls from the client code, where the mental thought is 1 thing.

    $ticket = new SupportTicket($customer, $title);
    
    $ticket -> handleNewSupportTicket(...other parameters...)
    
  • In the class of support tickets. Here it is expected that the business logic resides in the Support Ticket area, but since new support tickets absolutely need to be handled immediately and not later, this important task can't be left to anyone's imagination or mistyping, i.e., specifically not to the client code. I only know how to code class methods in Smalltalk, but I'll take a stab at pseudo-code:

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

    Assuming that the client code needs the handle to the new ticket for other purposes (otherwise the "$ticket =" would vanish).

    I'm not awfully fond of this, because other programmers don't find it so natural to look for business logic in class or static methods. But it's the third choice.

  • The only fourth choice is if there is another place the business logic happily resides and other programmers will naturally look for it, in which case it goes into a class/static function there.

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

    where that class/static function does the multiple calls needed. (But now we once again have the possibility that tickets can be constructed and not handled properly :(.


2
简单回答是不行的。
在硬件设计中,我们曾经有一句话:“不要在时钟或复位线上放置门 - 它会遮盖逻辑。”出于相同的原因,在这里也是如此。
更长的答案还需要等待,但可以参考 "Screetchingly Obvious Code"。

根据此上下文,用户“Alistair”很可能是Alistair Cockburn - Peter Mortensen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接