当前位置: 首页 > 后端技术 > Python

不追求干净代码的程序员不是好程序员

时间:2023-03-26 18:29:50 Python

编写干净的代码是每个程序员的追求。《clean code》指出,要想写出好的代码,首先要知道什么是脏代码,什么是干净代码;然后通过大量的刻意练习,你才能真正写出干净的代码。WTF/min是衡量代码质量的唯一标准,而Bob大叔在他的书中称糟糕的代码涉水,这只会凸显我们是糟糕代码的受害者。国内有一个比较合适的词汇:屎山,虽然不是很优雅但是比较客观,程序员既是受害者也是加害者。对于什么是cleancode,书中给出了大师的总结:BjarneStroustrup:Elegantandefficient;测试RonJeffries:无重复,单一职责,表现力(Expressiveness)其中,我最喜欢的是表现力(Expressiveness)的描述。功能,不多也不少。命名的艺术坦率地说,命名是一件很难的事情。想出一个合适的命名需要做很多工作,尤其是当我们的母语不是编程语言中常用的英语时。但这一切都是值得的,好的命名可以让你的代码更直观、更有表现力。一个好的起名应该具备以下特点:名副其实的名字。一个好的变量名会告诉你:它是什么,为什么存在,以及如何使用它。如果需要通过注释来解释变量,那就不是那么名副其实了。以下是书中的示例代码,展示了命名如何提高代码质量#badcodedefgetItem(theList):ret=[]forxintheList:ifx[0]==4:ret.append(x)returnret#goodcodedefgetFlaggedCell(gameBoard):'''扫雷游戏,标记:翻转'''flaggedCells=[]forcellingameBoard:ifcell.IsFlagged():flaggedCells.append(cell)returnflaggedCellstoavoidmissingandnot挂起不要覆盖这里的惯用缩写。不得不吐槽前两天看到的一段代码,用了l作为变量名;而且,user其实就是一个列表(我没学过单复数!!)有意义区分代码是写给机器执行和人阅读的,所以概念上一定要区分一下#baddefcopy(a_list,b_list):pass#gooddefcopy(source,destination):pass使用名字读出来的词如果不出来,讨论的时候会像个傻子。用于便于搜索的名称的长度应与其范围的大小相对应。避免思维导图,比如在代码中写一个temp,那么读者每次看到这个词都要翻译成它真正的意思。注释表达性代码未注释。正确使用注释是为了弥补我们在代码中无法表达自己的不足。这听起来令人沮丧,但这是事实。其实在代码中,注释只是二手信息,两者不同步或不相等才是注释最大的问题。书中给出了一个非常形象的例子来说明:用代码来解释,而不是评论bad//检查员工是否有资格获得全额福利if((employee.flags&HOURLY_FLAG)&&(employee.age>65))goodif(employee.isEligibleForFullBenefits())因此,当你想添加注释时,你可以考虑是否可以通过修改名称或修改函数(代码)的抽象级别来显示代码的意图。当然,你不能因为噎住而放弃进食。书中指出以下几种情况是很好的注解1.法律信息2.解释意图,为什么要这样做3.警告4.TODO注解5.放大看似不合理的事情的重要性其中,我个人是认同的与第2点和第5点。通过命名很容易表达做什么,但是为什么这样做并不直观,尤其是涉及专业知识和算法时。另外,一些一开始感觉“不太优雅”的代码可能会有特殊的愿望,所以应该对此类代码进行注释,说明为什么会这样。例如,为了提高关键路径的性能,可能会牺牲一些代码的可靠性。可读性。最糟糕的一种注释是过时的或错误的注释,这对代码的维护者(可能是几个月后的你自己)是一个巨大的伤害。不幸的是,除了代码审查,没有简单易行的方法来确保代码和注释的完整性。同步。一个函数的职责单一一个函数应该只做一件事,而这件事应该通过函数名来明确展示。判断的方法很简单:看功能是否可以拆分成另一个功能。该函数要么执行do_sth,要么查询query_sth。最恶心的是函数名明明只会query_sth,实际上却会do_sth,使得函数产生了副作用。比如书中的例子publicclassUserValidator{privateCryptographercryptographer;publicbooleancheckPassword(StringuserName,Stringpassword){Useruser=UserGateway.findByName(userName);if(user!=User.NULL){StringcodedPhrase=user.getPhraseEncodedByPassword();Stringphrase=cryptographer.decrypt(codedPhrase,password);if("有效密码".equals(phrase)){Session.initialize();返回真;}}返回假;函数是一个抽象层次,函数中的语句必须在同一个抽象层次,不同的抽象层次不能放在一起。例如,如果我们想把一头大象放进冰箱,它应该是这样的:defpushElephantIntoRefrige():openRefrige()pushElephant()closeRefrige()函数中的三行代码描述了如何完成大象有与放入冰箱的顺序相关的三个步骤。显然,pushElephant步骤可能包含很多子步骤,但是在pushElephantIntoRefrige这一层,不需要了解太多细节。当我们想通过阅读代码来理解一个新项目时,我们一般会采用广度优先的策略,从上到下阅读代码,先了解整体结构,再深入感兴趣的细节。如果实现细节没有很好地抽象(并浓缩成一个名副其实的函数),那么读者很容易迷失在细节的海洋中。这在某种程度上类似于金字塔的原理。每一层都是为了证明上层的观点,也需要下一层的支持;同一级别之间的多个参数需要某种逻辑。关系排序。pushElephantIntoRefrige是中心论点,需要多个子步骤的支持,并且这些子步骤之间有逻辑顺序。函数参数函数参数越多,输入条件组合越多,需要的测试用例越多,越容易出问题。与返回值相比,输出参数比较难理解。我非常同意这一点。输出参数确实不直观。从函数调用者的角度来看,返回值一目了然,但很难识别输出参数。输出参数通常会强制调用者检查函数签名,这确实很不友好。将布尔值(在书中称为标志参数)传递给函数通常不是一个好主意。尤其是当通过True或False后的行为不是一个事物的两个方面,而是两个不同的事物。这显然违反了函数的单一职责约束,解决方法很简单,就是使用两个函数。不要在功能层面重复自己,这是实现复用最简单、最直观的方式。很多IDE也很难帮我们把一段代码重构成一个函数。但是,在实践中,也会出现这样的情况:一段代码用在多个方法中,但并不完全相同。如果抽象成一个通用的函数,那么就需要加参数,加上ifelse来区分。这就有点尴尬了,貌似可以重构,但并不完美。上面的一些问题是因为这段代码也违反了单一职责原则,做了不止一件事,导致难以复用。解决方案是将方法细分,以更好地复用。也可以考虑用模板方法来处理差异。测试很遗憾的是,在我经历过的项目中,测试(尤其是单元测试)并没有得到足够的重视,也没有尝试过TDD。也正是因为缺席,我才感受到一场好考的珍贵。我们常说好的代码需要可读性、可维护性和可扩展性。好的代码和架构需要不断的重构和迭代,而自动化测试是这一切的基础。没有高覆盖率、自动化单元测试、回归测试,没人敢修改代码,只能任其烂掉。即使是为核心模块写了单元测试,一般也很随意,认为只是测试代码,不配生产代码的地位,只要能跑通就行。这导致测试代码的可读性和可维护性非常差,使得测试代码很难随着生产代码更新和演进,最终导致测试代码的失败。所以,肮脏的测试-相当于-没有测试。因此,测试代码的三要素:可读性、可读性、可读性。测试的原则和准则如下:除非是为了让失败的单元测试通过,否则不允许编写任何生产代码。不允许编写比现在更多的单元测试,编译失败就是失败。您不得编写足以通过一个失败的单元测试的生产代码。您不得编写足以通过一个失败的单元测试的生产代码。代码测试的FIRST准则:快速(Fast)测试应该足够快并且尽可能自动化。独立(Independent)测试应该是独立的。不要相互依赖可重复(Repeatable)测试在任何环境下都应该是可重复的。自验证测试应该有bool输出。不要通过低效的方式看日志来判断测试是否通过。及时(Timely)测试应该在其相应的生产代码之前及时编写