You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
trying to apply it in every case that seems appropriate。 手里有锤子看什么都是钉子。 如果是警官拦下你的车,找你要wallet里面的驾驶证,那你不把驾驶证整个信息给警察肯定是不对的。这个时候就是需要你直接返回驾驶证信息。所以也不是任何时候信息都不跨级传递。但是这里最好传递的是一个DTO,entity。 让警察自己去封装entity。 这样两边还是能独立演进。
最小知识原则是一个底层原则,很多设计模式体现的就是这
At its simplest, this is simply 'delegation'.
Consider the 'wrapper' patterns, Adapter, Proxy, and Decorator. Each of them hold onto an object, 'wrapping' it for some purpose.
This is the Law of Demeter in the sense that it is 'hiding' the wrapped object, making the user use it through a different interface. The 'Facade' pattern is another great example of the Law of Deme.
代码整洁之道
3. 函数
3.1 短小
以前我很讨厌有人写只有一行逻辑的函数, 直到我遇到了这些问题
含义不清楚,
strings.Contains(xxx)
这只是一个标准库的用法,跟你想要描述业务有什么关系? 代码不能做到自解释扩展性,
userType=Guest
, 当时只是一句就能写清楚的逻辑,因为没有抽出来函数,导致我们对Guest
这个身份扩展了, 所有地方都要修改一下3.2 只做一件事
只做一件事会把函数且的很细,这是不好的,但是会在扩展这个函数代表的意义的时候会变得很容易。
3.3 每个函数一个抽象层次
一个函数写的好不好就要看函数中的所有语句是不是在一个抽象层级上。
一个函数要么是做了一些事情,要么把下一层的粘连在一起。就像是砖和混凝土的关系。
自顶向下读代码, 每个函数天生就是包含了其他函数, 然后组合其他函数. 组合同一级别的函数才是合理的做法. 这样写出来的代码逻辑非常清晰, 你可以看广度优先遍历的方式去读代码.
3.4 对于swtich的态度
如果是小范围的,在底层的函数,还可以接受
如果是顶层的,经常变更的。需要换用工厂模式去实现。
3.5 使用描述性的名称
3.6 函数参数
如果参数和函数名处于不同的层级,那意味着你要去了解目前并不特别重要的细节(不符合广度优先便利读代码)
从测试角度来说,参数越多越难构造。( 这里是从整体拆分功能角度说的,如果你无脑把多个参数合成一个参数的struct,那你只是在骗自己, 并不能使得测试变简单)
二元函数,可以思考把第一个参数做成类
参数越少越好,不要传3个以上的参数, 如果超过了, 意味着这里有一个模块可以拆出来. 多个参数,肯定可以抽出来概念,组合在一起
3.7 无副作用
如果有副作用写到函数名中
输出参数别放到参数中。 因为很容易混淆对参数做事,和把事情做到参数中。
3.9 依赖磁铁
错误码通常实现为枚举,定义所有的错误码。
这样几乎所有的类都要导入和使用它。当Error枚举修改的时候,所有地方都要从新编译和部署。
我们应该使用异常类的方式,这样就符合开闭原则,无须重新编译和部署。
(因为java类编译有缓存, 对go没啥意义)
3.10 不要重复你自己
重复可能是软件中一切邪恶的根源
3.12 我并不从一开始就按照规则写函数,我想没人能做到。
我们需要不断地修改, 提升自己, 提升代码质量
4. 注释
代码本身就是注释。
注释不能让垃圾变美好。
不要把git能解决的事情写到注释中, 比如change log
4.3 好注释
4.4 坏注释
只对作者有意义的注释
多余
误导性注释
循规蹈矩生成的注释
位置标记
括号、锁注释
6 对象和数据结构
6.1 数据抽象
私有变量的含义并不是不让别人访问那么简单,而是把抽象和具体数据隔离开。隐藏实现,只暴露抽象接口。所有的get, set方法都要是业务上的set, get,而不是简单的赋值器。
6.2 过程和面向对象的对立
过程式代码
面向对象多态类:
上面两种风格的代码:
如果要增加一种新的计算方式, 对于过程式, 完全不用改数据结构, 只需要新增计算过程. 但是对于下面的, 就需要所有地方都新增一个方法
如果要增加一种新的类型, 对于过程式, 需要改所有的计算过程, 但是对于面向对象, 就只影响新增的对象.
由此, 我引申出来两个扩展的方向:
横向扩展: 增加一个新的种类, 比如新增一个形状, 新增一个用户类型, 新增一种验证方式(加数据结构)
纵向扩展: 对现在已有的流程,进行加固,比如登录流程增加一个验证码校验的过程. (加函数)
对于业务来说, 我们应该以支持横向扩展为目标, 尽可能简单的用纵向扩展的方式去写代码. 纯写成容易横向扩展的代码, 平时加一个简单功能都要改半天, 同时会不得不面对, 接口设置不合理, 需要时时重构, 写个小需求工作量巨大. 但是如果不支持横向扩展, 等需要改的时候, 就只能累死了.
另外, 对于学艺不精的人来说, 强行去写面向对象的代码(支持横向扩容) 容易做出来一个类需要1万个方法......等你真的横向扩展的时候, 发现自己要实现1万个方法, 而且还冗杂一些dispatch的过程式代码. 也是改不动的
另外大多数没有训练过写面向对象代码的人, 很容易写出来第一种过程式代码, 因为人的思维就是过程使得, 面向对象反而是一种抽象, 升华
我们常常会面对这样的场景:
总结一下
6.3.1 在谈对象和数据结构
得墨忒耳定律。 一个模块不应该了解其所操作对象的内部情况。
也就是说一个模块只能调用高级方法,不能访问内部数据结构,不允许存在get,set。只能有业务上的save,check....
要注意的是,这个定律中模块的概念应该是偏上层的。 也就是搞等级的类。 对于低级的数据结构来说,肯定要去操作内部成员的嘛。 所以这里还是两个截然不同的概念:
数据结构类: 也不要使用get, set, 直接使用a.b.c
模块类: 禁止访问成员变量字段, 只能使用方法, 即隐藏数据,暴露操作。
这里有一篇文章很好的解释了这个定律到底在讲什么:
https://www2.ccs.neu.edu/research/demeter/demeter-method/LawOfDemeter/paper-boy/demeter.pdf
重点在于OOP的抽象合理,人不能提供一个钱包对象给收银员,让收银员从里面拿钱。 但是人可以把钱包里的驾驶证直接给police。这都是对现实世界的建模,越贴近现实世界,需求才会越合理。
不使用这个定律的缺点:
这个定律的优势:
其实就是这样写更反映真实场景,意味着paperboy和customer.Pay和wallet都可以任意演化了。松耦合的最佳例子。同时反映真实场景,也意味着逻辑收敛到了一个地方。
这个定律的缺点:
最小知识原则是一个底层原则,很多设计模式体现的就是这
At its simplest, this is simply 'delegation'.
Consider the 'wrapper' patterns, Adapter, Proxy, and Decorator. Each of them hold onto an object, 'wrapping' it for some purpose.
This is the Law of Demeter in the sense that it is 'hiding' the wrapped object, making the user use it through a different interface. The 'Facade' pattern is another great example of the Law of Deme.
6.3.2 对象和数据结构混杂
7 错误处理
错误处理很重要,但是如果他搞乱了代码逻辑,那就是错误的做法。
作者始终对错误处理的态度是。将错误处理隔离看待,将其独立于主要逻辑之外。
7.1 是用异常而不是返回错误码
作者站在java的立场上,认为try catch把正常逻辑和错误逻辑分开了,这样逻辑紧凑,不会被分割。
实际上每一个最小函数单元,内部总有各种错误。可以通过各种手段兜底然后继续进行。通常这时候错误处理就是和业务一起写的,这里我跟赞同early return的做法。
不过错误码确实是很糟糕的做法。你永远不应该使用错误码。因为错误码包含的信息太少太少。需要定义的错误码太多太多,你需要的是一个struct, 一个interface, 一个类,反正不是一个数字。
7.2 先写try catch finally 语句
作者认为这样语义清晰,这种结果定义了一个范围,在各自的范围里面,你肯定知道不同的东西是在干什么。
我认为这件事并不重要,因为这个并是像if else一样通用,没必要做成语言特性的。只有在少数地方,比如写测试代码需要init, cleanup才有这种语义。
不过结构清晰我是赞同的。需要给不同的模式编写不同的scope函数。
7.3 使用不可控异常
这章是在讨论在return里面强制声明异常种类是不是好的行为。 结论是不好。原因是
这章我非常赞同,特别的,这对于入参和出参都是一样的道理。如果入参/出参有很多种,每次家业务都需要改函数签名。那么就是垃圾代码。不仅是中间所有的函数签名都需要修改。还意味着你没有抽象出来调用结构体。
当然,如果你写的是非常核心的代码。强行的定义各种参数是很合理的做法。
7.3 给出异常发生的环境说明
你抛出的每一个异常,都应该提供足够的环境说明,以判断错误的来源的所处。
stack trace并不能告诉你操作失败的初衷。
在go中,你应该用 fmt.Errrof("....%w....") 把环境信息裹进去。
7.5 定义异常类型
可以做聚合判断,不多讲。在go里就是 errors.As
7.6 定义常规流程
当需要检查错误然后走另外一个分支的时候, 想想能不能通过抽象。把不同的分支放到不同的实现中。
这种手法叫做特例模式。 创建一个类或者对象。用来处理特例。这样上层代码就不用应付异常行为了。
这个思路可以在很多地方应用, 很好的想法
7.7 别返回null值
返回null,比如抛出异常,或者返回特例对象
我对这件事的看法是,你提供一个函数要尽可能返回所有信息,拿不到东西是什么原因呢? 上层肯定要关心的为什么拿不到的。
否则wrap一下做成
mustGet
也可以接受。7.8 不要传递null值
因为go中存在指针,这个问题要比java更严重。
除此以外不应该大量使用指针(nil)。 而对于第一点,大部分人没有资格去争论这个。
8 边界
边界是指对第三方包的依赖,这个第三方包可以是utils,mysql,abase,rpc,github project...
这些需要整合到你的代码中,整合需要手段
8.1 使用第三方代码
作者举了一个java.utils.map的例子,最终给的建议是使用一个类封装map[user]的操作。
实际上做的更彻底的方式是这样的。
这两层写下来或有一些繁琐,但是可以极大地提高灵活性。
我在业务代码里遇到过两种类型的改动:
8.2 对第三方代码进行学习性测试
这种毫无成本,但是把测试代码固化到了项目中,之后第三方依赖改动不兼容很容易发现。
并且使得升级过程更大胆,因为测试都跑过了!
8.5 使用尚不存在的代码
有时候你可以先自己定义自己的接口,然后等真正的代码ready之后,适配你之前的定义。这个在mock的时候很好用。而且使得你不被block。多一层这个抽象并不是什么大问题。
8.6 整洁的边界定义
边界上改动是很正常的。作者说有着良好软件设计的,就无需巨大投入和重写即可进行修改。当你依赖控制不了的代码时,比如加倍小心投资,确保未来修改的代价不至于太大。
边界上的代码需要清晰地分割和定义期望的测试。避免我们了解过多第三方代码中的特定信息。
包装接口,或者使用adapter模式都可以是的后面的改动小一点。
9 单元测试
什么是专业的测试? 请看作者写的测试。 注意测试和代码的结构是密不可分的,垃圾代码是没办法测试的。
9.2 保持测试整洁
测试不是二等公民,他和代码一样重要。如果测试写多了改不过来,就会成为负担。
测试带来的一切好处:
测试覆盖越多,越能毫无顾虑的改进架构和设计。
测试最重的要的是什么?
可读性 可读性 可读性。 这个要求是对测试最重要的属性。
手段是什么: BUILD OPERATE CHECK
9.3.1 面向特定领域的测试语言
这种测试api并非起初就设计出来,而是对那些充满细节的令人迷惑的测试代码进行重构时逐渐演进。
9.3.2 双重标准
测试代码和正式代码的标准是不一样的。 整洁是第一位,效率是可以放在后面的。
9.4 每个测试一个断言
有个流派认为每个测试中只应该有一个assert。 但是这写代码重复太多了。
这样说是有道理的,但是有多个assert也不是接受。
这个是一个小问题。
更好的原则或许是,每个测试一个概念
9.5 FIRST 原则
快速, 独立,可重复(在各种环境都可以测试),自足验证(不需要人工干预就能得出结果), 及时编写
Fast, Independent,repeatable, self-validating, timely
The text was updated successfully, but these errors were encountered: