无状态以太坊:二进制状态树实验 | 火星技术贴
好的代码是清晰的代码,而不是聪明的代码
Photo by David Travis on Unsplash
许多程序员尝试编写干净,智能的代码。 但是,有时候,痴迷于智能可能会使代码库更难以理解,并且可能会花费大量时间来阅读和维护它。
如今,在团队合作中,人们逐渐意识到编写人工代码的意义,这意味着您在编写代码时应该尊重他人,而不是炫耀自己的智慧。 人们正在尝试不要使用\"干净\K x 1 v"一词,因为这意味着即使您不是故意的,代码也很脏。 丹尼尔欧l D h % 文(Daniel Irvine)在他的文章\"干( ! h 8净代码,肮脏代码,人类代码\"中谈到了这一点。
我K l Y }并不是说干净是一件坏事。 在处理个人项目时,我会尝试以一种聪明的方式使代码库变得干净。 但更重要的是,我使代码库更具可读性和可理解性。
正如鲍伯叔叔在他的《清洁守则》中9 4 W & P 1所说:
\"总6 W E的来说,程序员是非常聪明的 i * H ^ [ { j人。 聪明的人有时喜{ p _欢通过展示他们的心理杂耍能力来炫耀a B t自己的聪明人。 毕竟,如果您可以可靠地记住r是URL的小写版本,并且除去了主机和Z T / U & E * 4Schema,那么您显然必须非常聪明。 聪明的程序员和专业的程序@ g z员之间的区别是,w b ) (专业人士理解清晰为王。 专业人士会尽力而为,并编写他人可以理解的代码。\"-罗伯特C马丁
最重要的是编写清晰易懂的代码。 其他人不仅是其他人,而且还是您,他们 Q | L H e @ 7将在几个月内重写代码。
在本& l 8文中,我并不是在谈论人的代码方面。 相反,我将通过一些示例来重点介绍如何将该原理应用于您的代码,并最大程度地减少花一些时间来理解它的* l L } T L 8时间B M T g . o y。
注意:B Z f X * X ~ l z为了解释一些技巧,我将使用JavaScript或TypeScript。
涵盖的示例:
命名
注释
条件
循环
职能
测试
命名
在软件开发中,许多程序员在命名事物时遇到麻烦。 但是我个人认为,关键是避免歧义并使用特定的词语。
例如:
const fetch = async ()? U A 5 8 T B => {
return await axios.get(\'/us7 ~ v _ 8 $ G n 9ers\')
}
const users = await fetch()
...在此代码中,您可以预期将从服务器获取什么内容。 但是,如果导出了导出功能并在其他文{ Z ( u s #件中使用该怎么办?
export const fetch` & l m p ^ G =a U x # | N [ 8 h asyU B Q r v H { l 4nc () => {
return await axios.get(\'/users\')
}在其他文件中:
import { fetch } from # P h M ! Z #\'./utils\'fetch()
// fetch ... what?相反,您可以更具体地命名:
export cx T u P r b m = #onsi = i [ `t fetchU5 i csers = async () => {
return awail . J mt axios.get(\'/users\')
}我说过你应该避免歧义。 请注意以下通用i + $ F { n动词:
set
get
group
begin
validate
send
另一个例子:
const xxx = validateForm()在这段代码中,您可以理解validateForm是正在验证表单,但是您期望返回什么?
但是假设您这样写:
const xxx = isFormValid()然后非常清楚,该方法将返回true或false。
而且,如果您这样编写代码,则可以假定该方法将返回r : j f # s A一个数组或形式错误的映射:
const xxx = getFormErrors()另一个例子:
const toke: A O 8 % / U * Q l [ g w写代码时想出的宝贵见解。 如果没有这些注释,人们可能会认为存在错误,或者应该对代码进行s F # : V ` } I测试或修复,这可能会浪费时间。 为避免这种情况,您应该解释为什么以某种方式编写代码。重要的是要让自己穿上别人的鞋子。 提前考虑并预Z a M X H G D 0 T测人们可能会陷入的陷阱。
条件
在编写代码时,我们必须处理条件。 许多if / else语句使您停止阅读代码库,而陷入困境。 我相信条件语句越少,代码的可读性就越高。
通过使用圈复杂度,您可以计算代码的复杂度。 如果在代码中使用了大量的if / else,循环或switch语句,则计数将很高。 通常,计a r L h o .数越高,代码越复杂。
如果您是IntelliJ用户,则可以在首选项中检查复杂性:

选中\"功能过于复杂\"框。
并且当您在函数中编写许多if / else语句时,它会警告您:

注意:在VS Code中,可以使用一些插件,例如CodeMetrics。
关键是尽可能减少不必要的条件,并最大程度地减少理解代码的时间。
以下是一些简化和使条件可读的技巧:
首先处理正面的案例,而不是负面的案例
早点返回
使用Array.includes处理多个案例
使用可W 8 1 = p选链接处理未定义的检查
首先处理正面,而不是负面
哪个更适合您q _ =阅读?
if (!debug) {
// do something
} else {
debugSomething()
}
要么
if (debug) {
debugSomething()
} else {
// do something
}
在大多Q t b r l h N e数情况下,优先使用正{ w 9 V [ H ~面案例。 但是,如果否定情况是更简单,更谨慎的情况,则可以这样编写:
if (!user)
throw new Error(6 ` Q h h a R\'Please sign in first\')
// do a lot of things here
// ...
早点返回
例如:
export const formatDate = (date) => {
let result
if (date) {
const dateObj = new Date(date)
if (isToday3 + S , z h P V )(dateObj)) {
result = \'Today\'
} else if (isYesterday(dateObj)) {
result = \'Yesterday\'
} else if (!isThisYear(dateObj)) {
resa % 4 | 3ult = format(dateObj, \'MMMM d, yyyy\')
} else {
result = format(dateObj, \'MMMM d\')
}
} else {
throw new Error(\'No date\')
}
return result
}
它工作正常,但是代码有点长且嵌套。 而且,如果添加了if / else语句,将X m G更难弄清楚右H , ; @ $ S括号在哪里,并且更难调试代码。
为了使它看起来更整洁,我们需要做的是:
如果没有日w ~ p e A 5期,则抛出错误
如果日| L V期是今天,则C y # u J } .返回\"今天\"
如果日期是昨天,则返回{ R #\"_ E 8 e s 1 a o z昨天\"
如果日期不在今年K C u p k,则返回日期和年份
如果不符合上述条件,则返回日期和月份和日期
export const formatDate = (date) => {
if (!date) throw new Error(\'No date\') // If no date, throw an error
const dateObjN 9 } @ e @ = new Date(date)
if (isToday(dateObj4 s . [ . K z { p)) return \'Today\' // If the date is today, r? A ` _ e P P X Jeturn \'Today\'
if (isYesterday(daE + PteObjO a u E W | 7 8)) return \'Yesterday\' // If the date is yester{ t tday, return \'Yesterday\'
iL 1 1 C =f (!isThisYear(dateObj)) return format(dateObj, \'MMMM d, yyyy\') // If the date is not in this year, return date with year
return format(dateObj, \'MMMM d\f + O C q 5 N') // If noy a ~ K \'Persian\' || kind === \'Main , q 0 @ d ,e\' || kind === \q 6 ; [ g B & n 'British Shortha- 7 1 * o s , 6 $ir\') {
// do something ...
}
考虑到以后可以将其他条件添加` W H 7到语句中,我们想要重构代码,如下所示:
cons` P Y 6 x [ + a nt CATS_TYPE =
if (C; ^ D # S OATS_TYPE.includes(kind)){ j J d ~ y X {
// do[ | + something ...
}
具有类型数组,您可以从代码中单P | J E d 0 !独提取条件。
使用可选链接处理未定义的检查
可选的链接允许您深入访问嵌套对象,而无需在临时变量中重复分配结果。 通过使用此选项,可以1 @ O 0 0 ? 1 f o减E Y * L ( 9少条件检查中的多次检查。
注意:如果要在JavaScript中使用可选的链接运算符,则需要安装BabU s + , 4 o / a ~el插件。 在3.7以上的Typescript中,无需任何配置即Z L e { o u B l可使用它。
例如:
if (user && ust H 2er.addressInfo) {
let zipcode
if (user.addressInfo.zipcode) {
zipcode = user.addressInfo.zipcode
} else {
zipcode = \'\'
}
// do something
}
如果要检查用户是否存在并避免发生未定义的- H D )错误,则需要编o x h { :写类似于上面示例的条件。
但是通过使用可选的链接运算符,代码将是:
const zipcode = user?.addressInfo?.zipcode || \'j w e / 6 \'
这样看起来更好并且更易于维护。 可以访问内部的嵌套对象并避免发生未定义的错误。
您可以; D b &在TypeScript游乐场中使P L K用此炫酷功能
循环
简化循环使您的代码更容易理解。
在实际情况下, L 5 |您可能D _ % k p 7 X Z会在对象中遇到复杂的嵌套循环。 如果您有嵌套对象并且必须在todo3中获得列表N } F g名称,该怎么办:
const todos = [
{
code: \'code\',
name: \'name\',
list: ,
todo2: [
{
code2: \'co, ^ + C V 8 nde2\',
name2: \'name2\',
l4 6 s J @ V bist:` $ E u | { L ,
todo3: [
{
code3: \'code3\',
name3: \'name3\',
list:
}
]
}
]
},
]
例如,您可以这样编写:
const list = ;
todos.forEach(t => {
t.todo2) 4 i S = x.forEach(t2 => {
t2.todo3.forEac% 7 j Bh(t3 =>M M K B; {! J % ?
t3.list.forEach(l => {
list.pug m ush({
todo3Name: l.name
})
}j . M 3 2)
})
})
})
您会得到以下列表:

但这可以使用re/ e * $ 0 {duce函数来改进:
const list = todos
.r* p c O . Q Reduce((acc, t) => , )
.reduce((acc, t2) => , )
.reduce((acc, t& g 0 5 ! , 2 x3) => , )
.map(l =E N v F E w R W ^> ({ todo3Name: l.name }))
那也很好。

为了避免可读代码的复杂性,删除嵌套循环至关重要。
职能
在编写函数时,请牢记以下提示:
使用摘要Q I U O v ~ P名称来说明其操作。
为一个目的创建一个功能。
较小的函数更易读。
使用摘要名称来说明其操作
乍看之下的代码如下,您可~ r j 9 R , $ ~能会停止阅读并试图弄清楚它在做什么:
const tmp = new SP A d s et();
const filtered = lists.filter(a => !tmp.has(a.code) && tmp.add(a.code))
那呢?
const+ i filtered = uniqueByCode(lists)
您可能会期望有一S } - , H v S |个函数,} - 0 L - r m E通过查看对象来删除具6 [ o X有重复代码的O / x 1 w X t对象。
两者都能很好地工作,并获得相同的结果。

但是第二个更具可读性,可以帮助解释该功能的作用。
另一个例子:
const person = { score: 25 };
let newScore = person.score
newScore = newScore + newScore
newScore += 7
newSv r ~ Fcore = Math.max(0, Math.miH m 6 _ ( zn(100, newScore));
console.log(newScore) // 5A ) i ( r J a {7
如果我们为其编写函数,则代码将如下所示:
let newScore = person.s0 9 U u 9re = add(newScore, 7)
newScore = boundScore(0, 100, newScoreK l n m j Z)
console.log(newScore) // 5] T 7 Y 3 y7
好多了 但个人而言,我喜欢管道运算符的想法,该运算符与将多个函数o C m O h T链接在一起以提高函数编程的可读性一起使用。
如果我们在JavaScript中使用管道,则代码将如下所示:
const person = { score: 25 };
con= # # 8 X ) Q ~st newScore = person.score
|&gK . T } 5 ] tt; double
|> add(7, ?)
|> boundScore(0, 100, ?);
newScorj $ y V 1 R h ae //=> 57
为一个目的创建功能
现在我们了解了摘要名称的重要性。 但是 M A e / 8 B,如t s K 0 a F L g果您不能为您的功能起一个好名字怎么办?
例如:
const updat e DeUser = async (user) =&g d . Y 2 ^t; {
try {
await axios.post(\'/users\', user)
await axx t Lios.post(\'/user/profile\', user.profile)
const email = ne/ 3 r B T , 9w Email()
await email.send(user.email, \'User has be( 6 Y r % 0en updatedq y % / 2 successfully* z ) e w % 6 +\')
const logger = new Logger()
logger.notify()
} catch (e) {
conZ - U s k H | ~ `sole.log(e)
throw new Error(e)
}
}
您可能想知道它应该是updateUserAndProfid D qle,updateUserAndProfileAndNotify还是其他名称。 当您陷入困境时,就该将代码分成较小的部分了,因为人们很难同时理解多条代码。
当您编写用于更新用户的函数时,代码应如下所示:
const updateUser = async (user) => {
try {
await axios.post(\'/users\', user)
} catch (e) {
// handling error
}
}
const handleUpdate = async (user, onUpd ^ 8 u c #dated) => {
try {
await updateUser(K J i juser- a / E g)
await updateProfile(usr X U q H R xxAgeLimit\') || \'\')
if (params.maxAge) {
if (params.maxAge < 25) {
query.maxAge = params.maxAge
}
} elsL 0 y ae {
query.maxAge = defaultMaxAgeLimit
}
if (params.limit) {
query.limit = params.limit
}
// do a lot of things here
// ...
} catch(err) {
// error handing
}
return query
}
该函数输出如下:

假设您在一个函数中有大量代码,这些函数创建查询来搜索某些数. J L o c #据。 如果电子邮件查询出了点问题,则您必须浏览内部的函数,找到电子邮件的实现并进行修复。 之后,您将必须检查更改是否会影响该函数d W B y中的其他代码。
通常,人们一次只能考虑两件事。 代码表达越大,理解和维护就越困难。
因此h $ k { K 2,使代码G } G L } 9 e J &更小:
const email = (query+ x _ 5 v F |, paramM I m E Is) => {
if (!params.email) return query
const isJ * U 3 Q z ] aValid = isValidEmail(params.email)
if (!isValid) throw new Error(\'Invalid email\')
r6 : F M MetuE J V rn { ...query, ...{ email: params.= G R )email } }
}
const maxAge = (query, params) => {
const obj = { maxAge: \'\' }
if (!params.maxAge) obj.maxAge = JSON.parse(localStorage.getItem(\'defaultMaxAgeLq P E @ C R K $ .imit\') || \'\')
if (params.maxAge && params.maxAge < 25) obj.maxAge = params.maxAgy ( ; z / { Pe
return { ...query, ...obj }
}
const limit = (query, paramh ) 1 H j g ; } qs) => {
if (!params.limit) return query
return { ...queV % : Q O ` dry, ...{ limit! G % x ): pa5 t * xrams.limit } }
}
const generateQuery = (params) => {
let query = {}
try {
query = email(query, params)
query = maxAge(query, params)
query = limit(query4 [ t D : B M, params)
} cb r | h 4 k ^ Batch(err) {
// error handing
}
return query
}
将巨型代码分解为小段可以使代码更清晰,更易读。 更重要的是,每个问题都Y 3 | $ u j q N %与其余代码分开,因此您可以轻松地调试和测试它。
如果您还想使其更具通用性和q i A H声明性,则可以这样编写:
const generateQuery = (params, callbacks) => {
let query = {}
try {
ca| H q 9llbacks.forEach(c => {
query = c(query, params)
})
} catch(err) {R E u X
// error handing
}
return query
}
const result = generateQuery({ email: \'xxx@gmail.com\', maxAge: 20 }, )
它输出相同的结果。

尽管我建议我们希望使代码更小,更通用,但在重构之前,您将不得不首先考虑为h _ ; z =什么以这种方式编写此代码。 也许您的同事出于某种原因写了它。 即使有很多改进,您也想与编写它的人u h U n i {交谈,并讨论它是否好。
这全都与人工密码有关。 您可以在\"再见,干净代码\C * D ) k w T"一文中更详细地了解它。
测试中
我不是在谈论TDD,而是在团队开发中可读性对测试的重要性。
编写测试非常重要,因为:
在没有文档的情况下,您的队友可以通过q S v r ! ` )阅读测试说明轻松了解详细信息。
您的队友可以理解真正的代码应该如何工作以及为什Q S x I w |么。
您的队友可以轻松添加新功能,而不必担心会破坏代码。
鼓励您的队友添加测试。 (如果测试代码太大且令人生P U x p Y ! H畏,则可能会破窗!)
这些是我W ) W F个人在团队发展中的经验教训。 优秀的程序员始终会编写具有良好可维护性的测试。
这是编写测试时的一些技巧:
用简单的英语描述测试的目的(最好使用您的母语)。
遵循AAA(安排,执行,声明)模式。
使添加测试用例(表驱动的测试模式)变得容易。
用简单的英语描述测试要做什么
就像我在上面说的,如果测试是描述性的,则人们Q O Q 6 N U H 'RABBITS\} / } { ('
return null
}
并编写一个测试:
import { getAnimal } from \1 L j T O N 3'./util\';
describe(\'getAnimal\', () => {
it(\'passe1 9 ~ es\', () => {
expect(getAnimN M Y : 7 M @al(1)).toEqu, R ; Q b ( ` 1al(\'CATS\')
})
})这是一个非常简单的示例,因此您可能可以理解它正在尝试执行的操作。 但{ M ) R s u r 2 @是,如果它变大并弄乱了,您将很难理解它。
没关系,因为您已经编写了此功能,并且知道其功能。 但是测试不仅适合您,还适合您的队友。
让我们更具描述性:
describe(\'getAnimal\', () => {
it(\'should get CAc Y jTS when passing code 1\', () => {
expect(getAnimal(1)).toEqual(\'CATS\O ; 0')
})
})这看起来有点多余。 但这不是重点。 通过描述它,您可以使人们知道正确的行Z n G ? 2 q S @ 0 (下文中都更清楚,可以使用: R b Y T j ) I上下文块,如下所J S M t h a k e _示:
describe(\'getAnimal\', () => {
context(\'when passing code 1\', () => {
it9 S (\'should get CATX v ) n m p #S\', () => {
expect(getAnimal(1)).toEqual(\'CATS\')
})
}& , ] 8 p )
})注意:如果使用JestT 9 8 O B 7 R l ,,则可以安装jest-plugin-context。
通过这样编写,您可以在每个块中分隔特定的上下文。
遵循AAA模式
AAA模式允许您将测试分为三个部分:安排,操作] ! ? Z N A c p和声明。
在安排部分,您可以在其中设置数据或模拟要在测试中使用的| + n H功能。
act部分是调用测试方法并在需要时捕获输出值的地方。
assert部分是您对p 2 ] b x ) ,输出进行声明的地方。
如果将其应用于上面的示例,代码将如下所示:
describe(\'getAnimal\', () => {
context(\. D G r Q E D F'when ps % ! @ l % = uassing code 1\', () => {
beforeEach(() => {
// arrang7 8 & ) r g 0e
// prepare data here
})
itB 6 =(\'should get CATS\', () => {
// act
const result = getAnimal(1)
// assert
expect(result).toEqua4 s I 1 $ J *l(\'CATS\')
})
})
})这是使用酶进行反应测试的另一个示例:
describe(\'Component\', () => {
let wrapper: ReactWrapper;
beforeEach(()J | ] P . => {
// arrange
// mock useE- - p { k W ] 2 !ffect function
jest
.spyOn(React, \'useEffect\')
.mockImplementation(f => f());
});
it(\'should render successfully\', (X % G X h 5 l V) => {
// act
wrapper = mount(&7 d elt;Component {.% b K L q _ # j..props()? k t} /&K 2 Y =gt;);
// ass% o ) uert
expect(wrapper).toMatchSnapshot();
});
it(\'should update the text after clicking\', () => {
// act
wrapper = mount(<Component {...props()} />);
wrapper.find(\'button\').simulate(\'clicH @ Ak\');
// assert
expect(wrapper.text().includes(\"text updated!\"));
});
});一旦习惯了这种模式,就可以更轻松地阅读和理解测试。
在GitHub上,javascript-testing-best-practices是解释JavaScript测试的好指南。
轻松添加测试用例(表驱动测试模式)
在Go测试中,经常使用表驱动测试模式。 它的优点是能够通过定义每个表条目中的. [ O输入和预期结果来涵盖许多测试用j 2 I例。
这是Go中fmt包的示例:
var flagtests = struct {
in string
out string
}{
{\"%a\", \"\"},
{\^ r S"%-a\", \4 $ c"\"},
{\"%+a\", \"\"},
{\"%#a\", \"\"},
{\"% a\", \"g O 1\"},
{\"%0a\", \"\"},
{\"%1.2a\", \"\"},
{\"%-1.2a\", \"f c Q\"},
{\"%+1.2a\", \"\"},
{\"%-+1.2a\", \"\"},
{\"%-+1.2abc\", \"bc\"},
{\"%-1.2abc\", \"bc\"},
}
funcL O 1 M m TestFlagParser(t *testing.T) {
var flagprinter flagPrinter
for _, tt := range flagt5 l ? y , Rests {
t.Run(tt.in, func(t *testing.T) {
s := Sprintf(tt.in, &flagprinter)
if s !=~ : % i 3 ` L tt.out {
t.Errorf(\"got %q, want %q\", s, tt.out)
}
})M , ? $ Y :
}
}输入和预期输出在flagtests变量中定义。 您要做的就是循环浏览,运行测试并检查结果。
您可以将其应用于JavaScript测试。
例如:
import { getAnimal } from \'./util\'l S _ ] };
describe(\'getAnimal\', () =>} M _; {
context(\'when passing code 1\', () => {
it(\'should get CATS\', () => {
const result = getAn4 - =imal(1)
em ! } + E @xpect(result).toEqual(\'CATS\')
})
})
contextR ] o(\'when passing code 2\t $ / a W u ? g', () => {
it(\'should get DOGS\', () => {
const result = getAnimal(2)
expect(result).toEqual(\'DOGS\')
})
})
context(\'when passing coc M * n Z W rde 3\', () => {
it(\'should get Rc - oABBITS\', () => {
const result = getAnimal(3)
expex ! 3ct(resulV ] Z c 2 } p 7 bt).toEqual(\'RABBITS\')
})
})
context(\'whenu j d w no match\', () => {
it(i E I : Z F&gw a x s . ] )t; {
const result = getAnimal(100)
e_ c 0xpect(result).to| P $ G m d @Equal(null)
})
})
})并使其适用:
const cases =
describe(\'getAnimal2 w a ; U X 2\', () => {
cases.fv ` l ) sorEach(c => {
context(`when passing code ${c.cS 8 ; lode}`, () => {
it(`shoR ; 4uld get ${c.expected}`, () => {{ } 4 P 9
const result = g7 k I v R =etAnimal(c.code)
expect(result).O P utoEqual(c.ex| x { + V Z 5 8pected)
})
})
})
})测试将通过:
如果您有很多测试用例,则此技术将很有用。
结论
我通过一些示例介绍了如何使代码易于理解。 记住,干净,智能的代码并不总是更好。 重要的是退后一步并问自己:\"这比较干净,但是可以理解和阅读吗?\" 或\"其他队友是否可以维护?\"p * E + 2 如果是这样,你可以去做Z u。
希望本文L _ k ( l ) [ a Q对您有所帮助。
如果您有任何建议,意见和想法,请告诉我。
(本文翻译自Manato Kuroda的文章《m ! ? x & , e ZClarity Is Ki2 7 x =ng When Writing Code》,参考:https://medium.com/better-programming/clarity-is-king-when-writing-A B = jcode-752b85101484)