在更新方法中添加返回类型是否违反“单一职责原则”?


37

我有一种更新数据库中员工数据的方法。该Employee班是不变的,所以“更新”的对象实际上手段来实例化一个新的对象。

我希望该Update方法返回Employee具有更新数据的新实例,但是由于现在我可以说该方法的责任是更新员工数据,从数据库中获取新Employee对象,这是否违反了单一职责原则

数据库记录本身已更新。然后,实例化一个新对象来表示该记录。


5
少量的伪代码可能会在这里走很长的路:实际上是创建了新的数据库记录,还是数据库记录本身已更新,但是在“客户端”代码中,由于该类被建模为不可变的,因此创建了新的对象?
马丁巴

除了Martin Bra询问的内容之外,为什么在更新数据库时返回Employee实例?Employee实例的值在Update方法中没有更改,所以为什么要返回它(调用者已经可以访问该实例...)。还是update方法还从数据库中检索(可能不同)值?
Thorsal '16

1
@Thorsal:如果您的数据实体是不可变的,则返回具有更新数据的实例几乎是SOP,因为否则您必须自己对修改后的数据进行实例化。
mikołak

21
Compare-and-swap并且test-and-set是多线程编程理论中的基本操作。它们都是带有返回类型的更新方法,否则将无法正常工作。这是否会打破诸如命令查询分离或单一职责原则之类的概念?是的,这就是重点。SRP并不是普遍的好事,实际上可能是有害的。
MSalters

3
@MSalters:是的。命令/查询分离说,应该可以发出可以被认为是幂等的查询,并且不必等待答复就可以发出命令,但是原子的读-修改-写应该被认为是第三类操作。
超级猫

Answers:


16

与任何规则一样,我认为这里重要的是要考虑规则的目的,精神,而不是被困在准确分析某本教科书中该规则的措辞以及如何将其应用于这种情况下。我们不需要像律师那样处理这种情况。规则的目的是帮助我们编写更好的程序。编写程序的目的并不是要遵守规则。

单一职责规则的目的是通过使每个功能执行一项独立的,连贯的工作,使程序更易于理解和维护。

例如,我曾经编写过一个函数,该函数调用了诸如“ checkOrderStatus”之类的函数,该函数确定订单是否处于待处理,已发货,延期交货等任何情况,并返回指示哪一个的代码。然后另一位程序员来了,并修改了此功能,以便还可以在发货时更新现有数量。这严重违反了单一责任原则。另一位稍后阅读该代码的程序员将看到函数名称,返回值的使用方式,并且可能永远不会怀疑它进行了数据库更新。需要在不更新现有数量的情况下获得订单状态的人将处于尴尬的位置:他是否应该编写一个新功能来复制订单状态部分?添加一个标志来告诉它是否执行数据库更新?等等。

另一方面,我不会挑剔构成“两件事”的东西。我最近刚刚编写了一个函数,该函数将客户信息从我们的系统发送到我们客户的系统。该功能对数据进行了一些重新格式化,以满足其要求。例如,我们在数据库中有一些字段可能为空,但是它们不允许为空,因此我们必须填写一些虚拟文本(“未指定”),否则我会忘记确切的单词。可以说,此功能在做两件事:重新格式化数据并发送。但是我非常有意地将此功能放在一个函数中,而不是具有“重新格式化”和“发送”功能,因为我永远都希望不重新格式化就发送。我不想有人写一个新电话,却不知道他必须先重新格式化然后再发送。

在您的情况下,更新数据库并返回所写记录的图像似乎是两件事,它们很可能在逻辑上不可避免地结合在一起。我不知道您的应用程序的详细信息,所以我不能确切地说这是否是个好主意,但这听起来似乎很合理。

如果要在内存中创建一个对象,以保存记录的所有数据,请执行数据库调用以写入此对象,然后返回该对象,则很有意义。您手中有物体。为什么不把它交还?如果您没有返回对象,调用者将如何获取它?他是否必须读取数据库才能获取刚刚编写的对象?这似乎效率很低。他将如何找到记录?你知道主键吗?如果有人声明write函数返回主键是“合法的”,以便您可以重新读取记录,那么为什么不只返回整个记录,而不必这样做呢?有什么不同?

另一方面,如果创建对象的工作量与编写数据库记录的工作完全不同,并且调用者很可能希望执行写操作而不创建对象,那么这可能是浪费的。如果调用者可能想要该对象但不执行该写操作,则您必须提供另一种获取该对象的方法,这可能意味着编写冗余代码。

但是我认为方案1更有可能,所以我想说,可能没有问题。


感谢您提供所有答案,但这确实对我帮助最大。
西波

如果您不想在不重新格式化的情况下进行发送,并且除了稍后再发送数据之外就没有重新格式化的用途,那么它们是一回事,而不是两件事。
Joker_vD

public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
CJ Dennis

@CJDennis当然。实际上,这就是我的方法:执行格式化的功能,执行实际发送的功能,还有另外两个我将不介绍的功能。然后,一个顶级函数按适当的顺序调用它们。您可以说“格式化并发送”是一种逻辑操作,因此可以将其正确地组合为一个功能。如果您坚持认为它是两个,那么合理的做法是拥有一个可以完成所有任务的顶层函数。
杰伊

67

SRP的概念是在单独执行模块时停止模块执行2种不同的操作,从而可以更好地进行维护并及时减少意大利面条。正如SRP所说的“改变的一个理由”。

对于您的情况,如果您有一个例程“更新并返回更新的对象”,则您仍在更改对象一次-给它1个更改的理由。您返回的对象既不在这里也不在那里,您仍在对该单个对象进行操作。该代码仅负责一件事情。

SRP并不是真正要尝试将所有操作减少到一个调用,而是要减少您的操作以及操作方式。因此,返回更新对象的单个更新就可以了。


7
另外,它不是“试图将所有操作减少到一个调用中”,而是减少了在使用相关项目时必须考虑的几种不同情况。
jpmc26 2016年

46

与往常一样,这是程度的问题。SRP应该阻止您编写从外部数据库检索记录,对其执行快速傅立叶变换并使用结果更新全局统计信息注册表的方法。我认为几乎每个人都会同意这些事情应该通过不同的方法来完成。Postulating一个单一的每个方法的责任仅仅是为了说明这一点最经济,最令人难忘的方式。

在频谱的另一端是产生有关对象状态信息的方法。典型的isActive将提供此信息作为其唯一责任。可能每个人都同意这是可以的。

现在,有些人将原理扩展到了如此之远,以至于他们认为返回成功标志与执行报告其成功的操作有不同的责任。在极其严格的解释下,这是正确的,但是由于替代方法是必须调用第二种方法来获取成功状态,这使调用程序变得复杂,因此许多程序员完全可以从带有副作用的方法中返回成功代码。

返回新对象是实现多重职责的又一步。要求调用方对整个对象进行第二次调用比要求仅查看第一个是否成功的第二次调用稍微更合理。尽管如此,许多程序员还是会考虑完全返回更新结果。虽然这可以解释为两个稍有不同的职责,但肯定不是启发该原则开始的严重滥用之一。


15
如果必须调用第二个方法以查看第一个方法是否成功,是否不应该调用第三个方法来获取第二个方法的结果?然后,如果您实际上想要结果,那么……

3
我可能还要补充一点,SRP的过度狂热应用导致了无数的小类,这本身就是一种负担。多大的负担取决于环境,编译器,IDE /辅助工具等
埃里克Alapää

8

是否违反了单一责任原则?

不必要。如果有的话,这违反了命令查询分离的原理。

责任是

更新员工数据

隐含地理解此责任包括操作状态;例如,如果操作失败,则引发异常,如果操作成功,则返回更新员工,等等。

同样,这都是程度和主观判断的问题。


那么命令查询分离又如何呢?

好的,这个原则是存在的,但是实际上返回更新结果是很常见的。

(1)Java Set#add(E)添加了元素,并返回其先前包含在集合中的元素。

if (visited.add(nodeToVisit)) {
    // perform operation once
}

这比可能需要执行两次查询的CQS替代方案效率更高。

if (!visited.contains(nodeToVisit)) {
    visited.add(nodeToVisit);
    // perform operation once
}

(2)比较和交换获取和添加以及测试和设置是允许存在并发编程的通用原语。从低级CPU指令到高级并发集合,这些模式经常出现。


2

唯一的责任是关于一个类不需要出于多个原因进行更改。

例如,一名雇员有一个电话号码列表。当您更改电话号码的方式(可以添加国家/地区电话代码)时,根本不应更改员工类别。

我不需要员工类知道它如何将自身保存到数据库中,因为随着员工的变化和数据存储方式的变化,它会随之变化。

类似地,雇员类中不应有CalculateSalary方法。可以使用薪水属性,但应在其他地方进行税金等的计算。

但是Update方法返回它刚刚更新的内容就可以了。


2

Wrt。具体情况:

Employee Update(Employee, name, password, etc) (因为我有很多参数,所以实际上使用了Builder)。

似乎Update方法将现有Employee的第一个参数来识别(?)的现有员工和一组参数来改变这个员工。

确实认为可以这样做更清洁。我看到两种情况:

(a) Employee实际上包含一个数据库/唯一ID,通过它可以始终在数据库中对其进行标识。(也就是说,你根本没有需要设置为找到它在DB全程记录值。

在这种情况下,我希望使用一种void Update(Employee obj)方法,该方法仅通过ID查找现有记录,然后从传递的对象中更新字段。或者也许void Update(ID, EmployeeBuilder values)

我发现有用的一种变化是仅具有一种void Put(Employee obj)插入或更新的方法,具体取决于记录(按ID)是否存在。

(b)数据库查找需要完整的现有记录,在这种情况下,具有以下条件可能更有意义void Update(Employee existing, Employee newData)

就我在这里看到的而言,我的确会说建立一个新对象(或子对象值)以存储和实际存储它的责任是正交的,因此我将它们分开。

到目前为止,其他答案(原子设置和检索/比较交换等)中提到的并发需求并不是我一直在研究的DB代码中的问题。与数据库对话时,我认为这应该在事务级别上正常处理,而不是在单个语句级别上处理。(这并不是说可能没有“原子的”设计Employee[existing data] Update(ID, Employee newData)没有意义的设计,但是通过数据库访问它并不是我通常看到的东西。)


1

到目前为止,这里的每个人都在谈论课堂。但是从接口角度考虑。

如果接口方法声明了返回类型,和/或关于返回值的隐式承诺,则每个实现都需要返回更新的对象。

所以你可以问:

  • 您能想到不想打扰返回新对象的可能实现吗?
  • 您能想到依赖于接口(通过注入实例)的组件,这些组件不需要将更新的员工作为返回值吗?

还要考虑单元测试的模拟对象。显然,没有返回值将更容易进行模拟。如果依赖组件的注入依赖项具有更简单的接口,则它们更易于测试。

基于这些考虑,您可以做出决定。

而且,如果您以后感到后悔,您仍然可以引入第二个接口,两者之间带有适配器。当然,这样的附加接口会增加合成的复杂性,因此这是一个折衷方案。


0

您的方法很好。不变性是一个强有力的主张。我唯一要问的是:在构造对象的地方还有其他地方吗?如果您的对象不是一成不变的,则必须回答其他问题,因为引入了“状态”。并且对象的状态改变可能以不同的原因发生。然后,您应该知道您的案例,并且它们不应是多余的或分散的。

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.