2019-09-04 软件开发过程中:命名的艺术

https://segmentfault.com/a/1190000009108845

软件的复杂性:命名的艺术

在计算机科学中只有两件困难的事情:缓存失效和命名规范。
—— Phil Karlton

前言

编写优质代码本身是一件很困难的事情,为什么这么说?因为良好的编码风格是为了能更好的理解与阅读。通常我们会只注重前者,而忽略了后者的重要性。我们的代码虽然只编写一次,但是在阅读复审时会阅读许多次。

良好的编码习惯可以提高我们的阅读质量,比写作本身要轻松许多,我们可以站在宏观角度看待问题,远观大局,而不失细节。首先我们需要理解、分析清楚某个问题,然后用特有的,高效的,言简意赅的方式让更多人明白。对我来说,应该明确的把软件工程归属到社会科学领域。我们为谁编写代码,难道不是为了人类吗?(感觉原文作者装的有点过)

向其他人传递我们的想法以及编程思想,这就是我们在编码时要做的。

命名构造

为了说明我们的第一个概念,首先来做一个游戏,游戏名为 “我们住在哪个房间?”,如下会为你提供一张图片,请你说说看这是什么房间。

问题 1/3

[图片上传失败...(image-54a6be-1567579306511)]

从上面的图片不难看出,这肯定是客厅。基于一件物品,我们可以联想到一个房间的名称,这很简单,那么请看下图。

问题 2/3

[图片上传失败...(image-5b2c3b-1567579306511)]

基于这张图片,我们可以肯定的说,这是厕所。
通过上面两张图片,不难发现,房间的名称只是一个标签属性,有了这个标签,甚至我们不需要看它里面有什么东西。这样我们便可以建立第一个推论:

推论1:容器(函数)的名称应包含其内部所有元素

可以将这个推论理解为 鸭子类型。如果有一张床?那么它就是卧室。我们也可以反过来进行分析。

问题:基于一个容器名称,我们可以推断出它的组成部分。如果我们以卧室为例,那么很有可能这个房间有一张床。这样我们便可以建立第二个推论:

推论2:根据容器(函数)的名称推断其内部组成元素

现在我们有了两条推论,据此我们试着看下面这张图片。

问题 3/3

[图片上传失败...(image-4279c9-1567579306511)]

好吧,床和马桶在同一个房间?根据我们的推论,如上图片使我们很难立即做出判断,如果依然使用上述两条推论来给它下定义的话,那么我会称它为:怪物的房间。

这个问题并不在于同一个房间的物品数量上,而是完全不相关的物品被认作为具备同样的标签属性。在家中,我们通常会把有关联的,意图以及功能相近的东西放在一起,以免混淆视听,所以现在我们有了第三条推论:

推论3:容器(函数)的明确度与其内部组件的密切程度成正比

这可能比较难理解,所以我们用下面这一张图来做说明:

[图片上传失败...(image-fe639a-1567579306511)]

如果容器内部元素属性关联性很强,那么我们更容易找到一个用来说明它的名字。反之,元素之间的无关性越强,越难以描述说明。属性维度可能会关系到他们的功能、目的、战略,类型等等。关于命名标准,需要关联到元素自身属性才有实际意义。跟着我的思路,我们将很快明白这一点。

在软件工程方面,这个观点也同样适用。例如我们熟知的 组件函数方法服务应用。罗伯特·德拉奈曾说过:“我们的理解能力很大程度与我们的认知相关联”,那么在这种技术背景下,我们的代码是否可以使阅读者以最简单的方式感知到业务需求以及相关诉求?

Example 1:HTTP 域与汽车

HTTP 自身是一个域环境,它包含着我们的网络请求与响应状态。如果我们把一个 Car 的组件放入它的内部,那么我们不能再称它为 HTTP了,在这种情况下,它会变得让人困惑。

public interface WhatIsAGoodNameForThis {
    /* methods for a car */
    public void gas();
    public void brake();

    /* methods for an HTTP client */
    public Response makeGetRequest(String param);
}

[图片上传失败...(image-196ddf-1567579306511)]

Example 2:单词的耦合

有一种常见的命名模式,在名称时后缀附加上 Builder 或 er 一类的结束词,例如:SomethingBuilderUserBuilderAccountCreatorUserHelperJobPerformer 等等。

[图片上传失败...(image-88af5f-1567579306511)]

例如上图中的名字,我们可以推断出三件事情。第一,在类名中使用动词 Build 意味着它是具备功能性的。第二,它由两部分组成,一个是 User 用户,另一个的 Builder 构造者,这意味着它们之间可能在封装、维度归类上存在歧义。第三,Builder 构造者 可以在类内部访问 User 用户 的相关逻辑、数据,因为他们在同一纬度空间内。

这一点与工厂模式很相似,有自己的应用场景,当它在我们的工程中泛滥使用时,这将会是一个很麻烦的问题。另外,需要提醒大家,在工厂模式中,并不一定需要有一个类,通过一个 createUser 的方法足矣很好的实现工厂模式的功能。

Example 3: 基类

让我们先看几个生活中真实的例子。首先是 i18n Ruby gem(它的类与方法名称都是非常简练)。

class Base
    def config
    def translate
    def locale_available?(locale)
    def transliterate
end

这里,Base 这个命名本身并没有传达太多含义,其中内部结构包含了配置、翻译,区域设置,音译。它们可以看似无关的聚合在一起。

Example 4: 命名与构建

一个合理的命名可以引导我们构建出更为严瑾的组件容器。如下例所示。

class PostAlerter
    def notify_post_users
    def notify_group_summary
    def notify_non_pm_users
    def create_notification
    def unread_posts
    def unread_count
    def group_stats
end

PostAlerter 从这个名字本身可以发现,它意味着在内部会做一些类似提醒通知的功能。然而,其中 unread_postsunread_countgroup_status 并不在这个功能的主要范畴内,从这一点来看,这个类的名称并不是很理想。我们可以将这个三个方法移动到一个名为 PostStatistics 的类中,这样解耦后,事件功能会变得更加清晰,更可预测。

class PostAlerter
    def notify_post_users
    def notify_group_summary
    def notify_non_pm_users
    def create_notification
end

class PostsStatistics
     def unread_posts
     def unread_count
     def group_stats
end

Example 5: 奇怪的命名

在 Spring 框架中有一些例子,组件做的事情太多,其名称都非常冗长奇怪。这里只举一个例子(因为实在太多了):

class SimpleBeanFactoryAwareAspectInstanceFactory {
    public ClassLoader getAspectClassLoader()
    public Object getAspectInstance()
    public int getOrder() 
    public void setAspectBeanName(String aspectBeanName) 
    public void setBeanFactory(BeanFactory beanFactory)
} 

Example 6: 说说好的名称

我们聊了许多不太合理的命名,在 D3arc 中就有许多不错的命名定义,例如:

export default function() {
      /* ... */
      arc.centroid     = function() { /* ... */ }
      arc.innerRadius  = function() { /* ... */ }
      arc.outerRadius  = function() { /* ... */ }
      arc.cornerRadius = function() { /* ... */ }
      arc.padRadius    = function() { /* ... */ }
      arc.startAngle   = function() { /* ... */ }
      arc.endAngle     = function() { /* ... */ }
      arc.padAngle     = function() { /* ... */ }

      return arc;
}

上面这个例子中,每一个方法都是完全有意义的:他们都是以 arc 开头。并且他命名风格就像绘制下面的图片一样简练,令人欢喜。

[图片上传失败...(image-5fc70b-1567579306511)]

方法 1: 拆解

[图片上传失败...(image-6cfe47-1567579306511)]

应用场景:当你不能为类或方法找到一个合适的命名,但是你知道如何拆解它们,并且期望给他们的组合找到一个好的名称。

主要有两个步骤:

  1. 分辨出他们之间的特点和概念

  2. 将它们拆分开

在床和马桶这种特定耦合的场景下,为了拆解他们的不同之处,我们将床移动到左侧,将马桶移动到右侧。这样我们便将两个不同的事物分离开了。

当你不能为某个事物找到一个好的名称时,也许是因为你所面临的不止一件事物。不过现在我们已经知道,对多个事物进行命名是一件非常困难的事情,当我们遇到这类问题时,不妨确认一下构造这个事物的组成部分,以及动作行为。

事例:

我们现有一个未命名的类,其中包含了 requestreponseheadersURLsbodycachingtimeout,把所有这些从类中拉取出来,我们剩下这样一些组件:RequestResponeHeadersURLsReponseBodyCacheTimeout 等。如果我们已知这些类的名称,那么我们可以确定这个类是用于处理 Web 请求的,HTTPClient 是一个不错的 Web 请求组件的命名。

当我们编码遇到困难时,先不要想着整体,先考虑一下局部细节。

方法 2: 发现新概念

[图片上传失败...(image-e8249a-1567579306511)]

应用场景:当一个类并不简单或者内容并不相干。

发现新的概念需要大量业务领域的知识,当软件的命名和业务保持一致时,一个普遍的语言便建立起来,它允许来自不同专业领域的人来使用相同的语言。

方法 3: 分组标准

应用场景:当有一个好的命名,但是他们他们之间并不适合。

组件元素之前可以通过各种标准进行分组,譬如组件元素的物理性质,经济性,情感性,社会性以及软件中最常用的功能。

在软件工程中,我们倾向于按功能对组件元素进行分组。如果列出你的项目文件,你可能会看到像 controller/models/adapters/templates/ 等等目录名称,然后,有些时候,这些名称组合在一起并一定适合,这也是重新评估模块,重新定义,规划命名的时候。

每个应用程序都有自己不同的上下文环境,每个模块、每个类、每个方法也同样都有。User 这个词所代表的含义可以是操作系统用户,或是一张数据表,也可以是一个第三方的服务凭证,不同的上下文环境,它所表示的含义不尽相同。

[图片上传失败...(image-5711db-1567579306511)]

无意义的词与新词

多年以来,命名规范的演变上变得更具有意义,有更多的人来填补这个陈旧的空缺。

Helperhelpers 是一个支持应用程序实现的主要方式。应用程序实现与定义的标准是什么呢?应用程序中的所有内容都应该支持并实现其主要目标。

在实践中,它们被击中在一个非自然的分组中,为一写其他常用的操作提供可重用性。一般情况下,helpers 需要另一个组件元素的内部数据的依赖。这种命名一般会在找不到合适的名称时折中使用。

Base,许久之前,在 C# 中有需要继承类的命名方式都是以 Base 命名。例如:汽车和自行车的父类都是 Base 而不是 Vehicle。尽管微软提出建议去避免这类命名方式,但他依然影响了 Ruby 这门语言,其中最具代表性的是 ActiveRecord 类的继承。到目前为止,我们依然将 Base 看做为开发人员找不到合适命名的一种替代方式。

变动调整后的 Base 含括了 CommonUtils,例如,JSON Ruby gem 的 Common 类具有 parsegenerateload以及 jj 等方法,但这里 Common 真的具备它的含义吗?

Tasks,在 JavaScript 社区兴起了一种通过异步调用函数的方式,这种方式起源于 task.js,即使目前这个开源库很少再被提使用,但是这个术语流传了下来。

如果团队中所有人都能清楚的理解它的含义,那是可喜的。但如果有新人加入团队,并且他遇到了被抛弃在垃圾堆中的 60 年代便存在的古怪命名,那又怎么办呢?

在我之前的项目工作中,曾遇到过这样的一个类的命名,你们猜猜看,Atlanta,是的,亚特兰大,操蛋的亚特兰大。没人知道或者可以告诉我为什么要起这么个名字,以及含义是什么。

参考资料

Cwalina,Krzysztof.2009,框架设计指南:可重用 .NET 库的约定、惯用语和模式,第二版。 Boston: Pearson Education, Inc. 206。

Evans, Eric. 2003。域驱动设计:解决软件核心复杂性。Boston: Addison-Wesley Professional。

原文链接https://medium.com/hacker-dai...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容