分开的清单

菜单
清晰编码说明:第二部分文章

插图道格麦弗森

清晰编码:第二部分

任何傻瓜都能写计算机能理解的代码。优秀的程序员编写人类能够理解的代码。
马丁福勒

任何与其他开发者合作的开发者都可以证明,如果代码不清楚,出现问题。在第一部分本系列中,我讨论了一些原则,以提高代码的清晰度,以防止不清楚的代码可能引起的问题。随着我们的应用程序越来越大,清晰变得更加重要,我们需要格外小心以确保我们的代码易于阅读,理解,修改或扩展。本文讨论了与面向对象编程(OOP)相关的一些更高级的原则,以提高大型应用程序的清晰度。

文章如下

注:尽管本文中的原则适用于各种编程语言,这些例子来自面向对象的JavaScript。如果你不熟悉这个,阅读我的第一篇文章为了跟上速度,以及找到一些其他资源来帮助您提高对面向对象编程的理解。

德米特定律

想象一下你是公寓大楼的办公室经理。月底到了,租金到期了。你通过办公室的投币箱,找到大多数房客的支票。但在整齐折叠的支票中,有一张纸片上凌乱的纸条,上面指示你打开309号公寓。打开床左边梳妆台的最上面的抽屉,把钱从房客的钱包里拿出来。哦,别让猫出来!如果你认为这很荒谬,是啊,你说得对。为了每月拿到租金,你不应该被要求知道一个房客如何布置他们的公寓,以及他们把钱包放在哪里。当我们用这种方式编写代码时,这也同样荒谬。

这个得墨忒耳定律,请或最小知识原理,请声明一个代码单元应该只需要有限的其他代码单元的知识,并且应该只与亲密的朋友交谈。换言之,你的班级不应该为了完成它所需要的而深入到另一个班级的几个层次。相反,类应该提供抽象,使其任何内部数据对应用程序的其余部分可用。

注:德米特定律是松耦合,请我在第一篇文章中谈到的。

作为一个例子,假设我们在你们办公室有一个部门的班。它包括各种信息,包括经理。现在,假设我们有另一个代码要通过电子邮件发送给其中一个经理。没有德米特定律,这个函数的外观如下:

函数emailManager(department)const manager firstname=department.manager.firstname;const manager lastname=department.manager.lastname;const managerfullname=`$managerfirstname$managerlastname `;const manager email=department.manager.email;发送电子邮件(managerfullname,管理电子邮件);

非常乏味!除此之外,如果在类,这很有可能会被打破。我们需要的是一个抽象级别,以使这个函数的工作更简单。

我们可以将此方法添加到等级:

getmanageremailobj:function()返回名字:this.manager.firstname,姓:this.manager.lastname,全名:`$this.manager.firstname$this.manager.lastname `,邮箱:this.manager.email;

这样,第一个函数可以重写为:

函数emailManager(department)let emailobj=department.getManagerMilobj();发送电子邮件(emailobj.fullname,邮箱obj.email);

这不仅使功能更清晰,更容易理解,但它使更新如果需要的话(尽管这也很危险,我们稍后再讨论)。你不必寻找每一个试图访问其内部信息的地方,只需更新内部方法。

设置我们的类来强制执行这一点可能很棘手。它有助于区分传统OOP对象和数据结构。数据结构应公开数据不包含任何行为。OOP对象应公开行为并限制对数据的访问。在C语言中,这些是独立的实体,你必须明确地选择其中一种类型。在JavaScript中,线条有点模糊,因为对象类型用于这两者。

以下是javascript中的数据结构:

让经理=名字:'布兰登',姓:'格雷戈里',电子邮件:'brandon@myurl.com'

请注意如何轻松访问数据。这就是重点。然而,如果我们想暴露行为,根据最佳实践,我们希望使用类上的内部变量隐藏数据:

class manager constructor(options)让firstname=options.firstname;让lastname=options.lastname;this.setfullname=函数(newfirstname,newfirstname)名字=newfirstname;lastname=newlastname;}(二)this.getfullname=function()返回`$名字$名字`;} }

现在,如果你认为这是不必要的,您是对的,在这种情况下,在像这样的简单对象中拥有getter和setter没有多大意义。当涉及到内部逻辑时,getter和setter变得重要:

class department constructor(options)//其他一些属性让manager=options.manager;this.changeManager(newManager)if(checkifManagerList(newManager))manager=newManager;//ajax调用数据库中的更新管理器this.getManager_if(checkifUserHasClearance())返回管理器;}}}

这仍然是一个小例子,但您可以看到getter和setter在这里所做的不仅仅是混淆数据。我们可以将逻辑和验证附加到对象不必担心。如果逻辑改变,我们可以在getter和setter上更改它,而不必查找和更改试图获取和设置这些属性的每一位代码。即使在构建应用程序时没有内部逻辑,不能保证你以后不需要它。你不必知道将来需要什么,你只需留出空间,以便以后添加。限制对暴露行为的对象中的数据的访问会为您提供一个缓冲区,以备以后需要时使用。

一般来说,如果对象暴露了行为,它是一个OOP对象,不应允许直接访问数据;相反,它应该提供安全访问它的方法,如上例所示。然而,如果对象的点要公开数据,这是一个数据结构,它也不应该包含行为。混合这些类型会使代码中的水变得模糊不清,并可能导致对对象数据的一些意外(有时是危险的)使用,因为其他函数和方法可能不知道与该数据交互所需的所有内部逻辑。

界面分离原理

想象一下,你得到了一份为一家主要制造商设计汽车的新工作。你的第一个任务是设计一辆跑车。你马上坐下来开始画一辆设计得很快、操控性很好的汽车。第二天,你得到管理层的报告,让你把你的跑车变成一辆运动型小货车。好吧,真奇怪,但这是可行的。你画了一辆运动型面包车。第二天,你得到了另一份报告。你的车现在既要像汽车,也要像船一样工作。可笑?好,对。没有办法设计一辆满足所有消费者需求的汽车。同样地,取决于你的应用程序,编写一个足够灵活的函数或方法来处理你的应用程序所能处理的一切,这可能是一个坏主意。

这个接口隔离原则声明不应强制任何客户端依赖它不使用的方法。简单来说,如果你的类有很多方法,并且对象的每个用户只使用其中的一部分,将对象分解为几个更集中的对象或接口更有意义。同样地,如果函数或方法包含多个分支,根据接收到的数据,它们的行为会有所不同,这是一个很好的迹象,表明你需要不同的函数或方法,而不是一个巨大的函数或方法。

这方面的一个大警告标志是传递到函数或方法中的标志。标志是布尔变量,如果为真,则会显著改变函数的行为。查看以下功能:

函数addperson(person,ismanager)if(ismanager)//添加经理else//添加员工

在这种情况下,该函数被分成两个不同的独占分支,这两个分支都不可能被使用,所以把它分解成单独的函数更为合理,因为我们打电话的时候知道这个人是不是经理。

这是一个简单的例子。更接近接口隔离原则的实际定义的一个例子是,如果一个模块包含许多处理员工的方法和处理经理的单独方法。在这种情况下,将管理器方法拆分为单独的模块更有意义,即使管理器模块是Employee模块的子类,并且共享一些属性和方法。

请注意:旗子不是自动邪恶的。如果您使用一个标志来触发一个小的可选步骤,而这两种情况下的大多数功能都保持不变,那么这个标志就可以了。我们要避免的是使用标志来创建“聪明的”代码,这使得它更难使用,编辑,并理解。只要你从中获得一些东西,复杂性就可以。但如果你增加了复杂性,却没有显著的回报,想想为什么你要这样编码。

当开发人员试图实现他们认为将来可能需要的功能时,也会发生不必要的依赖性。这有一些问题。一个,对于现在或可能根本无法使用的特性,现在在开发和测试阶段都要付出相当大的成本。两个,团队不太可能对未来的需求有足够的了解,从而为未来做好充分的准备。事情会变的,你可能不知道怎样直到第一阶段生产出来,情况才会改变。您应该编写您的函数和方法,以便稍后进行扩展,但是要小心尝试猜测代码库的未来。

坚持界面分离原则,绝对是一种平衡行为。因为抽象化可能会走得太远,对象和方法的数量也很荒谬。这个,具有讽刺意味的是,导致同样的问题:增加了复杂性而没有回报。没有严格的规则来检查这个,这取决于你的应用程序,你的数据,还有你们的团队。但如果让事情变得复杂对你没有帮助的话,保持简单并不羞耻。事实上,这通常是最好的路线。

开闭原理

许多年轻的开发人员不记得Web标准改变开发之前的日子。(谢谢,杰弗里·塞尔德曼,为了让我们的生活更轻松!)以前每当新的浏览器发布时,它对事物有自己的解释,开发人员不得不争先恐后地找出不同之处,以及它是如何破坏了他们所有的网站的。有一些文章和博客很快就betway体育注册写了关于新的浏览器怪癖以及如何解决这些怪癖的文章和博客文章,在客户发现他们的网站被破坏之前,开发人员不得不放弃一切来实现这些修复。对于许多第一次浏览器战争的勇敢老兵来说,这不仅是一个噩梦,也是我们工作的一部分。听起来很糟糕,如果我们不小心修改代码,代码也很容易做同样的事情。

这个开闭原理声明软件实体(类,模块,功能,等)应打开进行扩展,但关闭进行修改。换言之,您的代码应该以这样的方式编写:当您不允许更改现有功能时,可以轻松地添加新功能。改变现有的功能是破坏应用程序的一个好方法,常常没有意识到。就像浏览器依靠网络标准来阻止新版本破坏我们的网站一样,您的代码需要依赖自己的内部标准来保持一致性,以防止代码以意外的方式破坏。

假设您的代码库具有以下功能:

函数getfullname(person)返回`$person.firstname$person.lastname `;

一个非常简单的函数。但是,有一个新的用例,你只需要姓氏。在任何情况下如果您这样修改上述函数:

函数getfullname(person)返回名字:person.firstname,姓:person.lastname;

解决了你的新问题,但它修改了现有的功能,并将破坏使用旧版本的每一位代码。相反,您应该通过创建一个新函数来扩展功能:

函数getLastname(person)返回person.lastname;

或者,如果我们想让它更灵活:

函数getnameobject(person)返回名字:person.firstname,姓:person.lastname;

这是一个简单的例子,但很容易看出修改现有功能是如何导致重大问题的。即使您能够定位对函数或方法的每个调用,它们都必须经过测试。打开/关闭原则有助于减少测试时间和意外错误。

那么,在更大的范围内,这是什么样子的呢?假设我们有一个函数可以通过XMLHTTPROQUEST用它做点什么:

函数请求(终结点,params)const xhr=new xmlhttprequest();打开(获取),终点,真的);xhr.onreadystatechange=function()if(xhr.readystate==4&&xhr.status==200)//对数据执行某些操作;xhr.send(params);请求(https://myapi.com','id=91');

如果你总是用这些数据做同样的事情,那就太好了。但这会发生多少次呢?如果我们这样做别的有了这些数据,以这种方式对函数进行编码意味着我们将需要另一个函数来做几乎相同的事情。

更好的方法是对请求函数进行编码,以接受回调函数作为参数:

函数请求(终结点,帕拉姆斯callback)const xhr=new xmlhttprequest();打开(获取),终点,真的);xhr.onreadystatechange=function()if(xhr.readystate==4&&xhr.status==200)回调(xhr.responseText);};xhr.send(params);const-defaultaction=function(responsetext)//对数据执行某些操作const-alternateaction=function(responsetext)//对数据执行不同的操作request('https://myapi.com','id=91',defaultaction);request('https://myapi.com','id=42',alternateaction);

以这种方式编码的函数,它对我们来说更加灵活和有用,因为在不修改现有功能的情况下,很容易添加新功能。将函数作为参数传递是我们在保持代码可扩展性方面最有用的工具之一,因此,当您在编码时要记住这一点,以便将来验证您的代码。

保持清晰

聪明的代码在不提高清晰度的情况下增加了复杂性,这对任何人都没有帮助。我们的应用程序越大,越是清晰重要,为了确保我们的代码是清晰的,我们需要计划的越多。遵循这些准则有助于提高透明度和降低总体复杂性,导致更少的错误,时间更短,更快乐的开发者。它们应该是任何复杂应用程序的考虑因素。

谢谢

特别感谢Zell Liew学习javascript因为他对这篇文章的技术监督。学习javascript是将您的javascript专业知识从初学者转移到高级人员的一个很好的资源,所以值得一看,以进一步了解您的知识!

2读者意见

加载注释