如何写出高质量代码

如果说学习算法与数据结构是为了教你写出高效的代码,那么学习设计模式就是为了教你学出高质量的代码。

接下来我们来了解两个问题:1、烂代码有哪些特征?2、高质量代码有哪些特征?

烂代码有哪些特征?

命名不规范,代码结构混乱,高度耦合等。这样到代码维护起来牵一发而动全身,无从下手,恨不得全部删了重写。

 

 一、高质量代码有哪些特征?

我觉得应该有7个特征,分别是:可维护性,可读性,可扩展性,灵活性,简洁性,可复用性和可测试性,接下来容我一一介绍。

可维护性

我们先来了解“维护代码”到底包含哪些具体的工作内容?代码维护指的是修改bug,修改老的功能代码,添加新的功能代码之类的工作,由此我们可以得出“可维护性”指的是:bug容易修复,修改和添加功能代码能够轻松完成

可读性

无论修改bug,还是修改,添加功能代码。首先要做的事情是读代码,所以可读性很重要。

如何评价一段代码可读性呢?主要有以下6点:代码是否符合编码规范;命名是否达意;注释是否详尽;函数长度是否合适;模块划分是否清晰等。

可扩展性

代码的可扩展性指的是我们添加性功能时,尽量不修改或者少改原有的代码的情况下,直接在扩展点插入新的功能代码。

灵活性

当我们要实现一个新功能时,发现现有的代码已经抽象出了很多底层可以复用的模块,类代码,我们可以直接拿来用,说明代码写得很灵活。

简洁性

代码简洁性,包含代码简单易懂,逻辑清晰,意味着易度,已维护。

可复用性

顾名思义指的是尽量复用已有的代码,减少重复代码的编写,比如面向对象的继承,多态特性。

可测试性

代码的可测试性差,说明比较难写单元测试,那几本说明代码设计的有问题。

要想写出满足以上7个高质量代码的指标,你需要掌握一些优秀的编程方法,包括编码规范,面向对象设计思想,设计原则,设计模式,持续重构等。

1编码规范能让我们写出可读性好的代码;

2面向对象设计思想中继承和多态能让我们写出可复用性的代码;

3设计原则中单一职责,DRY基于接口而非实现,里式替换原则,可以让我们写出可复用性,灵活,易扩展,易维护的代码;

4设计模式可以让我们写出易扩展的代码;

5持续重构可以时刻保持代码的可维护性。

二、面向对象

什么是面向对象编程?

面向对象编程是一种编程风格。它以类或对象作为组织代码的基本单元,并将封装,抽象,继承,多态四个特性,作为代码设计的实现基石。

什么是面向对象分析和面向对象设计?

简单来说,面向对象分析就是要搞清楚做什么,面向对象设计就是搞清楚怎么做。两个阶段最终的产出是类的设计,包括程序被拆解为哪些类?每个类有哪些属性方法,类与类之间如何交互等等。

如何理解面向对象编程四大特性的作用?

四大特性指的是封装,抽象,继承,多态,接下来我们来一一了解。

封装

封装也叫着信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权函数来访问内部信息或数据。好比洗衣机的内部结构我们不需要了解,通过类封装成几个函数(开,停,关,调节水量,调节时长),每个开关调用对应函数即可。它需要编程语言提供权限访问控制语法来支持,比如JAVA中的private,protected,public关键字。

封装特性存在的意义:一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。

抽象

抽象主要是如何隐藏方法的具体实现,让使用者只关心方法提供了哪些功能,不需要知道这些功能是如何实现的。抽象通过接口类和抽象类来实现,比如JAVA的interface和abstract关键字语法,但也并不需要特殊的语法机制来支持。

抽象存在的意义:一方面是提高代码的可扩展性,维护性,修改实现不需要修改定义,减少代码的改动范围;另一方面,它也是处理复杂系统的有效手段,能有效地过滤掉不必要关注的信息。

继承

继承是用来表示类之间的is-a关系,分为两种模式:单继承和多继承,单继承表示一个子类只能继承一个父类,多继承表示一个子类可以继承多个父类。为了实现这个特性,编程语言需要提供特殊的语法机制来支持。继承主要是用来解决代码复用的问题。

但是过渡使用继承,继承层次过深过复杂,就会导致代码可读性,可维护性差,所以应该“多用组合少用继承”。

多态

多态指子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性需要编程语言提供特殊的语法机制来实现,比如继承,接口类,duck-typing。多态可以提高代码的扩展性和复用性,是很多设计模式,设计原则,编程技巧的代码实现基础。

举例1:iterator是一个接口类,定义了可以遍历集合数据的迭代器。Array和LinkedList都实现了接口类iterator,我们通过传递不同的实现类(Array,LinkedList)到iterator的print函数中,可以打印出不同的中。

举例2:duck-typing实现多态的方式非常灵活。比如Python语言,只要两个类具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系。

面向对象比面向过程编程有什么优势?

首先我们来了解面向过程编程是什么?面向过程编程也是一种编程风格,它以过程(可以理解为方法,函数,操作)作为组织代码的基本单元,以数据(可以理解为成员变量,属性)与方法相分离为主要特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。

面向对象编程比面向过程编程的优势主要有三个:

优势1:oop更加能够应对大规模复杂程序的开发开发,能够清晰的,模块化的组织代码。比如我们开发一个电商交易系统,业务逻辑复杂,代码量很大,可能要定义好数百个函数,数百个数据结构,那如何分门别类这些函数和数据结构,才能不至于看起来比较凌乱呢?类就是一种非常好的方式。

优势2:oop风格的代码易复用,易扩展,易维护。因为oop具有更加丰富的特性(封装,抽象,继承,多态),利用这些特性编写出来的代码,更加易扩展,易复用,易维护。

优势3:oop语言更加人性化,更加高级,更加智能,从编程语言跟机器打交道的方式的演进规律中可以总结出。

日常工作中违反面向对象编程风格的典型代码设计有哪些?

1、滥用getter,setter方法;

在设计实现类的时候,除非真的需要,否则不要给属性定义setter方法。此外尽管getter方法相对setter方法要安全些,但是如果返回结果的是集合容器,那也要防范集合内部数据被修改的风险。

2、constants类,utils类的设计问题;

对于这两种类,我们尽量做到职责单一,定义一些细化的小类。此外,如果能够将这些类中的属性和方法,划分归并到其他业务中,能极大提高类的高内聚和代码的可复用。

什么情况下需要定义接口?

我们先来了解接口的本质,“接口”本质就是一组“协议”或者约定,是功能提供者提供给使用这的一个“功能列表”,那么这个“功能列表”就需要一个度,怎么来控制这个度呢?

“基于接口而非实现编程”这条原则的设计初衷是,将接口和实现分离,封装不稳定的实现,暴露稳定的接口。稳定意味着抽象,所以在接口定义的时候,一方面命名要足够通用,不能包含具体实现相关的字眼;另一方面与特定实现有关的方法不要定义在接口中

比如你要实现一个图片上传,下载功能,你需要封装上传和下载接口,而每个图片服务器类继承这个接口。

如何决定使用组合还是继承?

首先了解什么是组合?组合是通过implements引入一个或者多个类,然后通过new对象作为子类的属性,从而达到组合的目的。

什么情况下不推荐使用继承?

继承层次过深(超过2层),过复杂,会影响代码的可维护性,这种情况下少用。

组合比继承有哪些优势?

继承的主要作业有三个:表示is-a,支持多态写代码复用。而这三个可以通组合,接口,委托三个技术手段来达成,此外,组合还能解决层次过深,过复杂的继承关系影响代码可维护性问题。

如何判断该用组合还是继承呢?

如果类之间的继承结果稳定,关系不复杂,则用继承;反之则用组合。

 

根据多年来的web开发经验,业务系统都是基于MVC三层架构来开发的,准确的说是一种基于贫血模型的mvc架构开发模式,虽然它成为标准的web项目开发模式,但它违反了面向对象编程风格,特别是领域驱动(DDD)模式盛行之后。接下来我们分析如何取舍。

什么是基于贫血模型的传统mvc模式?

现在很多web或者app项目都是前后端分离,后端负责暴露接口给前端调用,我们一般将后端项目分为三层Repository层,Service层,Controller层,其中Repository层负责数据访问,Sercive层负责业务逻辑,Controller层负责暴露接口。之所以称之为贫血模型,是因为Repository,Sercive,Controller的数据实体类只包含数据,不包含业务逻辑类,这种数据与业务逻辑分离,破坏了面向对象的封装特性,是一种典型的面向过程的编码风格,所以叫作贫血模型。

什么是基于充血模型的ddd开发模式?

和贫血模型相反,数据和对应的业务逻辑被封装到同一个类中,符合面向对象的封装特性,是典型的面向对象编程风格。

什么是领域驱动设计(DDD)?

领域驱动设计主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互,常用与合理地指导微服务拆分。所以微服务加速了领域驱动设计的盛行。

贫血模型和充血模型的主要区别在于Service层。

贫血模型中的Service包含service类和Bo类,Bo是贫血模型,只包含数据,没有具体的业务逻辑,业务逻辑在service类中。

充血模型中service层包含service类和Domain类两部分,Domain相当于贫血模型中的Bo。而Domain既包含数据,也包含业务逻辑。

总结:贫血模型重service轻Bo,比较适合业务比较简单的系统开发,简单到只是基于SQL的CRUD操作;充血模型轻service重Domain,比较适合业务复杂的系统开发,需要复杂的数据处理逻辑

 

面向对象分析的产出是需求描述,面向对象设计的产出是类。如果将需求描述转化为具体的类设计?这里面一共有4个步骤:

1、划分功能点识别出有哪些类

根据需求描述,我们把其中涉及到的功能点,一个个罗列出来,然后再去看看哪些功能点职责相近,操作同样的属性,可否归为同一个类。

2、确定类及其属性和方法

根据划分类的功能点描述中的动词,作为候选到的方法,再进一步过滤筛选出真正的方法;把功能点中涉及到名词,作为候选属性,然后同样进行过滤筛选。

3、将类组装起来实现功能

将相关的类组装起来,实现功能。

 

三、设计原则

前面我们介绍了面向对象相关的知识,接下来我们学习一些经典的设计原则,其中包括SOLID,KISS,YAGNI,DRY,LOD等。其实这些设计原则从字面意思理解并不难。但是“看懂”和“会用”是两回事,而“用好”就难上加难了,接下来和我一起学习吧。

前面我们提到SOLID原则,他是由5个原则的首字母组成,分别是单一职责原则,开闭原则,里氏替换原则,接口隔离原则和依赖反转原则。

单一职责原则(SRP)

指的是一个类或者一个模块只负责完成一个职责(或功能)。也就是说不要设计大而全的类,要设计粒度小,功能单一的类。单一职责原则是为了实现代码高内聚,低耦合,提高代码的复用性,可读性,可维护性。但是拆得过细,反倒会降低内聚性,影响可维护性 。

如何判断类的职责是否单一?

如果类的设计出现下面的情况,这可以判断不符合单一职责:

1、类中代码行数,函数和属性过多;

2、类依赖的其他类过多,或者依赖类的其他类过多;

3、私有方法过多;

4、比较难给类起一个命名;

5、类中的大量方法都是集中操作类中的某几个属性;

 

开闭原则(OCP)

开闭原则指的是软件实体(模块,类,方法等)应该“对扩展开发,对修改关闭”。也就是添加一个新的功能时,应该在已有的代码基础是扩展新的模块,类,方法等,而不是修改已有代码(模块,类,方法等),所以说代码的扩展性是重点。

提高代码的扩展性有哪些方法?

多态,依赖注入,基于接口而非实现编程,以及大部分的设计模式(装饰,策略,模板,职责链,状态等)。

如何在项目中灵活应用开闭原则?

对于比较确定,短期内可能会扩展,或者需求改动对代码影响较大的情况,或者实现成本不高的扩展点,在编码代码的时候,可以事先做些扩展设计。对未来不确定,或者实现起来比较复杂的扩展点,你可以等有需求驱动的时候,再通过代码重构的方式来支持扩展的需求。代码的扩展性有时会跟可读性相冲突,你需要做好权衡

 

里氏替换原则(LSP)

里氏替换原则指的是子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。其核心思想“按照约定来设计”,这里的约定包括:函数生命要实现的功能;对输入,输出,异常的约定;甚至包括注释中所罗列的任何特殊说明

 

接口隔离原则(ISP)

接口隔离原则指的是:客户端不应该被迫依赖它不需要的接口,其中的“客户端”可以理解为接口的调用者或者使用者。“接口”可以理解为下面三种场景:一组API接口集合,单个API接口或函数,OOP中的接口概念。接下来我们一起来解读这三种场景。

一组API接口集合

可以是某个微服务的接口,也可以说某个类库的接口。如果部分接口被部分调用者使用,你可以将这部分接口隔离出来,单独给这部分调用者使用。比如app前后端分离的接口实现中,我们应该将后台管理和APP前台的接口分开定义,应该像删除这里功能一般只有后台管理才有权限,这样可以做到隔离保护,避免前台用户误删用户信息。

单个API接口或函数

部分调用者只需要函数中的部分功能,那你可以将函数拆分成粒度更细的对个函数,让调用者只依赖它需要的那个细粒度函数。函数的设计功能要单一,不要将多个不同的功能逻辑在一个函数实现,这样可以提高代码的可读性和可维护性。

OOP的接口概念。

指的是面向对象编程语言的接口语法,接口设计要单一,不要让接口的实现类也调用者,依赖不需要的接口函数。比如JAVA中的interface,假如你在项目中用了三个外部系统:Redis,MySQL,Kafka。每个系统都对应一系列配置信息,比如地址,端口,访问超时等。为了在内存中存储这些配置信息,供项目中的其他模块来使用,我们分别设计实现了三个Configuretion类:RedisConfig,MysqlConfig,KafkaConfig。这样更加灵活,易扩展,易复用。

接口隔离原则和单一职责原则的区别

单一职责原则是针对模块,类和接口设计。接口隔离原则侧重于接口的设计,它是提供了一种判断接口的职责是否单的标准,即调用者只使用部分接口或者接口功能,那接口的设计就不够单一。

 

依赖反转原则(DIP)

我们先来了解三个概念:控制反转(IOC),依赖注入(DI),依赖注入框架(DIF)。

控制反转(IOC)

“控制”指的是程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用了框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”到了框架。

控制反转是一种比较笼统的设计思想,一般用来指导框架层面的设计,比如模板设计模式,依赖注入方式等。

依赖注入(DI)

是一种编程技巧,不通过new()的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数,构造参数等方式传递(或注入)给类使用,这样提高了代码的扩展性。

依赖注入框架(DIF)

我们通过依赖注入框架提供的扩展点,简单配置所需要的类,以及类与类之间依赖关系,就可以实现由框架来自动创建对象,管理对象是生命周期,依赖注入等原本需要程序员来做的事情。

依赖反转原则(DIP)

高层模块不依赖底层模块,高层模块和底层模块应该通过抽象来相互依赖。除此之外,抽象不依赖具体的细节,具体实现细节依赖抽象。我们拿Tomcat这个Servlet容器为例子:

tomcat是运行java web应用程序的容器。那么tomcat是高层模块,web应用就是底层模块。tomcat与web应用代码直接没有直接的依赖关系,两者都依赖同一个“抽象”,也就是Servlet规范。servlet规格不依赖具体的tomcat容器和web应用的实现细节,而tomcat容器和web应用依赖servlet规范。

 

一、YAGNI设计原则

YAGNI原则的英文全称:You Ain't Gonna Need It。中午意思是:你不会需要它。

为什么说不需要它呢?她的意思的:不要过度设计当前用不到的代码,功能。

二、KISS原则

如果说YAGNI原则讲的是要不要做,那么KISS原则讲的是怎么做。

KISS原则的英文全称:Keep It Simple and Stupid。中午翻译是:简历保持简单。

我们都知道代码的可读性和维护性是衡量代码质量非常重要的两个标准。而KISS原则上保持代码可读性和可维护性的重要手段。

接下来请你思考两个问题:

第一个问题是:行数少就越“简单吗”?

第二个问题是:代码复杂就违背了KISS原则吗?

其实两者都不全对,“简单”并不是以代码行数来衡量。代码行数越少并不代表越简单,我们还要考虑逻辑复杂度,实现难度,代码的可读性等。而且本事就复杂的问题,用复杂的方法解决,并不违背KISS原则。

如何写出满足KISS原则的代码呢?

我这里总结了三条原则:

1、不要使用同事可能不懂的技术来实现;

2、不要重复造轮子,要善于使用已有的工具类库;

3、不要过度优化;

 

什么是DRY原则?
DRY英文全称:Don't Repeat Yourself
中午翻译:不要重复自己,即不要写重复代码。
代码重复有三种情况,分别是:实现逻辑重复,功能语意重复和代码执行重复。

如何识别是否违反DRY原则?
1、实现逻辑重复,但功能语义不重复代码,并不违反DRY原则。
2、实现逻辑不重复,但功能语义重复的代码,也算违反DRY原则。
3、除此之外,代码执行重复也算违反DRY原则。

什么是代码的复用性?
代码的复用性是评判代码质量的一个非常重要的标准。

先来了解三个概念:代码复用性,代码复用和DRY原则。
1、代码复用性表示一段代码可被复用的特性或能力:我们在编写代码的时候,让代码尽可能复用;
2、代码复用表示一种行为:我们在开发新功能的时候,尽量复用已存在的代码;
3、DRY原则是一条原则:不要写重复的代码;

代码的“可复用性”是从代码开发者的角度来讲的,“复用”是从代码使用者的角度来讲的。比如,A同事编写一个UrlUtil类,代码的“可复用性”很好,B同事在开发新功能时,可以直接“复用”。

这三者理解上有所区别,但实际要达到的目的是类似的,都是为了减少代码量,提高代码的可读性,可维护性。除此之外,复用已经测试过的老代码,bug会比从零重新开发要少。

如何提高代码复用性?
我总结了7调情,具体如下:
1、减少代码耦合:避免牵一发而动全身;
2、满足单一职责:越细粒度的代码,通用性越好,越容易被复用;
3、模块化:像搭积木一样,拿来即用;
4、业务与非业务逻辑分离:抽取成一些通用的框架,类库,组件等;
5、通用代码下沉:从分层角度看,越底层的代码越容易;
6、封装,抽象,继承,多态;
7、应用模板等设计模式;

第一次编写代码的时候,先不考虑复用性;
第二次遇到复用场景的时候,再进行重构使其复用。
 

四、重构代码

程序员重构代码的重要性不言而喻,但如何进行有效的重构呢?下面是一些建议和指导。

为什么要重构?
重构是提高代码质量和可维护性的重要手段,旨在在不改变软件可见行为的情况下,使其更易于理解,修改成本更低。

重构什么?
重构的规模可分为大规模重构和小规模重构。大规模重构是对顶层代码设计的重构,包括系统、模块、代码结构、类与类之间的关系等重构,主要手段有分层、模块化、解耦、抽象可复用性组件等。小规模重构则是对代码细节的重构,如规范命名、规范注释、消除超大类或函数、提取重复代码等。

如何重构?
在进行大规模重构前,应制定重构计划,并根据计划逐步完成重构,每个阶段完成一小部分的重构,然后执行单元测试、提交代码,再进行下一阶段的重构,以确保代码一直处于正确、可运行状态。
大规模重构需要有经验、熟悉业务的资深同事来指导。而小规模的重构,因为影响范围小、耗时短,可以随时进行。

如何保证重构代码不出错?
为了保证重构代码不出错,需要熟练掌握各种设计原则、思想、模式,并对重构的业务和代码有足够的了解。此外,单元测试也是最可落地执行、最有效的操作重构不出错的手段之一。在重构完成后,如果新的代码能够通过单元测试,则说明代码原有逻辑的正确性未被破坏。

总结
重构代码是程序员必备的技能之一,需要有系统的全局认识,包括为什么要重构、重构什么、如何重构等方面。同时,为了保证重构的有效性,需要制定重构计划,有经验、熟悉业务的资深同事来指导,并采用单元测试等手段来确保代码的质量。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员雪球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值