查看: 64|回复: 0

如何写代码 —— 编程内功心法

[复制链接]

该用户从未签到

发表于 2019-11-3 21:16:15 | 显示全部楼层 |阅读模式
写代码就是学一门语言然后开始撸代码吗?看完了我的《GoF设计模式》系列文章的同学或者本身已经就是老鸟的同学显然不会这么以为。编程是一项非常严谨的工作!固然我们自嘲为码农,但是这工作毕竟不是真正的搬砖,我们是软件工程师。编程需要关注的题目太多,不但仅有语言,还有算法、数据结构、编程本领、编码风格、设计、架构、工程化、开发工具、团队协作等方方面面,涉及到许多层面的题目。本文将分享一下根据我这几年来的编程经验总结出的一些关于怎样写代码的个人见解。
由于“跟我混”的一些小伙伴编程功底相对来说比较薄弱,所以在此总结一篇“编程内功心法”帮助他们渡过职业生涯的第一个瓶颈期。趁便,也造福一下途经的有缘的同学!于是有了此文。
前言

起首,思考一个题目,何谓编程?编程就是写代码吗?
所谓的编程,其实就是不断的对这个现实天下中的题目建立模型并将其固化为代码自动化实行的过程。
~ Bug辉GoF设计模式 - 解释器模式
在对题目建立模型的过程中,我们会遇到非常多不同层面的题目,所以我们需要许多领域的知识去解决这些题目。

  • 我们需要管理被操作的数据,因为数据与数据之前是相互有关联的。将数据结构化,通常是编程的第一步。关于结构化数据的相关理论以及实践,需要有一个专门的学品种Щ蛘咚悼翁馊パ芯俊数据结构
  • 我们需要解决一个详细的题目,这个详细的题目怎样一步步去解决,过程是怎么样子的——算法
  • 我们需要将解决方案进行自动化,并以代码的情势进行交付——编程语言
  • 如果将一个抽象的模型进行编码实现,怎样实现“这个功能”,怎样实现“那个功能”——编程本领
  • 题目的规模大了,浩繁代码糅合在一起,连程序员自己都看不懂了!怎么来拆分、模块化这些代码——设计
  • 代码量已经到了一个人无法完成的地步了,需要团队分工相助才能完成了——工程化
  • 你写的代码我看不懂,没法调用或者很难调用,我写的代码你也看不懂,或者很难看懂。还怎么舒畅的玩耍——编码风格/编码规范
  • 题目的规模继续扩大,到了体系工程的规模了,之前学的套路已经不管用了!怎么来构建这个体系才能实现精确、安全、高性能、高可用——架构
然而这些也只是一个体系工程中的冰山一角!这是一个巨大的体系。也正是因为软件开发需要考虑到的题目太多且团队成员水品参差不齐,所以团队开发中并不是每个程序员做的事情都是一样的。每个人都有自己的角色、低级工程师、中级工程师、高级工程师、架构师、CTO。。。
所以编程不但仅只是堆砌代码!
说到这里,我想起来了一件事情——为啥业界普遍鄙视培训出来半道出家的新人?人与人的区别是很大的!我见过培训出来也很牛的。其实,说到底,被鄙视的并不是全部人。而是那些培训了几个月之后发现随便找个工作也能拿“高薪”然后还自以为编程很简单的新人。因为这种经历给了他们一种错觉——编程如此简单,我培训几个月也会嘛!有点像刚学会开车的新司机,很嚣张的对老司机说“开车很简单嘛!你看我也会啊!”。语言和开发工具只是招式,这是外功。而编程思想、经验是内功。这些内功并不是靠短短几个月的培训能够掌握的,这一点有点像中国制造业和日本制造业的区别。动不动赶英超美可欠好。。。
编程并不简单!这是一件很严厉的事情。不过今天,我没有办法先容完全部的方面!或者说,到今天为止,我也并没能掌握全部领域的知识。所以今天我只是分享一些关于编码本身的一些经验。
别的,本文重要分享怎样写代码,并不是怎样用Java写代码。所以文章中各种语言都有可能出现。
编码风格

先来一个圈内的段子。
大部分程序员在工作中都很讨厌这四件事情:

  • 写解释
  • 文档
  • 别人不写解释
  • 别人不写文档
o(∩_∩)o 哈哈。。中枪了没!
这个段子其实反映出来一个题目,即大部分代码都需要通过大量解释和文档来阐明才能将意图转达给维护这些代码的程序员!然而,就像上面的段子说的那样,写大量的解释和文档其实是一件很麻烦的事情。所以许多时候,由于嫌麻烦,解释和文档就没写,导致维护代码的人相当的痛楚。这个苦同学们肯定都是体会过的!相当于给你个精密仪器要你维护还不给阐明书。
其实,冲破上面那个段子描述的那个怪圈的一个很有效的本领就是同一编码风格。优秀的代码可以实当代码即解释,代码本身就可以非常清楚的表现出它的意图来,让别人可以很容易读懂。这就是所谓的可读性!
定名

计算机科学领域中最难的两件事是定名和缓存失效!定名并不简单,很复杂。好的名字可以见名知意,非常容易理解。之所以说定名难是因为定名的过程同时也是概念提取的过程!对题目建立模型,需要提取概念并赋予其“术语”。这个过程其实是“万里长征”中最难的一步。毕竟,设计也好,架构也罢,都有成熟的套路可以参考。唯独这个过程,是需要程序设计者自己进行充分的思考的创造性工作!
以下是总结出来的一些定名经验:

  • 一个类是某物、某事、或人的抽象,是数据与行为的集合体。这恰好符合名词的定义,因此 类名 是一个名词!
  • 方法名 或者说 函数名 是某操作或者某过程的抽象,是一个动作。这恰好符合动词的定义,因此函数名通常是一个动词。
  • 变量名宁可长一些阐明清楚用途也不要用a、b、c之类的偶然义的名称,除非是循环计数器中用i、j、k等约定俗成的一些变量名。好比pageIndex和pageSize就要比取名成i和s好!取成这种和用肴杂器肴杂过后的代码一样的名称没有什么好处,如果算法比较复杂的话,过一段时间恐怕自己都会看不懂。
  • 变量名最好包罗变量本身的业务含义。好比List studentList = new ArrayList();就比List list = new ArrayList();好许多。如果同一段代码里再出现一个List的话,这样就可以很方便的取名为teacherList或者teachers而不是list1和list2这样的毫偶然义的名称!
英文欠好怎么办

这个题目怎么说呢。。
作为一名程序员吧,基础的英文照旧要懂的。要否则发展也容易遇到天花板,学欠好编程的。毕竟,最新的技能、解决方案、工具都是从国外传过来的。如果是解决一些基础性的题目,每天只做做CRUD,好像英文确实不怎么用得上。但是一旦遇到一些实质性题目,恐怕只能到英文网站上找喽!ㄟ(▔ ,▔)ㄏ 不要跟我说你编程可以不需要Stack Overflowcopying and pasting from stackoverflow 可是终极编程大法!o(∩_∩)o 这句话可是编程的真谛啊!(如果你看不懂这个梗那你有可能是伪程序员)
其实,话说回来,着实不方便用英文的时候,我以为也可以用拼音定名。这个题目上可以务实一点,量力而行。但是,拼音和英语混用的做法就不太好了。最好别这样!逼格不高。
解释

怎么添加代码解释

关于解释,我们需要解决的第一个题目是怎样添加代码解释。
对于Java、C#之类的语言,有专用的文档解释语法,很好处理。对于C/C++,则按约定的格式阐明一下类和函数、代码片段的作用和意图即可,至少编译器会进行静态检查。在Python中,有更牛逼的文档字符串这样的语言级特性支持,看解释用help()很方便。不过对于Lua这样的弱范例解释型语言,解释就比较难处理了。这里以Lua为例给出一种解释的解决方案。
借用Java语言文档解释的风格。
文件解释,或者说类/模块解释。
  1. --[[    Object-oriented helper functions for Lua    @author: Elvin Zeng    @date: 2017-8-21--]]
复制代码
函数解释
  1. --[[    create a class with specified super class.    if number of parameters is zero, derived class will extends from {}.    @param superClass super class of target class    @return derived class--]]local function createClass(superClass)    local derivedClass = {}    --  省略一堆代码    return derivedClassend
复制代码
  1. --[[    register a new account    @param user      {        username = "username",        password = "password"      }    @return registered user--]]local function register(user)end
复制代码
tips: Lua中可以通过metatable机制实现类和继承,这一点与javascript通过原型机制来实现类和继承有点类似。
解释里该写些什么

我们起首来看个反例。
  1. /** * 查询 */public List queryPage(int pageSize, int pageIndex) throws PageIndexOutOfBoundsException {    //  定义一个整型变量    int offset;    //  省略一堆代码}
复制代码
起首这个方法名本身就取得欠好,这个暂且不说,先说解释题目。这里的解释犯了几个错:

  • 方法解释为“查询”,这简直就是废话!方法名已经告诉别人这是查询方法了,还在这个解释里写这两个字有什么意义呢?而且到底查询些什么这里也没说!
  • 参数没有解释。没有描述每一个参数的意义以及取值范围等!
  • 什么情况下会抛出PageIndexOutOfBoundsException没有描述清楚。
  • “定义一个整型变量”这种垃圾解释就不要写了,这么简单的语句谁看不懂啊!如果要解释,也是写上这个变量的含义。
这里我们先不考虑设计题目(分页索引号最好做成可以自己调解成合理值),下面再来看改善解释之后的代码。
  1. /** * 列出指定分页的文章 * @param pageSize 分页大小。如果等于0则表示查询出全部文章。 * @param pageIndex 分页索引号。必须为一个大于0的整数,第一页索引为1。 * @return 指定分页的文章列表 * @throws PageIndexOutOfBoundsException 当分页索引号超出正常范围时抛出,即pageIndex小于0或大于最大页索引时。 */public List listArticle(int pageSize, int pageIndex) throws PageIndexOutOfBoundsException{    //  第一条文章记录在MySql数据库中的偏移量    int offset;    //  省略一堆代码}
复制代码
改完之后的解释有没有感觉信息更全许多!固然说代码本身就是最好的解释,但是须要的解释照旧得写上去,毕竟调用的时候别人没法推测你的索引号到底从0照旧从1开始。别的,如果函数内算法比较复杂,可以在代码块内解释,也可以在函数解释上直接写清楚这个函数内部的大概算法/逻辑。代码写出来就是给别人调用的,如果没有基本的解释信息,那么每次调用你的代码的时候,都得去看一下你的函数内详细逻辑才能知道怎么调用。这显然是非常低效的!
定名与解释这两个基本方面没做好的话,会影响到整个团队的运作。也就是说,你封装的东西并没有给队友节流什么时间,别人用到你的代码的时候,又需要花上一些时间去读你的代码。如果团队里每个人都这样,那整个团队都会极其低效。我个人是非常不愿意与这种代码风格恶劣的人相助的。
参考规范

关于编码风格的题目,本文只说定名和解释这两个方面。关于缩进、空格、断行、空行等其他方面的题目,可以参考本节给出的参考规范。
不同的企业会有不同的编码规范,所以这里没有办法给出一个符合全部公司的规范。不过订定自己团队的规范的时候,可以参考一些大企业的做法。以下是天下上最大的互联网公司谷歌的编码规范,同学们可以参考这个。
异常处理

异常与返回值有什么不同

在C语言中,我们的函数通常会返回一个整型值作为状态码用于通知客户端调用的效果。好比0表示成功,非0表示失败。而且可以通过不同的数值来表示不同原因导致的失败。然而在Java、C#、C++一类面向对象语言中,一般不会用返回值来表示状态。返回值一般用于表示返回的业务值,而异常用于通知客户端程序运行状态改变了。
什么时候需要抛出异常

关于这个题目,我想到了一句极其精粹的话:当函数无法完成宣称的任务的时候抛出异常!
好比上面的那个日子,当listArticle方法由于种种原因无法查询出文章列表的时候,则抛出异常。
抛出异常在这种场景下是非常有须要的,因为这样其他人调用你的代码时可以非常放心的去调用,只要调用了你的方法,就会返回文章列表。如果无法返回文章列表,则会抛出异常。完全不用在调用这个函数的时候去怀疑是否实行成功了。
再来一句至理名言:
宁愿终止程序也不要带错运行下去。
也就是说,遇到错误的时候,宁愿抛出异常终止程序,也不要带着错运行下去。这是在掩耳盗铃!
异常需要携带什么信息

起首,异常的范例本身会带有异常种类信息。其次,异常的message属性可以带上更详细一些的信息。这里需要注意,万万不要像下面这么做。
  1. throw new PageIndexOutOfBoundsException("失败!");
复制代码
抛出异常了肯定是实行失败了呀!带上这种信息有什么用,不是带了一句废话嘛!
应该是下面这样
  1. throw new PageIndexOutOfBoundsException("参数分页索引号pageIndex不能大于分页总数");
复制代码
此外,异常堆栈也会携带许多信息。
日志

谈到日志,起首要搞清楚一个题目,日志是干嘛用的?
用来记录运行时的错误信息啊!
是啊。好像各人都知道日志是干什么用的,但是为什么写起代码来就会忘记初衷呢!
来看看代码:
  1. /** * 异步发送通知邮件。 * @param templateFile 邮件模板文件路径,相对于classpath。 * @param modelMap 模型对象 */public void sendEmail(String templateFile, Map modelMap){    //  这里省略一些代码    System.out.println("1");    //  这里省略一些代码    System.out.println("2");    //  这里省略一些代码}
复制代码
这里的代码是什么意思呢?程序员们应该都能明白的!很显然,这位程序员是想借助这些标记来调试,想知道代码到底实行到哪一行了。但是,这里很明显地犯了两个错。

  • 为什么是System.out.println("");而不是logger.debug("");?
  • 为什么是1、2而不是一些更明确的文字信息呢?
在这里,合理的方式是下面这样。
  1. /** * 异步发送通知邮件。 * @param templateFile 邮件模板文件路径,相对于classpath。 * @param modelMap 模型对象 * @throws ServiceException 当邮件模板文件不存在或者modelMap中缺少必须的字段。 */ public void sendEmail(String templateFile, Map modelMap) throws ServiceException{     //  这里省略一些代码     if (isTemplateExists){         logger.debug("模板文件存在");         //  这里省略一些代码         logger.debug("邮件发送任务成功入队。任务id:" + taskId);         //  这里省略一些代码     }else{         logger.error("指定的模板文件[" + templateFile + "]不存在,邮件发送失败。");         //  抛出异常     } }
复制代码
我想给正在犯上面的错的同学提个醒:

  • 使用日志框架,并用合适的级别输出日志非常重要。
    好多程序员从来不负责也不参与运维相关的工作,甚至是做了好几年的Web都从来没有自己发布过网站。所以压根没有后期维护的意识!
    如果没有这些日志,当项目上线之后,运维的背锅侠兄弟发现网站挂了之后只能直接重启,然后当作什么也没看到。因为没有排错的线索。
  • 输出有效信息。
    不要去输出一些像1、2、3、成功、失败、hello这样的毫偶然义的日志,要输出logger.debug("邮件发送任务成功入队。任务id:" + taskId);这样的有效信息。
    也许当时你调试的时候,在你看来这些希奇的字符串是有意义的,但是在其他人看来,这些就是天书。运维的背锅侠会提刀过来砍你的!别的像"-------开始实行--------"这种对运行期间定位题目没有半点好处的日志就不要输出了!自己用可以,提交代码前肯定要删掉。
  • 日志中带上上下文信息。
    孤立的一句错误日志通常没有什么实际作用。好比上面的例子中,如果在找不到指定的模板文件的时候未将发送邮件时指定的模板文件名输出,那么排错的时候无法知道到底是少了哪个模板文件。
  • 不要在日志中输出用户的敏感信息。
    万万不要在日志中输出像用户密码、邮件内容之类的涉及用户隐私的敏感信息,也不要去输出像验证码的值之类的敏感信息。
参数校验

在你对外公开的方法前先插入一些检查参数的代码,以确保方法被“精确的姿势”调用。好比:
  1. /** * 列出指定分页的文章 * @param pageSize 分页大小。如果等于0则表示查询出全部文章。 * @param pageIndex 分页索引号。必须为一个大于0的整数,第一页索引为1。 * @return 指定分页的文章列表 * @throws PageIndexOutOfBoundsException 当分页索引号超出正常范围时抛出,即pageIndex小于0或大于最大页索引时。 */public List listArticle(int pageSize, int pageIndex) throws PageIndexOutOfBoundsException{    if (pageSize < 0){        throw new IllegalArgumentException("pageSize不能小于0");    }    if (pageIndex < 1){        throw new IllegalArgumentException("pageIndex不能小于1");    }    //  第一条文章记录在MySql数据库中的偏移量    int offset;    throw new PageIndexOutOfBoundsException("");    //  省略一堆代码}
复制代码
参数校验的作用

如果在对外公开的重要方法开始的位置不插入校验参数的代码,有时恐怕方法需要运行到方法内部比较深的位置才会抛出一个异常来。而且那种情况下,抛出的异常可能就会有各种各样的了。好比空指针、除零异常等。
这种情况下,很难一眼看出引发这个异常的根源是参数传错了。需要对你的代码进行一番调试才行!如果一开始就在代码的入口插入了校验参数的代码,那么调用的时候,一眼就能看出来是参数传错了导致了一个异常。这样其他程序员看到这个异常之后就会去看一下你的方法解释。他一看,哦!原来分页索引号是从1开始计数的,那么这个题目就会就此打住,给团队节流了时间。
参数校验题目是会影响团队运行效率的一个很关键的因素。所以,请同学们器重起这个题目来。我们都是工程师,团队作战的,自己写代码快不叫快,整个团队快起来才叫真的快!用好断言,可以让你的代码更坚固。
tips: Java中默认断言是不开启的,所以建议无视Java语言的断言,自己处理。
什么时候需要进行参数校验

我以为一个方法或者函数在满足以下条件时有须要进行参数校验:

  • 方法或者函数是对外公开的,不是私有的。
  • 参数有可能为空指针的时候。
  • 参数的合理值无法通过方法名、参数名、参数范例一眼看出来的时候!好比上面那个pageIndex是从1开始计数的,但别人并不知道你是从1开始计数的。
如果对每一方法都进行校验的话,其实挺麻烦的。程序员的时间是很名贵的,没这么多闲工夫。不过在满足上面条件的情况下,最好照旧校验一下。因为做了这个校验,你自己是会轻微浪费几分钟的时间,不过从团队整体来看,总的调试损耗的时间却降下来了。要记着方法/函数写出来就是给别人调用的!
参数校验需要做到什么水平

我有一个尺度,就是把自己当成调用这些代码的那个人,把自己想象成有可能以任何“姿势”调用的菜鸟(实际上也有可能是不相识你的代码的大牛)。如果这个时候自己也有可能会犯某些错(好比没注意边界值,没注意是否可空),那么这个时候是必须要做校验的。对于一些已经在其他层做过处理不太可能有错误的值的情况,可以不做校验。好比你的UserService中有一个署名为public void register(User user)的方法,用于注册一个用户。这种情况下,可以只校验一下user参数是否为空,而不用对user的username、password属性进行校验(用户名密码长度是否合法等)。因为你在上一层控制器层模型绑定的时候已经做过非常严谨的校验了。当然,这里如果你有充足的时间,也可以校验一下。详细做到什么水平,还需要你根据情况去自己把握。
跋文

编码规范就是用来束缚别人的!
o(∩_∩)o 哈哈!开打趣的啦!
其实许多时候,出于各种原因,如“项目周期紧”、“项目还在探索阶段可行性未知,先实现了再说”、“项目中其他代码已经这样了,破罐子破摔”等,最终导致的效果可能就是我们这些自称“有经验”的程序员自己也不愿定能写出完全符合这些理念的代码来。大概是吧!
ㄟ(▔ ,▔)ㄏ
我承认,我也写过奇葩代码。
但是,这好像并不是你这个作为将来优秀程序员的人不思进取的来由。
小时候,老师教我们要老实,但是老师自己也不见得能完全做到。我们可以因为这个鄙视他。
长大后,体验过了生活中会有许多的无奈,不再鄙视“不老实”的老师。甚至低下了高贵的头,自己也变得那般模样。
将来,你还会教诲你的后代“要老实”吗?
恐怕会!
因为,优秀的理念,不管结局怎样,都应该去提倡!
本文的观点仅代表现在的我,人是会成长的,来日诰日的我大概又会有新的见解! 如果你不认同部分观点或者还有其他的优秀理念,可以给我留言。我们一起成长!
作者:Bug辉
本文是我的原创文章。独立博客链接:https://www.bughui.com/2017/08/21/how-to-write-code/
版权声明:《怎样写代码 —— 编程内功心法》 由 Bug辉 接纳 知识共享 署名-非商业性使用-禁止演绎 4.0 国际 许可协议 进行许可。转载或引用文章时请注明原作者并带上原文链接。

相关技术服务需求,请联系管理员和客服QQ:2753533861或QQ:619920289
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

帖子推荐:
客服咨询

QQ:2753533861

服务时间 9:00-22:00

快速回复 返回顶部 返回列表