如何编写优雅的代码

时间:2019-01-11 11:53:22   |    软件故障   |   

我对优雅的代码的理解是:遵守规范,逻辑清晰,严谨可靠,化繁为简。本文分享我对写代码的一些思考。本篇文章大概有4000字,看完它我觉得要13分钟。

代码首先是写给人看的,其次才是计算机顺便能够运行

在讨论关于如何编写优雅代码的观点之前,先抛出个问题,希望我们对这一点能够达成共识:

为什么要编写优雅的代码?

有的人说,代码写得好不好无关紧要,能完成功能,并且不出什么bug就好了。
有的人说,项目进度那么赶,bug都改不完,哪有时间写好代码?
有的人说,老板那么抠(或公司怎样,产品设计多么渣),还指望我好好写代码?
有的人说,上一个人留下的代码就那么烂,我能怎么办?我也好无奈啊。
有的人说,代码写得再好又有什么用?公司又不给加薪升职。
还有其他各种理由,等等。

那么,这足够成为不写优良代码的充分理由吗?我不这么看。

代码首先是写给人看的,其次才是计算机顺便能够运行。 能完成功能,也就是完成了开发任务,固然是应该的。可是,我们的项目是持续迭代的,以前写的代码以后还要去看和去改,如果每次都只有完成功能的最低要求,日积月累,这个项目所能达到的质量也只会是最低要求,并且这个最低要求还会进一步降低。而且,你所编写的代码,在你维护这个项目的期间,你是面对着它最长时间的人。写得好,你看起来会舒服心情好,写得烂,恶心也只恶心到你自己。

写好代码更能省时间。 其实比起写低质量的代码,写出优雅的代码更能节省时间。优雅的代码是逻辑清晰的,简单直观的,所以首先在开发或维护的时候,读逻辑清晰的代码,自然要比读逻辑混淆的代码要更容易,由此就可以把更多的精力与时间花在功能开发上,而不是理清以前逻辑。其次,编写代码时,思维清晰,就可以写出更严谨的代码,这样就能减少bug,也就减少了修复bug所花费的时间。我坚持地认为并且努力地去做到,不应该把时间都耗费在代码的修复上,而应该更多地用于创造性的工作。而编写优雅的代码,正是达成这一目标的有效方法。

做一个有所追求的程序员。 我希望,你是一个有所追求的程序员,而不是一个得过且过的码农。你的代码质量,应该取决于你自己,而不是你的公司,你的老板,产品经理,设计人员,或是项目以前的负责人员。有追求的你,不应该让他们成为你降低自己要求的理由。你对自己有所追求,对代码也应当有所追求。

写出优雅的代码很困难吗?
不困难。只需要有所遵守,有所思考。

必须遵守的代码规范是整洁代码的必要条件

优雅的代码,首先是让人看起来就是很整洁的。而这种整洁,则来源于代码规范。严格地遵守代码规范,是提高且保证代码质量的最有效方法。从多人协作的角度上讲,统一的代码规范能够有效减少沟通的阻碍。而从个人开发的方面上看,一份良好的代码规范,能够为如何把代码写得整洁起到指导帮助作用。
比如以下代码,是我在公司项目找到的以前版本的代码:

    private RecyclerView rv_choose_parking;
   private CommonAdapter<Park> adapter;
   
   private ChooseParkingPresenter mChooseParkingPresenter;
   private ListData<Park> mListDataJSONResponse;
   private PtrFrameLayout mPullFragLayout;
   
   private String mParkName;

几个变量在命名上就用了三种不同的风格,到了调用的时候,就会更混乱了。
我按代码规范调整一下代码,如下:


    private PtrFrameLayout mPullFragLayout;
   private RecyclerView mRecyclerView;
   
private CommonAdapter<Park> mAdapter;

   
private ListData<Park> mParkList;
   
private String mParkName;

   
private ChooseParkingPresenter mChooseParkingPresenter;

可以对比一下阅读这两者的直接感受。

规范的代码能够带来更好的可读性,减少干扰,从而把更多精力花在代码修改而不是代码阅读理解上,所以代码规范对于我们代码编写的重要性是毋庸置疑的。
如果你所在的公司已经有经受考验的代码规范,请遵守。如果没有,请推动制定。Java代码可以参考Oracle的Java代码规范,Kotlin代码可以参考Kotlin官方的代码规范。Android规范如果没有找到合适的标准,可以参照Android源码的风格去制定。制定一份代码规范的原则应该至少是,它是通用的准则,它能够帮助编写可读性好的代码,它把那些约定俗成的习惯取其精华归纳为统一的要求或指导。

有逻辑地组织及编写自解释的代码

代码是逻辑的产物。 我们在写代码时,应该注意所定义的变量、方法、类的关系,有逻辑地组织它们。
比如说在Android中声明变量的时候,通常应当把所有的View的声明放在一起,表示数据的声明放在一起,并且不同种类的声明之间应当留有空行。适当的空行可以让人在阅读的时候,形成意识上的区分和类比,从而提升阅读效率。
如果是方法的声明,应该把相关联的方法放在一起,而不是按名字排序。比如定义了methodA()methodB()methodC(),其中methodA()调用了methodC(),那就应该把methodC()放在methodA()之下,而把methodB()放在这两个方法的上面或下面而不是中间。
再比如在定义类时,相关联的类应互相靠近。同一个包里的类应该是共同完成某个模块或功能的,即包内的代码应是高内聚的。
在方法的内部,根据具体的业务用空行隔开不同的逻辑,不但使代码显示有段落感,也有助于代码的整体理解。

编写能够自解释的代码是我们对自己应该有的要求。 不要总想着用注释来解决看不懂代码的问题。如果你的代码不易理解,那就应该去改进它,让它能够“自解释”,而不是依赖于大段的注释。
让代码做到自解释,常见的就是从命名、类型下手。
比如有一个变量:

    private boolean flag = false;

这让人看了会很费解。因为这个flag,从字面意识来讲它就是一个标记,而它的类型为布尔值,那么它标记的是什么?它为true时表示什么?为false时又表示什么?这些都不得而知。也就是flag这个名字并不能给我们带来有用的信息。可能读了代码才发现,原来它表示的是当前图标是否正在被拖动。那这样的话,其实把它改名为isDragging,显然更好。
特别强调一下,参数名也要重要,不要以为是参数名就可以随便取个abcpp0什么的。

除了变量名之外,方法名也很重要。方法的名称应该要正确描述方法里所做的事情,比如一个方法,里面是做的是提交表单信息,那就应该叫submit()而不是叫request()。如果你觉得方法的行为比较多,方法名不好取,那就应该重构它,抽取成多个职责独立的方法。

我对命名的要求依次是:正确、准确、直观、简洁。
正确是指,变量名能够表达它的属性,方法名能够表述它的行为,类名能够表明它的职责,包名能够表现它的功能。
在做到正确的前提下,再去追求准确。准确是指它的名字是正确并精确的,比如用手机号是否为空去判断一个用户是否绑定手机号,其实我们想要的是判断是否绑定手机号,而不是手机号是否为空,那方法名isPhoneBound()就要比isPhoneNotEmpty()要贴切些,尽管它里面的实现是判断手机号是否为空。
直观是要排在简洁前面的,名字长点没关系,只要直观就可以。在同样直观的前提下,如果有更简洁的命名再选用更简洁的命名。另外,直观不代表名字就一定会长,而是让我们能更直截了当地看明白代码。比如下面的代码:

    try {
       if (os != null) {            os.close()        }    } catch (Exception ignore) {    }

人异常的名字ignore我们就知道,这个异常是要忽略的,所以也不必再在catch块里添加//ignore的注释。

另外,命名可以使用缩写,但应该是大家约定俗成的缩写。比如message的缩写msg是可以接受的。做为for循环的下标,index简写成i同样也是可以的。但不应该notification缩写为notinotif,把common缩写为comm,把model缩写为mod,这种缩写简直不可理喻。
提到约定俗成,我还想到我常见到的一类错误:

    public void getUsername(String username) {
       this.username = username;    }
   
   public String setPhone() {
       return phone;    }

这完全颠倒了settergetter的用意。getter方法是返回某个值,它应该是有返回值的,通常参数为空,而且getter方法不应该改变被调用对象的内容。而setter方法是指设置值进去,而不是获取值。
还有isXXX()就应该是人畜无害的判断方法,它应该是返回一个布尔类型的值用于条件判断,如果方法返回类型为空并且会产生一些后果比如抛个异常什么的,那可以叫做checkXXX()toXXX()是转化,解析是parseXXX()manager是管理员,管理的名词是managementlength是长度,大小是sizenumber是数字,数量是count

另外要提一下的是类型。比如表示是或否的,就应该用boolean类型而不是int类型及是否为1或0来判断;表示几个有限的状态的,用枚举值显然会比用整型值更好,既有助于代码理解及调用又限定了可能的取值而减少出错。Android程序员不要觉得枚举值占内存就不用,事实上,一个枚举值相对于int值所增加的内存占用,你觉得会有多少?1K?10K?100K?真不用那么在意,比起那些粗心的代码,图片的滥用,它所增加的内存可以说是不值一提。

总而言之,我们应该尽力去编写能够自解释的代码。如果代码做不到这一点,那么就应该反复推敲,持续改进它。

奥卡姆剃刀定律:如无必要,勿增实体

其实剃刀原理和墨菲定律才是我最初想重点表达的内容,这两个可以说也是代码的哲学。我特别喜欢剃刀原理的这句“如无必要,勿增实体”,极适合用在代码编写上。
如果你想着:我增加一个checkedCode,因为每次都要调用checkedPark.code来获取很不方便。那么想想这句话,另外别忘了增加这个变量就意味着你要保证其他地方在更新checkedPark时也要同时更新checkedCode如无必要,勿增实体,我们应该避免冗余代码。
如果你想着:我要为这个x设计增加一个xx类,以后如果要增加xxx的需求的话,就可以使用这个xx类。那么想一下这句话,想想你为了一个还不存在的xxx的需求而增加的一个xx类所带来的复杂性。如无必要,勿增实体,不要过度设计。
不是json里的每一个字段,我们都要在实体类里声明对应的属性。你所需要声明的,只有你在代码中所需要的属性,这不仅对反序列化效率有微不足道的提升,更是为了代码的简洁与清晰。如无必要,勿增实体,小即是美。

我们的代码应该尽量地追求简单和简洁,而不是繁复和炫技。

墨菲定律:会出错的事总会出错

代码是逻辑的产物,所以,写代码时要注意严谨。严谨的代码,才能保证它的可靠性,减少bug。墨菲定律中有一条,凡是你认为可能会出错的,它一定会出错。这条定律在软件开发中非常常见,我也经历过几次。
举个例子,在支付记录列表里要显示支付方式的图标,本来支持的支付方式只有微信、支付宝、银联、钱包,你在显示时代码如下:

    when (type) {
        WECHAT -> textView.setText(R.string.wechat)
        ALIPAY -> textView.setText(R.string.alipay)
        UNION_PAY -> textView.setText(R.string.union_pay)
        WALLET -> textView.setText(R.string.wallet)
    }

看起来似乎没问题,但如果某天,产品说要增加一个公交卡支付。这下不好了,没有升级的用户,在列表里面会看到原本应该显示公交卡的支付方式,在上下滚动时,有时显示微信,有时显示支付宝。所以说,程序员生存法则有很重要的一条:不要相信产品经理,不要相信产品经理,不要相信产品经理。但更重要的是,你代码上就要更严谨。想一下,如果有其他类型怎么办?添加一个else的分支,就不会有这个问题了。

再举一个例子,比如一个值,是通过多种不同的条件来决定的,则应考虑把value声明为final类型。这样有什么好处呢?首先,它能够避免在一些条件里漏掉赋值,没有赋值的话编译器会报错。其次,它能够避免在复杂的if块里重复赋值,因为实际上可能if块里不会只有赋值一个操作,还有其他操作和比较复杂的逻辑。第三,它使得你所有的if及else分支能够覆盖所有条件,避免没有考虑到的情况。
我们在方法里只会赋值一次的变量,也应该声明为final,这样在用的时候就会很放心,清楚它的值不会被修改。
我们的成员变量,如果不需要被修改,也应该声明为final,从根源上确保它的不可变。
我们的参数值,比如需要传的值在某个范围内的,就应该用@IntRange注解声明它的范围。
我们的参数值,通常不应该在方法里被重新赋值,也就是参数也应该是final的。这一点kotlin就做得很好,在kotlin中函数的参数是不可再被赋值的。

我这里就不多举例了。写代码时要注意必须保证代码的严谨性,如果不够严谨,那么总有一天这个bug会被触发。墨菲定律,在软件开发上总是奏效。



关注并后台回复“qq群”,获取Android高级开发群群号。