欢迎来到思维库

思维库

偷偷看了同事的代码找到了优雅代码的秘密

时间:2025-11-05 12:50:52 出处:人工智能阅读(143)

引言

对于一个软件平台来说,偷偷软件平台代码的看同好坏直接影响平台整体的质量与稳定性。同时也会影响着写代码同学的代码代码的秘创作激情。想象一下如果你从git上面clone下来的优雅的工程代码乱七八糟,代码晦涩难懂,偷偷难以快速入手,看同有种想推到重写的代码代码的秘冲动,那么程序猿在这个工程中写好代码的优雅初始热情都没了。相反,偷偷如果clone下的看同代码结构清晰,代码优雅易懂,代码代码的秘那么你在写代码的优雅时候都不好意思写烂代码。这其中的偷偷差别相信工作过的同学都深有体会,那么我们看了那么多代码之后,看同到底什么样的代码代码的秘代码才是好代码呢?它们有没有一些共同的特征或者原则?本文通过阐述优雅代码的设计原则来和大家聊聊怎么写好代码。源码库

代码设计原则

好代码是设计出来的,也是重构出来的,更是不断迭代出来的。在我们接到需求,经过概要设计过后就要着手进行编码了。但是在实际编码之前,我们还需要进行领域分层设计以及代码结构设计。那么怎么样才能设计出来比较优雅的代码结构呢?有一些大神们总结出来的优雅代码的设计原则,我们分别来看下。

SRP

所谓SRP(Single Responsibility Principle)原则就是职责单一原则,从字面意思上面好像很好理解,一看就知道什么意思。但是看的会不一定就代表我们就会用,有的时候我们以为我们自己会了,但是在实际应用的时候又会遇到这样或者那样的问题。原因就是云南idc服务商实际我们没有把问题想透,没有进行深度思考,知识还只是知识,并没有转化为我们的能力。就比如这里所说的职责单一原则指的是谁的单一职责,是类还是模块还是域呢?域可能包含多个模块,模块也可以包含多个类,这些都是问题。

为了方便进行说明,这里以类来进行职责单一设计原则的说明。对于一个类来说,如果它只负责完成一个职责或者功能,那么我们可以说这个类时符合单一职责原则。请大家回想一下,其实我们在实际的编码过程中,已经有意无意的在使用单一职责设计原则了。因为实际它是符合我们人思考问题的方式的。服务器托管为什么这么说呢?想想我们在整理衣柜的时候,为了方便拿衣服我们会把夏天的衣服放在一个柜子中,冬天的衣服放在一个柜子。这样季节变化的时候,我们只要到对应的柜子直接拿衣服就可以了。否则如果冬天和夏天的衣服都放在一个柜子中,我们找衣服的时候可就费劲了。放到软件代码设计中,我们也需要采用这样的分类思维。在进行类设计的时候,要设计粒度小、功能单一的类,而不是大而全的类。

举个栗子,在学生管理系统中,如果一个类中既有学生信息的操作比如创建或者删除动作,又有关于课程的创建以及修改动作,那么我们可以认为这个类时不满足单一职责的设计原则的,因为它将两个不同业务域的业务混杂在了一起,所以我们需要进行拆分,将这个大而全的类拆分为学生以及课程两个业务域,这样粒度更细,更加内聚。

笔者根据自身的经验,总结了需要考虑进行单一职责拆分的几个原则,希望对大家判断是否需要进行拆分有个简单的判断的标准:

1、不同的业务域需要进行拆分,就像上面的例子,另外如果与其他类的依赖过多,也需要考虑是不是应该进行拆分;

2、如果我们在类中编写代码的时候发现私有方法具有一定的通用性,比如判断ip是不是合法,解析xml等,那我们可以考虑将这些方法抽出来形成公共的工具类,这样其他类也可以方便的进行使用。

另外单一职责的设计思想不止在代码设计中使用,我们在进行微服务拆分的时候也会一定程度的遵循这个原则。

OCP

OCP(Open Closed Principle)即对修改关闭,对扩展开放原则,个人觉得这是设计原则中最难的原则。不仅理解起来有一定的门槛,在实际编码过程中也是不容易做到的。

首先我们得先搞清楚这里的所说的修改以及扩展的区别在什么地方,说实话一开始看到这个原则的时候,我总觉得修改和开放说的不是一个意思嘛?想来想去都觉得有点迷糊。后来在不断的项目实践中,对这个设计原则的理解逐渐加深了。

设计原则中所说的修改指的是对原有代码的修改,而扩展指的是在原有代码基础上的能力的扩展,并不修改原先已有的代码。这是修改与扩展的最大的区别,一个需要修改原来的代码逻辑,另一个不修改。因此才叫对修改关闭但是对扩展开放。弄清楚修改和扩展的区别之后,我们再想一想为什么要对修改关闭,而要对扩展开放呢?我们都知道软件平台都是不断进行更新迭代的,因此我们需要不断在原先的代码中进行开发。那么就会涉及到一个问题如果我们的代码设计的不好,扩展性不强,那么每次进行功能迭代的时候都会修改原先已有的代码,有修改就有可能引入bug,造成系统平台的不稳定。因此我们为了平台的稳定性,需要对修改关闭。但是我们要添加新的功能怎么办呢?那就是通过扩展的方式来进行,因此需要实现对扩展开放。

这里我们以一个例子来进行说明,否则可能还是有点抽象。在一个监控平台中,我们需要对服务所占用CPU、内存等运行信息进行监控,第一版代码如下。

public class Alarm {

private AlarmRule alarmRule;

private AlarmNotify alarmNotify;

public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {

this.alarmRule = alarmRule;

this.alarmNotify = alarmNotify;

}

public void checkServiceStatus(String serviecName, int cpu, int memory) {

if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {

alarmNotify.notify(serviecName + alarmRule.getRule(ServiceConstant.Status).getDescription)

}

if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {

alarmNotify.notify(serviecName + alarmRule.getRule(ServiceConstant.Status).getDescription)

}

}

}

代码逻辑很简单,就是根据对应的告警规则中的阈值判断是否达到触发告警通知的条件。如果此时来了个需求,需要增加判断的条件,就是根据服务对应的状态,判断需不需要进行告警通知。我们来先看下比较low的修改方法。我们在checkServiceStatus方法中增加了服务状态的参数,同时在方法中增加了判断状态的逻辑。

public class Alarm {

private AlarmRule alarmRule;

private AlarmNotify alarmNotify;

public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {

this.alarmRule = alarmRule;

this.alarmNotify = alarmNotify;

}

public void checkServiceStatus(String serviecName, int cpu, int memory, int status) {

if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {

alarmNotify.notify(serviecName + alarmRule.getRule(ServiceConstant.Status).getDescription)

}

if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {

alarmNotify.notify(serviecName + alarmRule.getRule(ServiceConstant.Status).getDescription)

}

if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {

alarmNotify.notify(serviecName + alarmRule.getRule(ServiceConstant.Status).getDescription)

}

}

}

很显然这种修改方法非常的不友好,为什么这么说呢?首先修改了方法参数,那么调用该方法的地方可能也需要修改,另外如果改方法有单元测试方法的话,单元测试用例必定也需要修改,在原有测试过的代码中添加新的逻辑,也增加了bug引入的风险。因此这种修改的方式我们需要进行避免。那么怎么修改才能够体现对修改关闭以及对扩展开放呢?

首先我们可以先将关于服务状态的属性抽象为一个ServiceStatus 实体,在对应的检查方法中以ServiceStatus 为入参,这样以后如果还有服务状态的属性增加的话,只需要在ServiceStatus 中添加即可,并不需要修改方法中的参数以及调用方法的地方,同样单元测试的方法也不用修改。

@Data

public class ServiceStatus {

String serviecName;

int cpu;

int memory;

int status;

}

另外在检测方法中,我们怎么修改才能体现可扩展呢?而不是在检测方法中添加处理逻辑。一个比较好的实现方式就是通过抽象检测方法,具体的实现在各个实现类中。这样即使新增检测逻辑,只需要扩展检测实现方法就可,不需要在修改原先代码的逻辑,实现代码的可扩展。

LSP

LSP(Liskov Substitution Principle)里氏替换原则,这个设计原则我觉得相较于前面的两个设计原则来说要简单些。它的内容为子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

我们怎么判断有没有违背LSP呢?我觉得有两个关键点可以作为判断的依据,一个是子类有没有改变父类申明需要实现的业务功能,另一个是否违反父类关于输入、输出以及异常抛出的规定。

ISP

ISP(Interface Segregation Principle)接口隔离原则,简单理解就是只给调用方需要的接口,它不需要的就不要硬塞给他了。这里我们举个栗子,以下是关于产品的接口,其中包含了创建产品、删除产品、根据ID获取产品以及更新产品的接口。如果此时我们需要对外提供一个根据产品的类别获取产品的接口,我们应该怎么办?很多同学会说,这还不简单,我们直接在这个接口里面添加根据类别查询产品的接口就OK了啊。大家想想这个方案有没有什么问题。

public interface ProductService {

boolean createProduct(Product product);

boolean deleteProductById(long id);

Product getProductById(long id);

int updateProductInfo(Product product);

}

public class UserServiceImpl implements UserService { //...}

这个方案看上去没什么问题,但是再往深处想一想,外部系统只需要一个根据产品类别查询商品的功能,,但是实际我们提供的接口中还包含了删除、更新商品的接口。如果这些接口被其他系统误调了可能会导致产品信息的删除或者误更新。因此我们可以将这些第三方调用的接口都隔离出来,这样就不存在误调用以及接口能力被无序扩散的情况了。

public interface ProductService {

boolean createProduct(Product product);

boolean deleteProductById(long id);

Product getProductById(long id);

int updateProductInfo(Product product);

}

public interface ThirdSystemProductService{

ListgetProductByType(int type);

}

public class UserServiceImpl implements UserService { //...}LOD

LOD(Law of Demeter)即迪米特法则,这是我们要介绍的最后一个代码设计法则了,光从名字上面上看,有点不明觉厉的感觉,看不出来到底到底想表达什么意思。我们可以来看下原文是怎么描述这个设计原则的。

Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

按照我自己的理解,这迪米特设计原则的最核心思想或者说最想达到的目的就是尽最大能力减小代码修改带来的对原有的系统的影响。所以需要实现类、模块或者服务能够实现高内聚、低耦合。不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。打个比方这就像抗战时期的的地下组织一样,相关联的聚合到一起,但是与外部保持尽可能少的联系,也就是低耦合。

总结

本文总结了软件代码设计中的五大原则,按照我自己的理解,这五大原则就是程序猿代码设计的内功,而二十三种设计模式实际就是内功催生出来的编程招式,因此深入理解五大设计原则是我们用好设计模式的基础,也是我们在平时设计代码结构的时候需要遵循的一些常见规范。只有不断的在设计代码-》遵循规范-》编写代码-》重构这个循环中磨砺,我们才能编写出优雅的代码。

分享到:

上一篇:图解演示环境版本:本机系统: WIN7虚拟机:VMware Workstation 8 (英文版)安装目标:Ubuntu Desktop 12.04 LTS  (请点击这里)先下载好iso镜像文件详细过程图解:0. 初始画面,点击“Create a New Virtual Machine”(左上Ubuntu为本人已有开发环境机,请忽略)1. 点击“Custom(自定义)”2. 无需选择,直接Next(上面是选Workstation版本的兼容性的,这里默认为当前版本8.0,之前版本的不同在于Limitations(局限),如内存更少,不支持HD Audio等)3. 选择“I will install the operating system later”这里无严格要求的同学,是可以选择第二项“Installer disc image file (ios)”的,之后会VMware会自动得知你的iso是Linux(Ubuntu),只要求你输入Full name,和用户名密码等简单的用户设定,但是这是一个Easy install,如VMware原文所说“When the New Virtual Wizard detects an operating system that supports Easy Install, the wizard prompts you for information about the guest operating system. After the virtual machine is created, the guest operating system installation is automated and VMware Tools is installed.” 我觉得是因为这个OS的自动安装,不完全,导致一些核心命令无法使用、无反应等一些问题。所以有更高要求的同学,不能选这项,需要完全、自定义的安装。4. 在Version下选择“Ubuntu”,注:64位Ubuntu需要选下面那个“Ubuntu 64-bit”5. 设置虚拟机名称(即每次启动VMware左上方显示的名字),之后选择你想的在WIN7里的安装路径(默认在C盘,很不方便)。6. Number of processors(处理器个数)选择为2我是i7处理器,配置较好无压力的,感觉双核比单核好一些(假如没用VMware不会这么设计,但是对于更多的,没必要),下面那个应该没必要选,有非常懂的同学,请留言赐教。7. 内存大小选择,使用自动推荐的1G内存(本机内存8G)。同学们在虚拟机里,应该不会跑什么惊天地泣鬼神的大程序,内存大不等于快,而是更多的数据放在内存里而非硬盘里,对于内存消耗大的程序、系统会变快。去年做本科毕设的时候,调整过虚拟机的内存从1G为2G,结果竟然变慢了,应该是外面WIN7被占用了的问题。8. Network Type网络类型选择,本次选择默认的“NAT”注:这里有一点本人经历的非常重要需要说明,使用“NAT”的话,需要外面的WIN7使用一根线连接上网,才能在Ubuntu里上网(如同Ubuntu是你的真正OS的感觉,不需要手工配置任何IP信息),不能默认使用无线连接。这点对有些笔记本同学可能会造成麻烦。当然不是说不能通过手动配置IP相关解决,但是为了避免每次都配置的麻烦,请直接使用“bridged”桥接手动配置。9. 默认即可,直接“Next”10. 默认即可,直接“Next”第三项为直接划分硬盘给该虚拟机使用,意思应为绕过WIN7的那个文件夹管理,直接给虚拟机只用一块硬盘空间,有高级需要的同学可以选择。所以,注:默认的那个可以轻松实现copy,move,当你想拷给另外一个人,或者换机器的时候。11. 磁盘选择,默认即可,直接“Next”12. 选择“Store virtual disk as a single file”上面那个方框,是说现在就立即分20G给这个虚拟机,假如不够,还是会一点一点随着你的使用增加(跟不选一样)。假如同时没有很多个虚拟机装在WIN7上,或者硬盘空间太大又不放东西,可选。13. 虚拟机文件的存放地址,选个D盘的位置就行了。14. 点击“Finish”,完成了虚拟机的配置工作这里点击“Customize Hardware”的话,有机会对前面不满意的虚拟机硬件设置(处理器个数,内存大小等)重新设置,所以前面不满意的同学,不用点cancel重来,实际上在以后的使用过程,也是可以随时改变虚拟机的配置的,这点不用担心。15. 完成后,可以看到左上角多出了“Ubuntu 12.04”,先别急着Power on,还没装ubuntu呢。。。点击“Edit virtual machine settings”16. 在弹出的settings里,点击“CD/DVD(IDE)”,然后在右侧点击“Use ISO image file”,再选择你开始下载好的Ubuntu 12.04的iso镜像文件的路径然后点“OK”。17. 启动虚拟机,即点击step 15里的“Power on this virtual machine”,之后Ubuntu 12.04开始了安装,先选择语言,然后点击“Install Ubuntu”18. 假如选择“Download updates while installing”为安装过程直接安装最近的更新,假如选择“Install this third-party software”为安装第三方软件19. 选择“Something else”,将要对虚拟机的20G硬盘做手动分区20. 点击“New Partation Table”(新建分区表)21. 在弹出的对话框里,选择“Contunie”22. 选中新出现的“free space”(空闲空间),点击“Add”23. 注意下图中的“Primary”,“Beginning”, “Ext4 ...”均为默认,不需要修改;数字为大小,以MB为单位(注:不用追求1024凑整,硬盘实际上是凑不整的。。。),这里选择10000=10G;最后的“Mount point(挂载点)”下拉列表中,选中“/”,完成该步,点“OK”注意:“/ ” 建议大小在5GB以上。(根据关于“Ubuntu手动分区”的多个相关文章一致得来)非常注意:本人上次弄了个6G,结果进去下libraries,一下就满了,那叫一个悲剧!所以,同学们千万别抱着“5G以上”来想,ubuntu应该自己就占了4、5G,不想悲剧的同学至少8G以上吧,20G确实不大,但是假如打算长期的同学,应该不会使用虚拟机了,20G跑程序,绰绰有余,等喜欢了熟悉了,再来个真的吧。24. 再次选中“free space”(同step 22图中),点击“Add”;注意下图中“Logical”,“Beginning”均为默认,大小选择1000(1G);在Use as的下拉列表中选择“swap area”,注:最后的下拉列表为灰色,意为swap area不用选择挂载点;完成该步,点“OK”注意:“swap area” 即交换分区,建议大小是物理内存的1~2倍。(根据关于“Ubuntu手动分区”的多个相关文章一致得来)不需要太大,1G足以。25. 再次选中“free space”(同step 22图中),点击“Add”;注意下图中“Logical”,“Beginning”, “Ext4 ...”均为默认;注:大小选择也为默认,即所有的剩余空间;最后的“Mount point”下拉列表中,选中“/home”;完成该步,点“OK”注意:“/home” 存放普通用户的数据,是普通用户的宿主目录,建议大小为剩下的空间。(根据关于“Ubuntu手动分区”的多个相关文章一致得来)注:三个分区的顺序不要变,因为/home在最后便于默认选择“剩余的空间”,避免手工分配。26. 至此,所有分区工作已经完成,如下图所示。注:假如不满意可以点击“Revert(还原)”来重新分区,直到满意和准确无误为止。假如感到满意,点击“Install Now”注:上图为悲剧图,6G的/是不够的,这个图没有更新,仅供参考,不比看数字。27. 选择你所在的时区,自动调整时间,夏令时什么的手动调不方便,之后都点击“Continue”以继续28. 键盘选择US,一般国内买的电脑都是这样的,可根据情况自己选择29. Ubuntu的个人设置,根据自己需要填写用户名密码等30. 最后安装完成,点击“Restart Now”重启Ubuntu即可31. 停止在如下画面,按“回车”即可至此,全部安装过程完毕,我们可以进入到Ubuntu 12.04的桌面工作了。一定要注意:由于未使用自动安装,所以现在我们的虚拟机不含有VM Tools,导致无法全屏虚拟机等等问题,需要安装VM tools,详情请搜索即可。

下一篇:美图手机T8评价(美图T8手机评测报告,带你领略拍照新境界)

温馨提示:以上内容和图片整理于网络,仅供参考,希望对您有帮助!如有侵权行为请联系删除!

友情链接: