|陈子煜 得物技术
团队归属于后方业务支撑部门,组内的项目都以pc中后台应用为主。对比移动端应用,代码库比较庞大,业务逻辑也相对复杂。在持续的迭代过程中,我们发现当前的代码仓库仍然有不少可以优化的点:
21年前端平台决定技术栈统一迁移到React生态,后续平台的基础建设也都围绕React展开,这就使得商家使用Vue生态做开发的系统面临技术栈迁移的难题,将业务逻辑和UI框架节藕变得异常重要。
随着代码量和团队成员的增加,应用里风格迥异的代码也越来越多。为了能够持续迅速的进行迭代,团队急需一套统一的顶层代码架构设计方案。
随着业务变得越来越复杂,在迅速的迭代过程中团队需要频繁地对功能进行回归,因此我们对于自动化单测用例的诉求也变的越来越强烈。
为了完成以上的优化,四组对现有的应用架构做了一次重构,而重构的核心就是整洁架构。
整洁架构(The clean architecture)是由 Robert C. Martin (Uncle Bob)在2012年提出的一套代码组织的理念,其核心主要是依据各部分代码作用的不同将其拆分成不同的层次,在各层次间制定了明确的依赖原则,以达到以下目的:
为了实现以上目的,整洁架构把应用划分成了entities、use cases、interface adapters(MVC、MVP等)、Web/DB等至少四层。这套架构除了分层之外,在层与层之间还有一个非常明确的依赖关系,外层的逻辑依赖内层的逻辑。
entities封装了企业级的业务逻辑和规则。entities没有什么固定的形式,无论是一个对象也好,是一堆函数的集合也好,唯一的标准就是能够被企业的各个应用所复用。
entities封装了企业里最通用的一部分逻辑,而应用各自的业务逻辑就都封装在use case里面。日常开发中最常见的对于某个模型的crud操作就属于usecase这一层。
这一层类似于胶水层,需要负责内圈的entity和use case同外圈的external interfaces之间的数据转化。需要把外层服务的数据转化成内层entity和usecase可以消费的数据,反之亦然。如上面图上画的,这一层有时候可能很简单(一个转化函数), 有时候可能复杂到包含一整个MVC/MVP的架构。
我们需要依赖的外部服务,第三方框架,以及需要糊的页面UI都归属在这一层。这一层完全不感知内圈的任何逻辑,所以无论这一层怎么变(ui变化),都不应该影响到内圈的应用层逻辑(usecase)和企业级逻辑(entity)。
在整洁架构的原始设计中,并不是强制一定只能写这么四层,根据业务的需要还可以拆分的更细。不过无论怎么拆,都需要遵守前面提到的从外至内的依赖原则。即entity作为企业级的通用逻辑,不能依赖任何模块。而外层的ui等则可以使用usecase、entity。
前面介绍了当前代码库目前的一些具体问题,而整洁架构的理念正好可以帮助我们优化代码可维护性。
作为前端,我们的业务逻辑不应该依赖视图层(ui框架及其生态),同时应当保证业务逻辑的独立性和可复用性(usecase & entity)。最后,作为数据驱动的端应用,要保证应用视图渲染和业务逻辑等不受数据变动的影响(adapter & entity)。
根据以上的思考,我们对“整洁架构”做了如下落地。
对于前端应用来说,在entity层我们只需要将服务端的生数据做一层简单的抽象,生成一个贫血对象给后续的渲染和交互逻辑使用。
interface IRawOrder {
amount: number
barCode: string
orderNo: string
orderType: string
skuId: number
deliveryTime: number
orderTime: number
productImg: string
status: number
}
export default function buildMakeOrder({
formatTimestamp,
formatImageUrl,
}: {
formatTimestamp: (timestamp: number, format?: string)=> string
formatImageUrl: (
image: string,
config?: { width: number; height: number },
)=> string
}) {
return function makeOrder(raw?: IRawOrder) {
if (!raw || !raw.orderNo) {
Monitor.warn('脏数据')
return null;
}
return {
amount: raw.amount,
barCode: raw.barCode,
orderNo: raw.orderNo,
orderType: raw.orderType,
skuId: raw.skuId,
status: raw.status,
statusDescription: selectStatusDescription(raw.status),
deliveryTime: formatTimestamp(raw.deliveryTime),
orderTime: formatTimestamp(raw.orderTime),
productImg: formatImageUrl(raw.productImg),
}
}
}
function selectStatusDescription(status: number): string {
switch (status) {
case 0:
return '待支付'
case 1:
return '待发货'
case 2:
return '待收货'
case 3:
return '已完成'
default:
return ''
}
}
以上是商家后台订单模型的entity工厂函数,工厂主要负责对服务端返回的生数据进行加工处理,让其满足渲染层和逻辑层的要求。除了抽象数据之外,可以看到在entity工厂还对数据进行了校验,将脏数据、不符合预期的数据全部处理掉或者进行兜底(具体操作要看业务场景)。
有一点需要注意的是,在设计entity的时候(尤其是基础entity)需要考虑复用性。举个例子,在上面orderEntity的基础上,我们通过简单的组合就可以生成一个虚拟商品订单entity:
import { makeOrder } from '@/entities'
export default function buildMakeVirtualOrder() {
return function makeVirtualOrder(raw?: IRawPresaleOrder) {
const order=makeOrder(raw)
if(! order || !raw.virtualOrderType) {
Monitor.warn('脏数据')
return null
}
return {
...order,
virtualOrderType: raw.virtualOrderType,
virtualOrderDesc: selectVirtualOrderDesc(raw.virtualOrderType)
}
}
}
如此一来,我们就通过entity层达到了2个目的:
usecase这一层即是围绕entity展开的一系列crud操作,以及为了页面渲染做的一些联动(通过ui store实现)。由于当前架构的原因(没有bff层),usecase还可能承担部分微服务串联的工作。
举个例子,商家后台订单页面在渲染前有一堆准备逻辑:
现在大致的实现是:
{
mounted() {
const { subType }=this.$route.query
/*
7-15行处理了几种分支链路场景下对subType的赋值问题
*/
if (Number(subType)===0 || subType) {
this.subType=subType.toString()
} else {
if (this.user.merchant.typeId===4) {
this.subType=this.tabType.cross
} else {
this.subType=this.tabType.ordinarySpot
}
}
/*
getAllLogisticsCarrier有没有对subType赋值呢?光看这段代码完全不确定
*/
this.getAllLogisticsCarrier()
/*
21-22行又多出来一个分支需要对subType进行再次赋值
*/
if (this.isPersonPermission && !this.crossUser) {
this.subType=this.tabType.warehouse
}
},
getAllLogisticsCarrier() {
let getCarrier=API.getAllLogisticsCarrier
if (this.crossUser) {
getCarrier=API.getOrderShipAllLogistics
}
getCarrier({}).then(res=> {
if (res.code===200) {
const options=[]
.......... // 给options赋值
this.options2=options
}
})
},
}
我们能看到7-15、24-125行对this.subType进行了赋值。但由于我们无法确定20行的函数是否也对this.subType进行了赋值,所以光凭mounted函数的代码我们并不能完全确定subType的值究竟是什么,需要跳转到getAllLogisticsCarrier函数确认。这段代码在这里已经做了简化,实际的代码像getAllLogisticsCarrier这样的调用还有好几个,要想搞清楚逻辑就得把所有函数全看一遍,代码的可读性一般。同时,由于函数都封装在ui组件里,因此要想给函数覆盖单测的话也需要一些改造。
为了解决问题,我们将这部分逻辑都拆分到usecase层:
// prepare-order-page.ts
import { tabType } from '@/constants'
interface IParams {
subType?: number
merchantType: number
isCrossUser: boolean
isPersonPermission: boolean
}
/*
做依赖倒置主要是为了方便后续的单测和复用
*/
export default function buildPrepareOrderPage({
queryLogisticsCarriers,
}: {
queryLogisticsCarriers: ()=> Promise<{ carriers: ICarrires }>
}) {
return async function prepareOrderPage(params: IParams) {
const activeTab=selectActiveTab(params)
const { carriers }=queryLogisticsCarriers(params.isCrossUser)
return {
activeTab,
carriers,
}
}
}
function selectActiveTab({
subType,
isCrossUser,
isPersonPermission,
merchantType,
}: IParams) {
if (isPersonPermission && !isCrossUser) {
return tabType.warehouse
}
if (Number(subType)===0 || subType) {
return subType.toString()
}
if (merchantType===4) {
return tabType.cross
}
return tabType.ordinarySpot
}
// query-logistics-carriers
export default function buildQueryLogisticsCarriers({
fetchAllLogisticsCarrier,
fetchOrderShipAllLogistics,
}: {
fetchAllLogisticsCarrier: ()=> Promise<{ data: {carriers: ICarrires }}>
fetchOrderShipAllLogistics: ()=> Promise<{ data: {carriers: ICarrires }}>
}) {
return async function queryLogisticsCarriers(isCrossUser: boolean) {
if (isCrossUser) {
return fetchAllLogisticsCarrier()
}
return fetchOrderShipAllLogistics()
}
}
// index.vue
{
mounted() {
const {activeTab, carriers}=prepareOrderPage(params)
this.subType=activeTab;
this.options=buildCarrierOptions(carriers) // 将carries转换成下拉框option
}
}
首先,可以看到所有usecase一定是一个纯函数,不会存在副作用的问题。
其次,prepareOrderPage usecase专门为订单页定制,拆分后一眼就能看出来订单页的准备工作需要干决定选中的tab和拉取供应商列表两件事情。而另一个拆分出来的queryLogisticsCarriers则是封装了商家后台跨境、国内两种逻辑,后续无论跨境还是国内的逻辑如何变更,其影响范围被限制在了queryLogisticsCarriers函数,我们需要对其进行功能回归;而对于prepareOrderPage来说,queryLogisticsCarriers只是()=> Promise<{ carriers: ICarrires }>的一个实现而已,其内部调用queryLogisticsCarriers的逻辑完全不受影响,不需要进行回归。
最后,而由于我们做了依赖倒置,我们可以非常容易的给usecase覆盖单测:
import buildPrepareOrderPage from '@/utils/create-goods';
function init() {
const queryLogisticsCarriers=jest.fn();
const prepareOrderPage=buildPrepareOrderPage({ queryLogisticsCarriers });
return {
prepareOrderPage,
queryLogisticsCarriers,
};
}
describe('订单页准备逻辑', ()=> {
it('当用户是国内商家且在入仓白名单上,在打开订单页时,默认打开入仓tab', async ()=> {
const { prepareOrderPage }=init();
const params={
merchantType: 2
isCrossUser: false
isPersonPermission: true
}
const { activeTab }=await prepareOrderPage(params)
expect(activeTab).toEqual({tabType.warehouse});
});
it('当用户是跨境商家,在打开订单页时,默认打开跨境tab', async ()=> {
const { prepareOrderPage }=init();
const params={
merchantType: 4
isCrossUser: true
isPersonPermission: true
}
const { activeTab }=await prepareOrderPage(params)
expect(activeTab).toEqual({tabType.cross});
});
......
});
单测除了进行功能回归之外,它的描述(demo里使用了Given-When-Then的格式,由于篇幅的原因,关于单测的细节在后续的文章再进行介绍)对于了解代码的逻辑非常非常非常有帮助。由于单测和代码逻辑强行绑定的缘故,我们甚至可以将单测描述当成一份实时更新的业务文档。
除了方便写单测之外,在通过usecase拆分完成之后,ui组件真正成为了只负责“ui”和监听用户交互行为的组件,这为我们后续的React技术栈迁移奠定了基础;通过usecase我们也实现了很不错的模块化,对于使用比较多的一些entity,他的crud操作可以通过独立的usecase具备了在多个页面甚至应用间复用的能力。
上面usecase例子中的fetchAllLogisticsCarrier就是一个adapter,这一层起到的作用是将外部系统返回的数据转化成entity,并以一种统一的数据格式返回回来。
这一层很核心的一点即是可以依赖entity的工厂函数,将接口返回的数据转化成前端自己设计的模型数据,保证流入usecase和ui层的数据都是经过处理的“干净数据”。除此之外,通常在这一层我们会用一种固定的数据格式返回数据,比如例子中的 {success: boolean, data?: any}。这样做主要是为了抹平对接多个系统带来的差异性,同时减少多人协作时的沟通成本。
type Request=(url: string, params: Record<string, any>)=> Promise<any>;
import makeCarrier from '@/entities/makeCarrier'
export default function buildFetchAllLogisticsCarrier({request}: {request: Request}) {
return async function fetchAllLogisticsCarrier() {
// TODO: 异常处理
const response=await request('/fakeapi', info)
if (!response || !resposne.code===200) {
return {
success: false
}
}
return {
success: true,
data: {
carriers: response.list?.map(makeCarrier)
}
}
}
}
通过Adapter + entity的组合,我们基本形成了前端应用和后端服务之间的防腐层,使得前端可以在完全不清楚接口定义的情况下完成ui渲染、usecase等逻辑的开发。在服务端产出定义后,前端只需要将实际接口返回适配到自己定义的模型(通过entity)即可。这一点对前端的测试周提效非常非常非常重要,因为防腐层的存在,我们可以在测试周完成需求评审之后根据prd的内容设计出业务模型,并以此完成需求开发,在真正进入研发周后只需要和服务端对接完成adapter这一层的适配即可。
在实践过程中,我们发现在对接同一个系统的时候(对商家来说就是stark服务)各个adapter对于异常的处理几乎一模一样(上述的11-15行),我们可以通过Proxy对其进行抽离实现复用。当然,后续我们也完全有机会根据接口定义来自动生成adapter。
在经过前面的拆分之后,无论咱们的UI层用React还是Vue来写,要做的工作都很简单了:
由于entity已经做了过滤和适配处理,所以在ui层我们可以放心大胆的用,不需要再写一堆莫名其妙的判断逻辑。另外由于entity是由前端自己定义的模型,无论开发过程中服务端接口怎么变,受影响的都只有entity工厂函数,ui层不会受到影响。
最后,在ui层我们还剩下令人头痛的技术栈迁移问题。整个团队目前使用vue的项目有10个,按迭代频率和项目规模迁移的方案可以分为两类:
通过整洁架构我们形成了统一的编码规范,在前端应用标准化的道路上迈下了坚实的一步。可以预见的是整个标准化的过程会非常漫长,我们会陆续往标准中增加新的规范使其更加完善,短期内在规划中的有:
后续在标准逐渐稳定之后,我们也期望基于稳定的规范进行一些工程化的实践(比如根据mooncake文档自动生成adapter层、基于usecase实现功能开关等),敬请期待。
The Clean Architecture:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Module Federation:https://webpack.js.org/concepts/module-federation/
Anti-corruption Layer pattern:https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer
*文/陈子煜
关注得物技术,每周一三五晚18:30更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
译自: https://opensource.com/article/18/10/book-to-website-epub-using-pandoc
作者: Kiko Fernandez-reyes
译者: jlztan
通过 Markdown 和 Pandoc,可以做到编写一次,发布两次。
Pandoc 是一个命令行工具,用于将文件从一种标记语言转换为另一种标记语言。在我 对 Pandoc 的简介 一文中,我演示了如何把 Markdown 编写的文本转换为网页、幻灯片和 PDF。
在这篇后续文章中,我将深入探讨 Pandoc ,展示如何从同一个 Markdown 源文件生成网页和 ePub 格式的电子书。我将使用我即将发布的电子书《 面向对象思想的 GRASP 原则 》为例进行讲解,这本电子书正是通过以下过程创建的。
首先,我将解释这本书使用的文件结构,然后介绍如何使用 Pandoc 生成网页并将其部署在 GitHub 上;最后,我演示了如何生成对应的 ePub 格式电子书。
你可以在我的 GitHub 仓库 Programming Fight Club 中找到相应代码。
我用 Markdown 语法完成了所有的写作,你也可以使用 HTML 标记,但是当 Pandoc 将 Markdown 转换为 ePub 文档时,引入的 HTML 标记越多,出现问题的风险就越高。我的书按照每章一个文件的形式进行组织,用 Markdown 的 H1 标记(#)声明每章的标题。你也可以在每个文件中放置多个章节,但将它们放在单独的文件中可以更轻松地查找内容并在以后进行更新。
元信息遵循类似的模式,每种输出格式都有自己的元信息文件。元信息文件定义有关文档的信息,例如要添加到 HTML 中的文本或 ePub 的许可证。我将所有 Markdown 文档存储在名为 parts 的文件夹中(这对于用来生成网页和 ePub 的 Makefile 非常重要)。下面以一个例子进行说明,让我们看一下目录,前言和关于本书(分为 toc.md、preface.md 和 about.md 三个文件)这三部分,为清楚起见,我们将省略其余的章节。
关于本书这部分内容的开头部分类似:
# About this book {-}
## Who should read this book {-}
Before creating a complex software system one needs to create a solid foundation.
General Responsibility Assignment Software Principles (GRASP) are guidelines to assign
responsibilities to software classes in object-oriented programming.
每一章完成后,下一步就是添加元信息来设置网页和 ePub 的格式。
我创建的网页的元信息文件(web-metadata.yaml)是一个简单的 YAML 文件,其中包含 <head> 标签中的作者、标题、和版权等信息,以及 HTML 文件中开头和结尾的内容。
我建议(至少)包括 web-metadata.yaml 文件中的以下字段:
---
title: <a href="/grasp-principles/toc/">GRASP principles for the Object-oriented mind</a>
author: Kiko Fernandez-Reyes
rights: 2017 Kiko Fernandez-Reyes, CC-BY-NC-SA 4.0 International
header-includes:
- |
```{=html}
<link href="https://fonts.googleapis.com/css?family=Inconsolata" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Gentium+Basic|Inconsolata" rel="stylesheet">
```
include-before:
- |
```{=html}
<p>If you like this book, please consider
spreading the word or
<a href="https://www.buymeacoffee.com/programming">
buying me a coffee
</a>
</p>
```
include-after:
- |
```{=html}
<div class="footnotes">
<hr>
<div class="container">
<nav class="pagination" role="pagination">
<ul>
<p>
<span class="page-number">Designed with</span> ?? <span class="page-number"> from Uppsala, Sweden</span>
</p>
<p>
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="http://www.hmttv.cn/uploadfile/2024/1009/20241009093826494.png" /></a>
</p>
</ul>
</nav>
</div>
</div>
```
---
下面几个变量需要注意一下:
这些只是其中一部分可用的变量,查看 HTML 中的模板变量(我的文章 Pandoc简介 中介绍了如何查看 LaTeX 的模版变量,查看 HTML 模版变量的过程是相同的)对其余变量进行了解。
网页可以作为一个整体生成,这会产生一个包含所有内容的长页面;也可以分成多章,我认为这样会更容易阅读。我将解释如何将网页划分为多章,以便读者不会被长网页吓到。
为了使网页易于在 GitHub Pages 上部署,需要创建一个名为 docs 的根文件夹(这是 GitHub Pages 默认用于渲染网页的根文件夹)。然后我们需要为 docs 下的每一章创建文件夹,将 HTML 内容放在各自的文件夹中,将文件内容放在名为 index.html 的文件中。
例如,about.md 文件将转换成名为 index.html 的文件,该文件位于名为 about(about/index.html)的文件夹中。这样,当用户键入 http://<your-website.com>/about/ 时,文件夹中的 index.html 文件将显示在其浏览器中。
下面的 Makefile 将执行上述所有操作:
# Your book files
DEPENDENCIES=toc preface about
# Placement of your HTML files
DOCS=docs
all: web
web: setup $(DEPENDENCIES)
@cp $(DOCS)/toc/index.html $(DOCS)
# Creation and copy of stylesheet and images into
# the assets folder. This is important to deploy the
# website to Github Pages.
setup:
@mkdir -p $(DOCS)
@cp -r assets $(DOCS)
# Creation of folder and index.html file on a
# per-chapter basis
$(DEPENDENCIES):
@mkdir -p $(DOCS)/$@
@pandoc -s --toc web-metadata.yaml parts/$@.md \
-c /assets/pandoc.css -o $(DOCS)/$@/index.html
clean:
@rm -rf $(DOCS)
.PHONY: all clean web setup
选项 - c /assets/pandoc.css 声明要使用的 CSS 样式表,它将从 /assets/pandoc.cs 中获取。也就是说,在 <head> 标签内,Pandoc 会添加这样一行:
<link rel="stylesheet" href="/assets/pandoc.css">
使用下面的命令生成网页:
make
根文件夹现在应该包含如下所示的文件结构:
.---parts
| |--- toc.md
| |--- preface.md
| |--- about.md
|
|---docs
|--- assets/
|--- index.html
|--- toc
| |--- index.html
|
|--- preface
| |--- index.html
|
|--- about
|--- index.html
通过以下步骤将网页部署到 GitHub 上:
你可以在 GitHub Pages 的网站上获得更多详细信息。
我的书的网页 便是通过上述过程生成的,可以在网页上查看结果。
ePub 格式的元信息文件 epub-meta.yaml 和 HTML 元信息文件是类似的。主要区别在于 ePub 提供了其他模板变量,例如 publisher 和 cover-image 。ePub 格式图书的样式表可能与网页所用的不同,在这里我使用一个名为 epub.css 的样式表。
---
title: 'GRASP principles for the Object-oriented Mind'
publisher: 'Programming Language Fight Club'
author: Kiko Fernandez-Reyes
rights: 2017 Kiko Fernandez-Reyes, CC-BY-NC-SA 4.0 International
cover-image: assets/cover.png
stylesheet: assets/epub.css
...
将以下内容添加到之前的 Makefile 中:
epub:
@pandoc -s --toc epub-meta.yaml \
$(addprefix parts/, $(DEPENDENCIES:=.md)) -o $(DOCS)/assets/book.epub
用于产生 ePub 格式图书的命令从 HTML 版本获取所有依赖项(每章的名称),向它们添加 Markdown 扩展,并在它们前面加上每一章的文件夹路径,以便让 Pandoc 知道如何进行处理。例如,如果 $(DEPENDENCIES 变量只包含 “前言” 和 “关于本书” 两章,那么 Makefile 将会这样调用:
@pandoc -s --toc epub-meta.yaml \
parts/preface.md parts/about.md -o $(DOCS)/assets/book.epub
Pandoc 将提取这两章的内容,然后进行组合,最后生成 ePub 格式的电子书,并放在 Assets 文件夹中。
这是使用此过程创建 ePub 格式电子书的一个 示例 。
从 Markdown 文件创建网页和 ePub 格式电子书的过程并不困难,但有很多细节需要注意。遵循以下大纲可能使你更容易使用 Pandoc。
via: https://opensource.com/article/18/10/book-to-website-epub-using-pandoc
作者: Kiko Fernandez-Reyes 选题: lujun9972 译者: jlztan 校对: wxy
本文由 LCTT 原创编译, Linux中国 荣誉推出
作者:xybaby 来源:https://www.cnblogs.com/xybaby/p/11335829.html
出整洁的代码,是每个程序员的追求。
《clean code》指出,要想写出好的代码,首先得知道什么是肮脏代码、什么是整洁代码;然后通过大量的刻意练习,才能真正写出整洁的代码。
WTF/min是衡量代码质量的唯一标准,Uncle Bob在书中称糟糕的代码为沼泽(wading),这只突出了我们是糟糕代码的受害者。
国内有一个更适合的词汇:屎山,虽然不是很文雅但是更加客观,程序员既是受害者也是加害者。
对于什么是整洁的代码,书中给出了大师们的总结:
其中,我最喜欢的是表达力(Expressiveness)这个描述,这个词似乎道出了好代码的真谛:用简单直接的方式描绘出代码的功能,不多也不少。
本文记录阅读《clean code》之后个人“深有同感”或者“醍醐灌顶”的一些观点。
命名的艺术
坦白的说,命名是一件困难的事情,要想出一个恰到好处的命名需要一番功夫,尤其我们的母语还不是编程语言所通用的英语。
不过这一切都是值得了,好的命名让你的代码更直观,更有表达力。
好的命名应该有下面的特征:
名副其实
好的变量名告诉你:是什么东西,为什么存在,该怎么使用
如果需要通过注释来解释变量,那么就先得不那么名副其实了。
下面是书中的一个示例代码,展示了命名对代码质量的提升
避免误导
这里不得不吐槽前两天才看到的一份代码,居然使用了 l 作为变量名;而且,user居然是一个list(单复数都没学好!!)
有意义的区分
代码是写给机器执行,也是给人阅读的,所以概念一定要有区分度。
# bad def copy(a_list, b_list): pass # good def copy(source, destination): pass
使用读的出来的单词
如果名称读不出来,那么讨论的时候就会像个傻鸟
使用方便搜索的命名
名字长短应与其作用域大小相对应
避免思维映射
比如在代码中写一个temp,那么读者就得每次看到这个单词的时候翻译成其真正的意义
注释
有表达力的代码是无需注释的。
The proper use of comments is to compensate for our failure to express ourself in code.
注释的适当作用在于弥补我们用代码表达意图时遇到的失败,这听起来让人沮丧,但事实确实如此。
The truth is in the code, 注释只是二手信息,二者的不同步或者不等价是注释的最大问题。
书中给出了一个非常形象的例子来展示:用代码来阐述,而非注释
bad // check to see if the employee is eligible for full benefit if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)) good if (employee.isEligibleForFullBenefits())
因此,当想要添加注释的时候,可以想想是否可以通过修改命名,或者修改函数(代码)的抽象层级来展示代码的意图。
当然,也不能因噎废食,书中指出了以下一些情况属于好的注释
其中个人最赞同的是第2点和第5点,做什么很容易通过命名表达,但为什么要这么做则并不直观,特别涉及到专业知识、算法的时候。
另外,有些第一感觉“不那么优雅”的代码,也许有其特殊愿意,那么这样的代码就应该加上注释,说明为什么要这样,比如为了提升关键路径的性能,可能会牺牲部分代码的可读性。
最坏的注释就是过时或者错误的注释,这对于代码的维护者(也许就是几个月后的自己)是巨大的伤害,可惜除了code review,并没有简单易行的方法来保证代码与注释的同步。
函数
函数的单一职责
一个函数应该只做一件事,这件事应该能通过函数名就能清晰的展示。判断方法很简单:看看函数是否还能再拆出一个函数。
函数要么做什么 do_sth, 要么查询什么 query_sth。最恶心的就是函数名表示只会 query_sth, 但事实上却会 do_sth, 这使得函数产生了副作用。比如书中的例子
函数的抽象层级
每个函数一个抽象层次,函数中的语句都要在同一个抽象层级,不同的抽象层级不能放在一起。比如我们想把大象放进冰箱,应该是这个样子的:
def pushElephantIntoRefrige(): openRefrige() pushElephant() closeRefrige()
函数里面的三句代码在同一个层级(高度)描述了要完成把大象放进冰箱这件事顺序相关的三个步骤。
显然,pushElephant 这个步骤又可能包含很多子步骤,但是在pushElephantIntoRefrige 这个层级,是无需知道太多细节的。
当我们想通过阅读代码的方式来了解一个新的项目时,一般都是采取广度优先的策略,自上而下的阅读代码,先了解整体结构,然后再深入感兴趣的细节。
如果没有对实现细节进行良好的抽象(并凝练出一个名副其实的函数),那么阅读者就容易迷失在细节的汪洋里。
某种程度看来,这个跟金字塔原理也很像
每一个层级都是为了论证其上一层级的观点,同时也需要下一层级的支持;同一层级之间的多个论点又需要以某种逻辑关系排序。
pushElephantIntoRefrige 就是中心论点,需要多个子步骤的支持,同时这些子步骤之间也有逻辑先后顺序。
函数参数
函数的参数越多,组合出的输入情况就愈多,需要的测试用例也就越多,也就越容易出问题。
输出参数相比返回值难以理解,这点深有同感,输出参数实在是很不直观。
从函数调用者的角度,一眼就能看出返回值,而很难识别输出参数。输出参数通常逼迫调用者去检查函数签名,这个实在不友好。
向函数传入Boolean(书中称之为 Flag Argument)通常不是好主意。
尤其是传入True or False后的行为并不是一件事情的两面,而是两件不同的事情时。这很明显违背了函数的单一职责约束,解决办法很简单,那就是用两个函数。
Dont repear yourself
在函数这个层级,是最容易、最直观实现复用的,很多IDE也难帮助我们讲一段代码重构出一个函数。
不过在实践中,也会出现这样一种情况:一段代码在多个方法中都有使用,但是又不完全一样,如果抽象成一个通用函数,那么就需要加参数、加if else区别。这样就有点尴尬,貌似可以重构,但又不是很完美。
造成上述问题的某种情况是因为,这段代码也违背了单一职责原则,做了不只一件事情,这才导致不好复用,解决办法是进行方法的细分,才能更好复用。也可以考虑template method来处理差异的部分。
测试
非常惭愧的是,在我经历的项目中,测试(尤其是单元测试)一直都没有得到足够的重视,也没有试行过 TDD。正因为缺失,才更感良好测试的珍贵。
我们常说,好的代码需要有可读性、可维护性、可扩展性,好的代码、架构需要不停的重构、迭代,但自动化测试是保证这一切的基础,没有高覆盖率的、自动化的单元测试、回归测试,谁都不敢去修改代码,只能任其腐烂。
即使针对核心模块写了单元测试,一般也很随意,认为这只是测试代码,配不上生产代码的地位,以为只要能跑通就行了。
这就导致测试代码的可读性、可维护性非常差,然后导致测试代码很难跟随生产代码一起更新、演化,最后导致测试代码失效。
所以说,脏测试 - 等同于 - 没测试。
因此,测试代码的三要素:可读性,可读性,可读性。
对于测试的原则、准则如下:
测试的FIRST 准则:
*请认真填写需求信息,我们会在24小时内与您取得联系。