兰特:枯燥的原则是不好的建议

2020-05-19 15:27:31

Dry原则可能是您开始编码时学习的第一个软件设计概念。它听起来很严肃,很有说服力,毕竟它有一个缩写!此外,不重复自己的想法与我们中的许多人喜欢为计算机编程的原因产生了深刻的共鸣:将我们从令人头脑麻木的重复工作中解放出来。这是一个非常容易理解和解释的概念(每当我讨论实体设计时,我仍然需要谷歌搜索Liskov替换),应用它通常会给你的大脑带来当它与模式匹配时的那种奇妙的兴奋。有什么不喜欢的?

嗯,防止代码中的重复通常是个好主意。但有时,我发现,在我想在这篇文章中讨论的方式上,这是适得其反的。

在两个调用者之间共享一段代码通常是个好主意。如果您有两个需要发送事务性电子邮件的服务,这两个服务将获取有关用户的一些详细信息,呈现一个模板并将电子邮件发送出去,它可能如下所示:

类OrderService:#.。def send_order_Receipt(self,user_id,order_id):user=UserService.get(User_Id)SUBJECT=f";order{order_id}已收到";body=f";您的订单{order_id}已收到,将很快处理";content=Render(';user_email.html';,user=user,body=body)email_Provider.send(user.email_address,Subject,Content)。def send_invoice(self,user_id,order_id):user=UserService.get(User_Id)SUBJECT=f";{order_id}已收到付款";body=f";订单{order_id}付款已收到,谢谢!";content=Render(';user_email.html';,user=user,body=body)email_Provider.send(user.email_address,Subject,Content)。

看看那些重复的代码!用以下几句话把它擦干是非常诱人的:

def send_transaction_email(user_id,order_id,Subject,Body):user=UserService.get(User_Id)content=Render(';user_email.html';,user=user,body=body)email_Provider.send(user.email_address,Subject,Content)。

好的!。我们将服务之间的公共代码提取到帮助器函数中,现在我们的服务如下所示:

类OrderService:#.。def send_order_Receipt(self,user_id,order_id):SUBJECT=f";order{order_id}Received";body=f";您的订单{order_id}已收到,将很快处理";send_transaction_email(user_id,,order_id,Subject,Body)类PaymentService:#.。def send_invoice(self,user_id,order_id):SUBJECT=f";{order_id}已收到付款";body=f";订单{order_id}付款已收到!";send_transaction_email(user_id,,order_id,Subject,Body)

DRY的承诺之一是它将允许我们更好地发展我们的软件;业务需求和工程约束一直在变化,如果我们需要改变这段代码的行为方式,我们只需要改变它一次,它就会在任何地方反映出来。

在上面的示例中,我们可以非常容易地更改获取用户信息的方式,甚至我们使用的电子邮件提供商也可以轻松更改。

但如果盲目应用,干式代码可能会起到促进更改的相反作用。在我们的示例中考虑一下,如果由于业务决策,PaymentService的发票邮件需要使用不同的模板,我们如何促进这一点呢?或者现在需要OrderService来检索已购买项目的列表并将其馈送到电子邮件模板中?我们将共享逻辑提取到Send_Transaction_Email方法中,导致OrderService和PaymentService变得紧密耦合:您不能只更改其中一个而不更改另一个。

当您遇到名为Helper的类时,它最不会做的事情就是帮助您。

我们再举一个例子。假设我们正在为正在工作的Web服务器编写单元测试,到目前为止我们有两个测试:

函数TestWebserver_BAD_PATH_500(t*testing.T){srv:=createTestWebserver()defer srv.Close()resp,err:=http.Get(srv.url+";/ad/path&34;)if err!=nil{t.Ftal(";)}if res.StatusCode!=500{t.Fatalf(";)}。)}Body,err:=ioutil.ReadAll(res.Body)If err!=nil{t.Ftal(";Failed Read Body Bytes";)}If String(Body)!=";500内部服务器错误:处理失败/BAD/Path";{t.Fatalf(";Body与预期";)}}函数TestWebserver_UNKNOWN_PATH_404(t*testing.T){srv:=createTestWebserver()defer srv.Close()resp,err:=http.Get(srv.url+";/UNKNOWN/PATH&34;)if err!=nil{t.Ftal(";;调用测试服务器";)}如果res.StatusCode!=404{t.Fatalal。)}if res.Header.Get(";X敏感标题";)!=";";{t.Fatalf(";期望敏感标题不会发送";)}}。

有大量的重复项需要重构!这两个测试的功能大致相同:它们启动一个测试服务器,对其进行GET调用,然后在http.Response上运行简单的断言。

func runWebserverTest(t*testing.T,Request Requester,validators[]Validator){srv:=createTestWebserver()defer srv.Close()Response:=request(t,srv)for_,validator:=Range validators{validator.Valify(t,response)}}

为了节省空间,我在这里编辑了请求者和验证器的确切定义,但是您可以在本文的要点中看到完整的实现。

func Test_DRY_BAD_PATH_500(t*Testing.T){runWebserverTest(t,getRequester(";/BAD/Path";),[]Validator{getStatusCodeValidator(500),getBodyValidator(";500内部服务器错误:无法处理/BAD/Path";),})}func Test_Dry_UNKNOWN_PATH_404(t*testing.t){。),[]验证器{getStatusCodeValidator(404),getHeaderValidator(";X-Sensitive-Header";,";";),})}。

编写新的类似测试会更快。如果我们有15个行为相似且需要相似断言的不同端点,我们可以非常简洁高效地表达它们。

我们的代码变得非常难以阅读和扩展。如果我们的测试由于将来的某些更改而失败,调试该问题的可怜的人将不得不进行大量的点击操作,直到他们很好地掌握了正在发生的事情:我们用聪明的抽象和间接的方式替换了琐碎、简单、直接的代码。

将公共代码放入可以在应用程序之间共享的库中是一种经过验证的有效实践,这在我们的行业中已经很好地确立了,当然我们并不是说我们应该停止这样做!

为了帮助我们决定何时应该干燥代码,我想提出一个想法,出自安迪·亨特(Andy Hunt)和戴夫·托马斯(Dave Thomas)最近出版的一本绝妙的书:“务实的程序员”(The Practice Programmer):

“如果一件东西适合使用它的人,那么它就是设计得很好的东西。对于代码来说,这意味着它必须通过更改来适应。因此,我们相信ETC原则:更容易改变。等。就这样。

据我们所知,每个设计原则都有一个ETC的特例。为什么脱钩是好的?因为通过隔离关注点,我们可以使每个关注点更容易改变。等。

为何单一责任原则有用呢?因为需求中的更改仅由一个模块中的更改来反映。等。

在这个精华的章节中,Hunt和Thomas探讨了这样一个想法,即评估经常冲突的设计决策有一个元原则--如果我们选择这条特定的路径,发展我们的代码库有多容易?在上面的讨论中,我们展示了干燥代码使更改变得更加困难的两种方式,要么是通过紧耦合,要么是通过阻碍可读性-这与ETC的元原则背道而驰。

认识到干代码的这些可能的含义可以帮助我们决定什么时候不应该干我们的代码;要了解我们什么时候应该这样做,让我们回到原始经文并重新检查这一原则。

干原理最初是由同一个亨特和托马斯在2000年版的书中介绍给世界的,所以他们写道:

“在一个系统内,每一项知识都必须有一个单一的、明确的、权威的表述。另一种选择是在两个或多个地方表达相同的东西。

如果你改变了一个,你必须记住改变其他的,[..]。这不是你会不会记得的问题,而是你什么时候会忘记的问题。

托马斯,这是大卫。实用程序员,第二版。主题9-重复的害处。

请注意,DRY原则最初根本不处理代码的重复或复制,相反,它讨论了系统中的一段知识没有单一的真理源表示的危险。

当我们重构Send_Transaction_Email方法以替换OrderService和PaymentService之间的重复代码时,我们混淆了重复代码和重复知识。如果两个程序在某个时间点是相同的,就不能保证将来会继续要求它们这样做。我们必须能够区分巧合共享的程序和本质上共享的程序。

回到原点,我必须承认,Dry原则毕竟是一条相当重要的建议;尽管它一直被抛来抛去,但我们应该记住这一点: