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

优雅密码的秘密就藏在这6大设计原则中

时间:2023-04-01 23:56:31 Java

优雅密码,如亭亭玉立的美人,赏心悦目。但糟糕的代码就像一堆狗屎,让人望而却步。如何写出优雅的代码?那么你必须理解并熟悉这6条设计原则的应用:开闭原则、单一职责原则、接口隔离原则、Dimiter原则、Liskov替换原则、依赖倒置。在本文中,我们将通过代码演示让大家轻松理解这6条代码设计原则。加油~1.开闭原则开闭原则是对扩展开放,对修改关闭。对于扩展和修改,我们怎么理解呢?扩展开放意味着未来业务需求会快速变化,代码要保持灵活性和适应性。Modificationclosed表示不允许在原类中进行修改,保持稳定性。因为每天的需求都是迭代更新的,所以我们经常需要修改原来的代码。如果代码设计不好,可扩展性不强,每次需求迭代都要修改原代码,很可能引入bug。因此,我们的代码应该遵循开闭原则,即对扩展开放,对修改关闭。为了方便大家理解开闭原理,我们来看一个例子:假设有这样一个业务场景,大数据系统推送文件,根据不同的类型采用不同的解析方式。大部分小伙伴都会写如下代码:if(type=="A"){//按照A格式解析}elseif(type="B"){//按照B格式解析}else{//按照默认格式解析}这段代码有什么问题?如果分支很多,这里的代码就会变得臃肿,难以维护,可读性低。如果需要接入新的分析类型,只能修改原来的代码。显然,增加或删除某个逻辑,需要修改原类的代码,这就违反了开闭原则。为了解决这个问题,我们可以使用策略模式对其进行优化。可以先声明一个文件解析的接口,如下:publicinterfaceIFileStrategy{//是哪种类型的文件解析,A还是B//封装的公共算法(具体解析方法)voidresolve(Objectparam);}然后实现不同的文件解析策略,比如A类解析:文件_A_RESOLVE;}@Overridepublicvoidresolve(Objectobjectparam){logger.info("类型分析文件,参数:{}",objectparam);//A类型解析具体逻辑}}如果以后需求发生变化,比如增加或者删除某个逻辑,不会修改到原来的类,只需要修改对应??文件解析类型的类即可。2.单一职责原则SingleResponsibilityPrinciple:一个类或者一个接口最好只负责一个职责。假设类C违反单一原则,它负责两个职责P1和P2。当职责P1需要修改时,会改为C类,可能会影响到原来正常的P2。如何更好地理解它?比如你实现一个图书馆管理系统,如果一个类包含图书和读者的增删改查,你可以认为这个类违反了单一原则。因为这个类涉及到不同的功能责任点,所以可以拆分这个。上面图书馆管理系统的例子违反了单一原则,按业务拆分。这样比较容易理解,但是有时候,一个类并不是那么容易区分的。这时候你可以看这个标准来判断功能职责是否单一:类中的私有方法太多,你很难给类起一个合适的名字。类中代码、函数或属性的行数过多。是因为集中操作类中的某些属性类依赖太多其他类,或者依赖类中其他类太多。比如你写了一个方法,包括日期处理和借还书的业务操作,你可以把日期处理抽取出来一个私有方法。然后,如果你发现很多私有方法和日期处理类似,你可以把这个日期处理方法提取到一个工具类中。在日常开发中体现单一原则的思想。比如微服务拆分。3、接口隔离原则接口隔离原则:不应该强迫接口的调用者或使用者去依赖它不需要的接口。要求建立单一的接口,不要创建大而臃肿的接口,尽量细化接口,接口中的方法尽量少,让接口只包含客户端(调用者)是的方法即一个类对另一个类的依赖应该建立在最小的接口上。例如,A类通过接口I依赖于B类,C类通过接口I依赖于D类。如果接口I不是A类和B类的最小接口,那么B类和D类必须实现它们的方法不需要。.如下图:这张图表达的意思是A类依赖接口I中的method1和method2,B类是依赖A类的实现。C类依赖接口I中的method1和method3,类D是对类C的依赖的实现,对于实现类B和D,它们都有未使用的方法,但是因为实现了接口I,所以必须实现这些未使用的方法。你可以看下面的代码:publicinterfaceI{voidmethod1();无效方法2();voidmethod3();}@ServicepublicclassA{@Resource(name="B")privateIi;publicvoiddepend1(){i.method1();}publicvoiddepend2(){i.method2();}}@Service("B")publicclassBimplementsI{@Overridepublicvoidmethod1(){System.out.println("ClassBimplementsmethod1ofinterfaceI");}@Overridepublicvoidmethod2(){System.out.println("B类实现接口I的方法2");}//这个方法没有用到,但是必须默认实现,因为我有这个接口方法@Overridepublicvoidmethod3(){}}@ServicepublicclassC{@Resource(name="D")privateIi;publicvoiddepend1(Ii){i.method1();}publicvoiddepend3(Ii){i.method3();}}@Service("D")publicclassDimplementsI{@Overridepublicvoidmethod1(){System.out.println("ClassDimplementsinterfaceIMethod1");}//这个方法没有用到,但是必须默认实现,因为我有这个接口方法@Overridepublicvoidmethod2(){}@Overridepublicvoidmethod3(){System.out.println("类D实现接口I的方法3");}}你会发现,如果接口过于臃肿,只要接口出现了Method,无论是否为依赖它的类使用,实现类都必须实现这些方法。实现类B没有使用method3,它也有一个默认实现。实现类D没有使用method2,它也有默认实现。显然,这不是一个好的设计,违反了接口隔离原则。我们可以拆分接口I,拆分设计如图2所示:接口是不是越细越好?并不真地。在日常开发中,在使用接口隔离原则对接口进行限制时,要注意以下几点:接口尽量小,但要有一个限度。界面细化可以提高程序设计的灵活性,这是不可否认的事实,但如果界面太小,又会造成界面过多,使设计复杂化。所以一定要适度地做。为依赖接口的类定制服务,只将它需要的方法暴露给调用类,隐藏它不需要的方法。只有专注于为模块提供定制服务,我们才能建立最小的依赖关系。提高凝聚力,减少外部互动。让接口用最少的方法完成最多的事情。使用接口隔离原则,一定要适度,接口设计过大或过小都不好。在设计界面时,只有花更多的时间思考和规划,才能准确地实践这一原则。4.得墨忒尔定律的定义:又称最少知识原则。一个类对其他类的了解越少越好,也就是说一个对象对其他对象的了解越少越好,只和朋友交谈,不和陌生人交谈。其核心思想是尽量减少类之间的耦合,尽量减少代码修改对原有系统的影响。举个生活中的例子:你肯定对你的对象很了解,但是如果你对别人的对象也很了解,如果你的对象知道了,那么就会有大事发生。让我们看下一个违反迪米特定律的例子。业务场景如下:某学校要求打印出所有师生的身份证。//学生类Student{privateStringid;publicvoidsetId(Stringid){this.id=id;}publicStringgetId(){返回id;}}//教师类Teacher{privateStringid;publicvoidsetId(Stringid){this.id=id;}publicStringgetId(){返回id;}}//manager(monitor)publicclassMonitor{//所有学生publicListgetAllStudent(){Listlist=newArrayList();for(inti=0;i<100;i++){Studentstudent=newStudent();//给每个学生分配一个IDstudent.setId("StudentId:"+i);list.add(学生);}返回列表;}}//PrincipalpublicclassPrincipal{//所有教师publicListgetAllTeacher(){Listlist=newArrayList();for(inti=0;i<20;i++){Teacheremp=newTeacher();IDemp.setId("教师编号"+i);list.add(emp);}返回列表;}//所有师生publicvoidprintAllTeacherAndStudent(ClassMonitorclassMonitor){Listlist1=classMonitor.getAllStudent();for(Students:list1){System.out.println(s.getId());}Listlist2=this.getAllTeacher();for(Teacherteacher:list2){System.out.println(teacher.getId());}}}这段代码的问题在于Principal类。根据迪米特定律,只能和直接好友通信,而Student类不是Principal类的直接好友(表现为局部变量Coupling不属于直接好友),按理说principal就够了只和managerMonitor耦合,Principal可以继承类Monitor重写一个printMember方法。优化后的代码如下:publicclassMonitor{publicListgetAllStudent(){Listlist=newArrayList();for(inti=0;i<100;i++){Studentstudent=newStudent();//给每个学生分配一个IDstudent.setId("StudentId:"+i);list.add(学生);}返回列表;}publicvoidprintMember(){Listlist=this.getAllStudent();for(Studentstudent:list){System.out.println(student.getId());}}}publicclassPrincipalextendsMonitor{publicListgetAllTeacher(){Listlist=newArrayList();for(inti=0;i<30;i++){Teacheremp=newTeacher();//依次为所有教师分配一个IDemp.setId("教师编号"+i);list.add(emp);}返回列表;}publicvoidprintMember(){super.printMemberr();for(Teacherteacher:this.getAllTeacher()){System.out.println(teacher.getId());}}}5。里氏替换原则里氏替换原则:如果对于每个类型为S的对象o1都有一个类型为T的对象o2,使得当T的所有对象o1都被替换为o2时,程序P的行为保持不变,则类型S是类型T的子类型一句话描述就是:只要有父类,就可以换成子类,不会有错误和异常。更一般地说,子类可以扩展父类的功能,但不能改变父类原有的功能。其实,里氏代换原则的定义可以概括为:子类可以实现父类的抽象方法,但不能重写父类的非抽象方法。使用方法时,方法的前置条件(即方法的入参)比父类的入参宽松。当子类的方法实现父类的方法(重写/重载或实现抽象方法)时,postcondition设置条件(即方法的输出/返回值)更严格或等于方法的方法父类。我们来看一个例子:publicclassCache{publicvoidset(Stringkey,Stringvalue){}}publicclassRedisCacheextendsCache{publicvoidset(Stringkey,Stringvalue){}}这里的例子没有违反Liskov代换原则,凡是父类或父接口出现的地方,子类都可以出现。如果在RedisCache中加入参数校验,则如下:==null||key.length()<10||key.length()>100){System.out.println("key的长度不符合要求");抛出新的IllegalArgumentException();}}}这违反了采用里氏替换原则,因为子类方法添加了参数验证并抛出异常,虽然子类仍然可以替换父类。6.依赖倒置原则依赖倒置原则的定义:高层模块不应该依赖低层模块,两者都应该依赖于它们的抽象;抽象不应该依赖于细节,细节应该依赖于抽象。它的核心思想是:为接口编程,而不是为实现编程。依赖倒置原则可以降低类之间的耦合度,提高系统的稳定性,降低并行开发带来的风险,提高代码的可读性和可维护性。为了满足依赖倒置原则,我们需要在项目中满足这个规则:每个类都试图提供一个接口或抽象类,或两者兼而有之。变量的声明类型尽量是接口或者抽象类。任何类都不应派生自具体类。使用继承时尽量遵循里氏代换原则。我们来看一段违反依赖倒置原则的代码。业务需求是:客户在淘宝购物。代码如下:classCustomer{publicvoidshopping(TaoBaoShopshop){//ShoppingSystem.out.println(shop.buy());}}上面的代码有问题。如果以后产品需要改变,把客户换成京东网购,需要修改代码为:classCustomer{publicvoidshopping(JingDongShopshop){//ShoppingSystem.out.println(shop.buy());}}如果商品改成天猫购物怎么办?那你就得修改代码,显然这违反了开闭原则。客户类在设计时就绑定了具体的购物平台类,违反了依赖倒置原则。可以设计一个店铺界面,不同的购物平台(如淘宝、京东)实现这个界面,即修改客户类为这个界面编程,就可以解决这个问题。代码如下:classCustomer{publicvoidshopping(Shopshop){//ShoppingSystem.out.println(shop.buy());}}interfaceShop{Stringbuy();}ClassTaoBaoShopimplementsShop{publicStringbuy(){return"淘宝购物";}}ClassJingDongShopimplementsShop{publicStringbuy(){return"京东购物";}}