mybankaccount主旨(程序命名的原则与重构)



作者:余敦刚(苏宣)

命名是对事物本质的认知探索,是对读者有价值的承诺。不好的命名会像雾一样,把读者引入深渊;好的名字会像灯塔一样,照亮读者前进的道路。名字太奇妙了,本文将一步步揭开它的神秘面纱!

命名来源生活



从左到右:正三角形、正方形和正六边形代表边长等。,因此正XXX的边长必须相等。

这些东西的特点很明显,容易被人记住。但也有一些难以命名,如化学有机物:



与化学有机物相比,软件世界里的东西更多,命名会更困难。

菲尔顿(Phil Karlton)曾经说过:计算机科学只有两个难题:缓存失效和命名约定。



命名一直是软件领域的一个难题。好的命名可以是忠实的、富有表现力的和优雅的:

三难:信达雅。

——严复


信:准确的意思
达:通顺流畅
雅:简洁优雅

命名的坏味道

看下面的代码,说出它的意思:

公共列表& ltint[]>getThen(){ List & lt;int[]>list1 =新数组列表& ltint[]>(); for(int[]x:the list) if(x[0]= = 4) list 1 . add(x); 返回列表1; }问题不在于代码的简明性,而在于代码的模糊性:即代码中没有清晰反映上下文的程度。

  • 什么样的东西在列表中?
  • 列表中零下的条目有什么意义?
  • 4是什么意思?
  • 如何使用返回的列表?
  • 问题的答案并没有反映在代码片段中,但是它们应该在那里。

    再次查看重命名后的代码:

    公共列表& ltint[]>getFlaggedCells(){ List & lt;int[]>flaggedCells = new ArrayList & ltint[]>(); for(int[]cell:game board) if(cell[STATUS _ VALUE]= = FLAGGED) FLAGGED cells . add(cell); 返回flaggedCells }注意代码的简单性没有被触动。运算符和常数的数量保持完全不变,嵌套的数量保持完全不变。但是代码已经变得清晰多了。

    站在用户的角度,能不能更进一步?

    尽管名称getFlaggedCells表明该方法将返回FlaggedCells,但返回的数据结构并未表达:

    公共列表& ltCell & gtgetFlaggedCells(){ List & lt;Cell & gtflaggedCells = new ArrayList & ltCell & gt(); for(Cell Cell:game board) if(Cell . isflag()) flagged cells . add(Cell); 返回flaggedCells }你能看出代码中的两个区别吗?

    如果你仔细看,你会发现有两点得到了改进:

  • int[]-& gt;单元:建模数据并赋予其意义。
  • 单元格[状态值]= = flaged = = & gt;cell.isFlagged()
  • 建模后,该语句可以语义化,可读性增强。
  • STATUS_VALUE和FLAGGED隐藏在单元格中,这使它们更有凝聚力。
  • 只需更改以下名称,您就可以很容易地知道发生了什么。这就是命名的力量。

    命名的游戏

    首先,我们来玩一个游戏,叫做“我们住在哪个房间?”下面给大家提供一张图。请告诉我这是什么房间。



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



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

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

    如果有床。然后是卧室。我们也可以反过来分析。

    问题:根据一个容器名,我们可以推断出它的组件。如果以卧室为例,这个房间很有可能有一张床。这样,我们可以建立第二个推论:

    推断二:根据名称推断容器(函数)的内部组件

    现在我们有两个推论,根据这两个推论我们试着看看下图。

    问题3/3



    好吧,床和马桶在同一个房间?根据我们的推断,上图让我们很难立刻做出判断。如果还是用上面两个推论来定义的话,那我就叫它怪物的房间。

    问题不在于同一个房间的物品数量,而在于完全不相关的物品被视为具有相同的标签属性。在家里,我们通常把意图和功能相似的相关事物放在一起,以避免混淆,所以现在我们有了第三个推论:

    推论三:容器(函数)的清晰程度与其内部组件的紧密程度成正比

    这可能很难理解,所以我们用下图来说明:



    如果容器内部元素的属性紧密相关,那么更容易找到一个名称来解释它。相反,元素之间的不相关性越强,就越难以描述。

    属性维度可能与其功能、目的、策略、类型等有关。至于命名标准,需要和元素本身的属性相关,才有实际意义。

    这个观点也适用于软件工程。比如我们熟悉的组件、类、函数方法、服务、应用。罗伯特·德朗奈曾说:“我们的理解能力很大程度上与我们的认知有关。”那么,在这种技术背景下,我们的代码能否让读者以最简单的方式感知业务需求和相关诉求呢?

    命名的原则

    名副其实

    名字应该描述它所做的一切(或者它的意图)。

    读者在阅读上面这个名为恶趣味的例子中的getThen()时,无法理解getThen()的用意是什么。要得到什么?GetFlaggedCells()可以更准确地表达。

    mybank

    //年糕的命名 Public list < int[]>getThen(); //好名字 公共分发名单< Cell & gtgetFlaggedCells(); //年糕的命名 private Date user cache time; //好名字 私人约会customerstaytotaltime避免误导

    避免留下虚假线索来隐藏代码的本意。

  • 不要用accountList来指代一组账户,除非真的是列表类型。
  • 单词列表对程序员来说有着特殊的意义。如果装账的容器不是真实的单子,就会导致判断错误。最好使用accountGroup或bunchOfaccounts,甚至accounts。

  • 避免在变量名中使用小写字母L和大写字母O。
  • 这种拼写方式很容易误导读者或者让他们花更多的力气去识别。

    有意义的区别

    如果同一个作用域内有多个名字,最好区分开来。

    public static void copy chars(char a1[],char a2[]){ for(int I = 0;我& lta1 .长度;i++){ a2[I]= a1[I]; } }这里参数a1和a2是根据含义命名的,并没有正确的信息或提示作者的意图。

    如何进行有意义的区分?

    如果将参数更改为source和destination,则此函数的命名更适合其用途。

    public static void copy chars(char source[],char destination[]){ for(int I = 0;我& ltsource .长度;i++){ 目的地[i] =源[I]; } }准确使用对仗可以提高命名的区分度。

    命名时,遵循对立词的命名规则有助于保持一致性,这也可以提高可读性。像first/last这样的矛盾很好理解;像FileOpen()和_lclose()这样的组合是不对称的,容易混淆。以下是一些常见的对立短语:

    添加/删除

    递增/递减

    打开/关闭

    开始/结束

    插入/删除

    显示/隐藏

    创造/毁灭

    锁定/解锁

    源/目标

    名/姓

    最小/最大

    开始/停止

    获取/放置

    下一个/上一个

    向上/向下

    获取/设置

    旧/新


    尽量不要使用info/data作为类名或变量名的结尾。

    info和data的含义过于宽泛,导致没有多余的信息,反而增加了读者的阅读成本,得不偿失。

    一贯的风格

    保持同一项目中代码命名规则的统一。

    例如:

  • 每个类的LOGGER命名为logger、log或Logger,可以选择哪个名字,但要保持项目统一;
  • 该类的getter/setter方法的命名是统一的。可以使用GetName()或Name(),但项目需要统一;
  • 注释的风格是统一的。
  • /** *用户名 */ Public Sring用户名; /**用户名**/是抽象且一致的

    使同一作用域内的变量或方法具有相同的抽象。

    公共类雇员{ ... 公共字符串getName(){...} public String get address(){...} public String getwork phone(){...} public boolean isjobclassificationvalid(jobclassificationjobclass){...} public boolean iszip code valid(Address Address){...} public boolean isPhoneNumberValid(phone number phone number){...} public SQL query getQueryToCreateNewEmployee(){...} 公共SQL查询GetQueryTomodifyemployee () {...} 公共SQL查询GetQueryTomodifyemployee () {..} ... }看看这个类,看看它有多少级抽象。

    您可以通过功能名称查看:

  • GetName、getAddress、getWorkPhone是获取Employee的主要属性,符合Employee的抽象。
  • IsJobClassificaitionValid用于检查JobClassification对象是否有效,属于JobClassification的抽象层次,与Employee无关;
  • IsZipCodeValid是检查地址的合理性,属于地址的抽象层次,而地址是员工的属性,不是抽象层次。IsPhoneNumberValid相同;
  • GetquerytocreatenewEmployee/getquerytodifyEmployee/getquerytoretrieveEmployee看似与Employee有关,但这里SQL语句查询的细节是实现细节,级别低于employee。
  • 不同层次的方法会让这个类看起来很奇怪,就像把晶体管、芯片零件、手机放在一个桌子上。

    公共类雇员{ ... 公共字符串getName(){...} public String get address(){...} public String getwork phone(){...} [/ H/]公共字符串创建雇员(...) {..} 公共字符串更新雇员(...) {..} 公共字符串删除雇员(...) {...}

    如果你在一个项目中找到一段集合搜索标准的代码,它会在许多地方重复。这个搜索条件比较复杂,以元数据的形式存在于数据库中,所以组装过程如下:

  • 首先,我们需要从缓存中获取搜索条件列表;
  • 然后,遍历这些条件并填入搜索到的值。
  • //获取默认搜索条件 列表< String & gtdefault conditions = searchconditioncachetunnel . getjsonquerybylabelkey(label key); for(String JSON query:default conditions){ JSON query = JSON query . replace all(search constants。SEARCH _ DEFAULT _ public sea _ ENABLE _ TIME, string . value of(system . current TIME millis()/1000)); jsonquerylist . add(JSON query); } /取主搜索框的搜索条件 if (string utils。不为空(cmd。getcontent ())) { list

    简单分析,不难发现,这里我们遗漏了两个概念:一个是用来表示搜索条件的类——搜索条件;另一个是用来组装搜索条件的类——SearchConditionAssembler。只有对这两个概念进行命名和明确表达,才是完整的重建。

    重构后,搜索条件的组装会变成非常简洁的形式,几十次复用只需要引用SearchConditionAssembler。

    公共类SearchConditionAssembler { 公共静态SearchCondition assemble(String label key){ String jsonSearchCondition = getJsonSearchConditionFromCache(label key); search condition sc = assembleSearchCondition(jsonSearchCondition); 返回sc; } }可见提取重复代码只是我们重构工作的第一步。抽象出重复码的概念,找到有意义的名称,是我们工作的重点。

    所以,每次遇到重复的代码,都要兴奋起来,想,这是锻炼自己抽象能力的绝佳机会,除了测试代码。



    上下文泛化

  • 不要动摇你的智慧
  • 如果你用的名字来自冷门的语境,比如俚语或者俚语,不知道这个语境的人会很难理解它的意思。

    比如用whack识别kill,用wsBank识别网商银行。

  • 使用问题域的名称。
  • 如果不能用程序员熟悉的术语来命名,就用涉及到的问题领域的名称,至少维护代码的程序员可以咨询领域专家。至少这样一来,问题领域的专家可以清楚地理解开发者命名的上下文,读者可以向领域专家请教,或者查询领域词汇的含义。

    以消息中间件领域为例:主题、消息、标签、偏移量、提交日志。

    名词

    意义

    主题

    消息类型

    经纪人

    消息处理代理

    消息

    邮件主题

    标签

    主题下的次要消息类型

    抵消

    消费者的消费进程

    commitLog

    消息的存储文件


    改名

    如果子例程名、类名和变量不明确或命名错误,则需要重命名或重新构造该变量。

    更改函数声明



    好的名字可以让读者一眼看出函数的用途,而不必查询实现代码。

    1动机

    功能名称:

    改进函数声明的技巧:首先写一个注释,描述这个函数的用途,然后把这个注释变成函数的名字。

    函数的参数:

    函数的参数表说明了函数是如何与外界共存的,这是函数和函数用户共同的依赖,实际上是一种耦合。

  • 最小使用原则:函数的参数列表正是函数所依赖的,不会依赖于未使用的信息:
  • 比如用一个函数把某人的电话号码转换成特定的格式,而这个函数的参数是一个人,所以我不能用这个函数处理公司的电话号码。如果函数接收的参数从“个人”更改为“电话号码”,则用于处理电话格式的代码可以使用得更广泛。
  • 根据函数的意图引入函数参数:
  • 如果这个函数的目的只是将电话号码转换成特定的格式,那么只介绍电话号码是合适的;
  • 如果这个函数的目的是获取“人”的电话号码格式,而他的号码是由他自己的其他属性合成的,那么就要引入“人”。
  • 如何选择正确的参数没有简单的规则,要看具体情况。

    练习2

    常用的重构方法有两种:简单方法和迁移方法。

    简单方法:适用于一步修改函数声明及其所有调用方。

  • 如果你想删除一个参数,你需要确保这个参数没有在函数体中使用。
  • 修改函数声明,使其成为所需状态;
  • 找出所有使用旧函数声明的地方,改成新函数声明;
  • 测试。
  • 最好把大的修改拆分成小的步骤,所以如果要修改函数名和添加参数,最好分两部分来做。幸运的是,简单的方法通常可以用IDE工具直接重构。

    实战:以下功能名称太短。

    public long circumn(long radius){ return 2 * Math。PI *半径; }将此名称更改为更有意义的名称:

    公长周长(长半径){ 返回2 *数学。PI *半径; }迁移途径:函数在很多地方被调用,修改不容易,或者是要修改的多态函数,或者函数声明修改复杂。

  • 如有必要,先对函数体内部进行重构,以便于后续的细化步骤;
  • 使用精炼函数(106)将函数体精炼成新函数;
  • 提示如果您打算使用旧函数的名称,可以给新函数一个临时名称,以便于搜索。
  • 如果精化函数需要添加新的参数,只需用之前的简单方法添加即可;
  • 测试;
  • 对旧函数使用内嵌函数(115);
  • 如果新函数使用临时名称,则再次使用更改函数声明(124)将其更改回原始名称;
  • 测试。
  • 实战:还是刚才那个把circum方法改名的例子。

    暂时不修改这个简短的函数名。

    public long circumn(long radius){ return 2 * Math。PI *半径; }再次添加周长函数:

    公长周长(长半径){ 返回2 *数学。PI *半径; }逐步将circum方法的调用地点改为circumference方法,每次修改都要运行测试;如果测试成功,提交此修改进行修改;否则,返回上一步,再次进行修改。这样如果中间出了问题,也能准确定位某个修改,稳定推进重建,间接提高重建效率。

    变量已重命名




    1动机

    一个好的变量名可以解释一个程序做什么——如果变量名起得好的话。

    2机制

  • 如果变量应用广泛,可以考虑使用封装变量进行封装;
  • 找出所有使用这个变量的代码,逐一修改;
  • A.如果该变量在另一个代码库中使用,则为“已发布变量”,此时不能进行此重构;

    B.如果变量值从不修改,可以复制到一个新的名称,然后逐个修改使用代码,每次修改后执行测试;

    3.测试。

    3示例

    如果要重命名的变量只作用于一个函数,重命名是最简单的,直接用IDE重命名即可。

    如果变量不作用于单个函数,那么重命名的风险就不容易控制,所以需要对变量进行封装。

    变量初始化

    int treeName = & # 34无标题& # 34;;变量被修改。

    treeName = & # 34大树& # 34;;读取更改

    leftTree = treeName这时候可以考虑用封装变量来完成。

    private int treeName public void init(){ treeName = & # 34;无标题& # 34;; } public String get treeName(){ return treeName; } public void setTreeName(String treeName){ this . treeName = treeName; }命名过程命名是一个迭代的过程。当你久久想不出更好的名字时,不要陷入命名的陷阱。您可以先用一个折衷的名称提交或重构这个程序。当你想到一个更合适的名字时,不要犹豫,重建它。

    命名是一个接近描述事物本质的过程。名字越好,越容易描述事物的本质。

    选择一个好名字最困难的部分在于需要好的描述技巧和共同的文化背景。

    结语

    好的命名是不言自明的,读者可以在不知道程序实现细节的情况下知道程序实现(契约式编程)的意图。在实际项目中,有时很难为一个子程序取一个更好的名字。这实际上是程序在说话——让我做的事情太复杂了,我都不知道自己在做什么。一般来说,需要重新构建这个子程序,在对齐和分而治之中划分职责。命名是一门艺术,美在于简单,美在于清晰,美在于名副其实。

    参考文件

  • 两个硬汉:https://martinfowler.com/bliki/TwoHardThings.html
  • 软件复杂性:命名的艺术:
    https://medium . com/hacker noon/software-Complexity-Naming-6e 02 e 7 E6 c8 CB
  • 代码百科全书
  • 代码清洁之道
  • 关注【阿里巴巴移动科技】微信微信官方账号,每周写3篇移动科技实务&干货供你思考!

    您可以还会对下面的文章感兴趣

    使用微信扫描二维码后

    点击右上角发送给好友