我目前正在重建一个专门的票务系统(主要用于支持遥感硬件故障的人......)。无论如何,我想知道在对象的构造函数中做很多工作流类型活动是个好主意。
例如,目前有:
$ticket = new SupportTicket(
$customer,
$title,
$start_ticket_now,
$mail_customer
);
创建对象后,它会将一行放入数据库,然后向客户发送确认电子邮件,可能会向最近的技术人员发送短信等。
构造函数应该解雇所有这些工作,还是更像下面的工作?
$ticket = new SupportTicket($customer, $title);
$customer->confirmTicketMailed($ticket);
$helpdesk->alertNewTicket($ticket);
如果有帮助,对象都基于ActiveRecord样式。
我想这可能是一个意见问题,但您认为最好的做法是什么?
答案 0 :(得分:44)
如果构造函数完成了所有工作,那么构造函数就会知道许多其他域对象。这会产生依赖性问题。 ticket
真的应该知道Customer
和HelpDesk
吗?添加新功能时,是否可能会将新的域对象添加到工作流中,这是否意味着我们的穷人ticket
必须知道不断增加的域对象数量? / p>
像这样依赖的spiderwebs的问题是,对任何一个域对象的源代码更改都会对我们的穷人ticket
产生影响。 ticket
将拥有对系统的知识,无论发生什么,都会涉及ticket
。您将在该构造函数中收集讨厌的if
语句,检查配置数据库,会话状态以及许多其他内容。 ticket
将成长为神级。
我不喜欢做事情的构造函数的另一个原因是它使周围的对象很难测试。我喜欢写很多模拟对象。当我针对customer
编写测试时,我想将其传递给模拟ticket
。如果ticket
的构造函数控制工作流,以及customer
和其他域对象之间的舞蹈,那么我不太可能将其模拟出去测试customer
。
我建议你阅读The SOLID Principles,这是我几年前写的关于管理面向对象设计中的依赖关系的论文。
答案 1 :(得分:4)
将事情分开。真的,你不希望机票知道它应该如何发送电子邮件等。这是类似服务的工作。
[更新]我不认为建议的工厂模式对此有好处。如果要创建票证的不同实现而不将此逻辑放入票证本身(例如,通过重载构造函数),则工厂非常有用。
查看域驱动设计中提出的服务概念。
服务:当某个操作在概念上不属于任何对象时。根据问题的自然轮廓,您可以在服务中实现这些操作。服务理念在GRASP中被称为“纯粹制造”。
答案 2 :(得分:4)
从OO设计的角度来看,问题不在于该功能是否应该在构造函数中实现(而不是在该类的其他方法中),而是SupportTicket类是否应该知道如何执行所有这些功能的东西。
简而言之,SupportTicket类应该为支持票证建模,并且只对支持票证建模。创建一封电子邮件,了解如何将该电子邮件发送给客户,将票据排队等等,这些都是您应该从SupportTicket类移动并封装在其他地方的功能块。这样做的好处包括更低的耦合,更高的内聚力和更好的可测试性。
查看Single Responsibility Principle,它解释了这样做的好处。特别是,this博客是阅读SRP以及良好OO设计的其他基本原则的好地方。
答案 3 :(得分:2)
简短的回答是否定的。
在硬件设计方面,我们曾经说过,“不要在时钟或复位线上放置一个门 - 它会掩盖逻辑。”出于同样的原因,这同样适用于此。
较长的答案必须等待,但请参阅“Screetchingly Obvious Code”。
答案 4 :(得分:1)
实际上我对任何可用的答案都不满意,但让我们来看看它们。选择围绕两个评估问题构建:
E1。知识在哪里属于业务逻辑?
E2。代码的下一位读者会在哪里看? (Screechingly Obvious Code)
一些选择:
在客户端代码(执行“new SupportTicket”的对象)中。它可能不会/不应该知道业务逻辑,显然,否则你不会想要创建那个花哨的构造函数。如果它是业务逻辑的正确位置,那么应该说:
$ticket = new SupportTicket($customer, $title);
handleNewSupportTicket($ticket, ...other parameters...)
其中,为了保护E2,“handlenewSupportTicket”是定义业务逻辑的地方(下一个程序员可以很容易地找到它)。
在支持服务单对象中,作为单独的商家电话。就个人而言,我对此并不满意,因为它是来自客户端代码的两个调用,其中心理思想是一件事。
$ticket = new SupportTicket($customer, $title);
$ticket -> handleNewSupportTicket(...other parameters...)
在支持票类中。这里预计业务逻辑位于支持票据区域,但由于新支持票据绝对需要立即处理而不是以后处理,因此这个重要任务不能留给任何人的想象或错误输入,即特别是不要客户代码。我只知道如何在Smalltalk中编写类方法,但我会尝试伪代码:
$ticket = SupportTicket.createAndHandleNewSupportTicket(...other parameters...)
假设客户端代码需要新票证的句柄用于其他目的(否则“$ ticket =”将消失)。
我并不是非常喜欢这个,因为其他程序员并不觉得在类或静态方法中寻找业务逻辑是如此自然。但这是第三种选择。
唯一的第四个选择是,如果有另一个地方,业务逻辑很乐意驻留,而其他程序员自然会寻找它,在这种情况下,它会进入类/静态函数。
$ticket = SupportTicketBusinessLogic.createAndHandleNewSupportTicket(...other params...)
其中class / static函数执行所需的多个调用。 (但现在我们再一次有可能建造门票而不能妥善处理:(。