版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
4-设计可复用的类概述面向复用的软件构造时,我们重复利用执行前写好的代码,进行进一步的开发。而我们在编程时,就需要我们的软件具有更好的可复用性,也就是开发可复用的软件。在一般的软件开发的过程中,我们经常会有需求的改变,如果我们写的抽象与接口都具有很良好的可复用性那么我们就可以很快地选取我们需要的抽象,然后对它进行修改,拓展以及适配。概述上一章提到,解耦和复用是软件开发的两大命门。事实上,只要一个物体是由很多部分组成,都存在耦合,不仅仅是软件。例如房子,由砖、钢筋、水泥等组织在一起,这些不同物品就需要粘接在一起,动其中一个,就可能会影响到其他的部分,这就是耦合。什么是解耦呢?还是拿房子来看,例如门和墙,是连接在一起的,假如我要换一个门,我不需要对墙有什么变动,只需要把门卸下来,重新装上另外一个门就是了,这就是解耦。概述但是,这里有一个前提条件,就是门和墙的连接是活页,这个活页既适合这个门,又适合另外一个门,否则要是换一个门,连接器就要换,那就可能对墙造成破坏;第二,门的大小一定要正合适,如果第二个门比第一个门大,或者比第一个门小,那么都需要对墙体重新调整,这也会造成对墙体的破坏。因此,我们发现,要实现解耦,必须要有标准的接口,这个接口包括连接接口和装配接口。概述软件其实也是一样,软件是由各种各样的模块组合在一起的,这些模块之间有着千丝万缕的联系。而且,我们发现,软件各个部分之间的数据关系更复杂,同时,这种数据关系还会不断的发生变化,因此要实现彻底解耦难度更大。还是拿房屋来举例,一般来说,为了美观,一个房子的电线一般走暗线,这些线往往是埋在墙里面。假如,某一天某个电线出问题了,或者是这个房子需要扩展,需要改变电线线路,那么问题就麻烦了,我们不得不把墙体打开,然后把线路重新调整,这就把墙体给破坏了。概述软件之间的数据关系、逻辑关系,更像房屋部署的电线,是藏在各个模块里面的有时候变更一个模块,不得不把相关得模块都进行改进,这就导致了软件变更的麻烦。更麻烦的就是底层数据结构的改变,对于软件来说,底层数据结构就好比地基,我们知道,房屋的地基一变,整个房子就可能推倒重来软件也一样,底层数据结构的变动就可能导致整个软件从上到下的变更,甚至推倒重来。概述所以,我们发现,对于软件来说:操作层面的解耦比较容易;中间业务逻辑层解耦相对麻烦;而底层数据库的解耦最为困难。如果有一种技术能够把操作层、业务层、数据结构层都能实现彻底解耦,那么,这个解耦就是一场革命性的技术。概述复用可以大大的节省开发时间。复用也不是说简单的复制粘贴,也不是一成不变的引用,而是可以在调整的基础上再次利用,这个也算是复用。复用和解耦是密切关联的,如果耦合度过高,你必须把耦合在一起的都复用过来,这样的复用,其实效率很低。例如,早期面向过程开发,模块和模块之间都是硬耦合在一起,需要把软件整个复用过来,然后再二次修改,因为,你发现单单复用个别的模块不行,都是纠缠在一起,只能全部都拿过来。概述再后来,面向对象开发,人们把很多通用的标准的模块提取出来,开发成通用类,这些类有标准的接口,供其他类调用。这个时候,这些通用类就能很好的单个复用。另外就是很多开发人员总结出了整体框架,把一些环境和工具类、通用类做成一种开发结构,开发人员可以把整个框架复用过来,也是很标准的,也会省去很多开发工作。目前,主流的复用就是这种模式,即通用类和通用框架复用。概述但是,一旦涉及到和业务相关,就会有具体的业务逻辑,以及具体的数据结构,这个时候,业务逻辑耦合、数据结构耦合度太大,在这个层面上的复用就很糟糕了。现在比较热门的微服务架构,或者是中台战略,其实是在业务层面上解耦。例如,原来是10个业务放在一个软件里,现在将10个业务分离出来,每个业务成为一个独立的服务或者独立的软件。但是这种模式并没有彻底解决问题。概述首先,每个业务内部的业务逻辑还是耦合在一起,无非是原来10种耦合,换成了1种耦合,量上有变化,质上无改善。其次,每个业务的所涉及的数据结构仍然制约着上下左右,数据结构的变化同样会导致一系列的变更。第三,这种复用还是单层复用,无法实现无限嵌套。也就是说,假如我需要一个类似的服务,我就去把同样的服务拷贝一份,然后再调整。但是,能不能实现服务里面嵌套服务,嵌套的服务再嵌套,以至于层层无穷迭代嵌套。这是目前无法实现的。概述第四,服务分离出来,需要增加额外的开销管理这些服务,这些额外的开销也需要开发。总之,微服务架构或者所谓的中台,虽然很热,但是都没有彻底解决问题。概述我们的构件技术,无论是从操作层面还是业务逻辑层面,甚至是数据结构层,都实现了相对彻底的解耦。以数据结构来说,数据结构的变化,只需要更改配置文件,而不需要进行代码上的变更,甚至数据访问服务也无需任何变更,就能实现软件的更新。这套技术极大的提升了开发效率,开发者不必为每次数据结构的变化而痛苦。而在以前,每当数据结构变化,就会从数据访问层,到业务逻辑,然后是UI层,层层变化,改完了,还需要重新部署服务,真的很痛苦,现在是彻底解脱了。概述因为在解耦上的彻底,因此,构件的复用也做到了极致,可以层层嵌套复用,极大的提升了复用的空间。这一章我们一起来看一下如何构造可复用的类。01SOLID02其他原则03可复用模块的演化之路04提高代码复用性的方法SOLID几乎所有学过编程的人都声称“熟悉面向对象”,可是有几人能够概括?面向对象方法和原则,面向对象设计和过程式设计的关键区别在哪,以及怎么定义和衡量可读性、究竟什么是可复用性、怎样的代码称为脆弱和僵化的。SOLID是由RobertC.Martin提出,指代面向对象编程和面向对象设计的五个基本原则:SRP单一职责、OCP开闭原则、LSP里氏替换、LOD迪米特法则、ISP接口隔离以及DIP依赖反转(两个L算做一个)。SOLID概括的讲,单一职责是所有设计原则的基础。开闭原则是设计的终极目标。里氏替换原则强调的是:子类替换父类后程序运行时的正确性,它用来帮助实现开闭原则。而接口隔离原则用来帮助实现里氏替换原则,同时它也体现了单一职责。依赖倒置原则是过程式设计与面向对象设计的分水岭,同时它也被用来指导接口隔离原则。SOLIDSOLID(1)它违反了单一职责:这个main方法即获取了当前日期、又做了日期的处理、数据的写入。这个违反让它更难理解和维护了吗?(2)它违反了开闭原则:很显然这个逻辑并不是可扩展的,只能通过修改的方式去增加功能。这个违反让它更难修改了吗?(3)它违反了依赖倒置原则:main函数直接依赖底层模块fs的writeFile方法,调用main时必须和fs模块绑定。这个绑定让它无法在其他fs实现里复用。
SOLID对于一个负责日常工作的、功能不太会扩展的小脚本而言,这段代码是比较完美的。当我们不认为它的维护是困难的,就不应该对它套用各种原则和模式来把问题复杂化(或过度设计)。但是,当这段代码变得更复杂,我们发现它难以维护时,再去应用原则来诊断和分析问题。有这样的概念后,我们再来看具体的每项原则。SRP单一职责原则单一职责原则(SingleResponsibilityPrinciple,SRP)是指一个类只负责完成一个职责或者功能。一个类应该只有一个发生变化的原因。单一职责希望避免的问题很简单:避免修改一个地方反而引发另一个地方的问题。如果你的项目经常发生这样的问题,或你的QA同学经常有这样的质疑,那么应该抱着这个原则去重新审视你的业务逻辑。此外当一个类(或一个函数)足够复杂时,它可读性、健壮性、可复用性一定会存在问题。SRP单一职责原则1)可读性差。无论是否有注释,谁都很难把握一个连续好几页的函数到底在做什么。2)脆弱。在很难理解一个函数职责的情况下,更改其中的一部分是很容易犯错的。尤其在存在很多横贯整个函数的变量时,或者存在大量条件分支的情况。3)难以复用。把多个职责包裹在一起就像打包销售一样,很少有其他场景恰好有需要完全一样的打包功能。SRP单一职责原则单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起、来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也就会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。SRP单一职责原则一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:类中的代码行数、函数或者属性过多;类依赖的其他类过多或者依赖类的其他类过多;私有方法过多;比较难给类起一个合适的名字;类中大量的方法都是集中操作类中的某几个属性。SRP单一职责原则SRP单一职责原则问题由来:类T负责两个不同的职责,即职责P1和职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。解决方案:分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。SRP单一职责原则单一职责原则应用场景:适用于类、接口、方法。这个原则有什么用呢?它让类的职责更加单一。这样的话,每个类只需要负责自己的那部分,类的复杂度就会降低。如果职责划分的很清楚,那么代码维护起来也更加容易。试想如果所有的功能都放在一个类中,那么这个类就会变得非常臃肿,而且一旦出现bug,要在所有代码中去寻找;可能要改变整个代码的结构,想想非常可怕。由于一些其他因素的影响,类的单一职责在项目中是很难保证的。通常,接口和方法的单一职责更容易实现。程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic。SRP单一职责原则我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的。虽然违背了单一职责原则,但花销却小的多。
SRP单一职责原则这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改,会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。SRP单一职责原则SRP单一职责原则这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。
SRP单一职责原则这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。可参考的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则。OCP开闭原则开闭原则(Open/ClosedPrinciple,OCP)定义:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。如何理解“对扩展开放,对修改关闭”?添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等)而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。是新增而不是修改。OCP开闭原则有两点要注意:第一点是,开闭原则并不是说完全杜绝修改而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。OCP开闭原则如何做到“对扩展开放、对修改关闭”?时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,要多花点时间思考一下:这段代码未来可能有哪些需求变更?如何设计代码结构?事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。OCP开闭原则OCP开闭原则很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是23种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。OCP开闭原则用开闭原则改善因变化带来的问题:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化而不是通过修改已有的代码来实现。开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。其实只要遵循SOLID中的另外5个原则,设计出来的软件就是符合开闭原则的。OCP开闭原则用抽象构建架构,用实现扩展细节:因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。OCP开闭原则开闭原则是SOLID中最难理解的一项原则:如果一个类/函数对修改是封闭的,那么怎么扩展这个类的功能?这恰好是开闭原则要解决的问题,这个原则尝试去做到:新增功能时,已有的业务代码可以几乎完全保持不变。因为已有的类和函数都没有变更,因此我们才能有信心判断这次变更不会影响其他的功能,这样的设计才是健壮的。应用单一职责只需要梳理和归纳,而应用开闭原则的关键则在于抽象。把它想象成它要表达的有问题的代码,比如shape的类型不止两种,分支条件不止是instanceof,每个条件下做的事情也不止一行。问题是:1)脆弱:如果需要新增一种Shape,我们必须继续更改这个函数,并继续新增条件语句。2)僵化:如果需要新增一种Shape(比如Triangle),我们必须改很多份代码,新增一个drawTriangle方法,再去draw函数里做相应的import和处理。3)牢固:drawAll依赖于Rectangle和Circle,无法拿出来复用。
OCP开闭原则TypeScript实现:注意应用开闭原则进行重构对上述代码产生的影响:1)因为我们从drawAll里干掉了条件分支,这份代码不再脆弱。2)也不再僵化,因为需要新增类型时我们只需要新增一个Triangle类,不需要对drawAll引入变更。3)也不再牢固,因为drawAll不再依赖任何具体的类,它的单元测试和复用也更容易。设计是一种权衡的艺术,让某些事情变好的同时也让另外一些事情变坏了。OCP开闭原则LSP里式替换原则里式替换原则(LiskovSubstitutionPrinciple,LSP)定义定义1:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。定义2:所有引用基类的地方必须能透明地使用其子类的对象。LSP里式替换原则程序中的对象应该可以被其子类实例替换掉,而不会影响程序的正确性。所有引用基类的地方必须透明地使用其子类的对象。子类对象(objectofsubtype/derivedclass)能够替换程序(program)中父类对象(objectofbase/parentclass)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。LSP里式替换原则里式替换原则是用来指导继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“designbycontract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数的原有“约定”。这里的“约定”包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。LSP里式替换原则LSP里式替换原则问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。解决方案:里氏替换原则的意思是,所有基类在的地方,都可以换成子类,程序还可以正常的运行。这个原则是与面向对象语言的继承特性密切相关的。当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。LSP里式替换原则以Java为例,其继承机制有一些优点:可以减少创建类的工作量;提高了代码的重用性;提高了代码的扩展性。但有优点也同样存在缺点:继承是侵入性的,只要继承,就必须拥有父类的方法和属性;降低了代码的灵活性,因为继承时,父类会对子类有一种约束;增强了耦合性,当需要对父类的代码进行修改时,必须考虑到对子类产生的影响。如何扬长避短呢?方法是引入里氏替换原则。LSP里式替换原则里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:1)子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。若子类不完全对父类的方法进行实例化,那么子类就不能被实例化,那么这个接口或抽象类就毫无存在的意义。里氏替换原则规定,子类不能重写父类已实现的方法。父类中已实现的方法其实是一种已定好的规范和契约,如果我们随意的修改了它,那么可能会带来意想不到的错误。LSP里式替换原则我们想要的结果是“1+2=3”。可以看到,方法重写后结果就不是我们想要的结果了。也就是这个程序中子类B不能代替父类A。这违反了里氏替换原则,从而给程序造成了错误。LSP里式替换原则LSP里式替换原则2)子类中可以增加自己特有的方法。这个很容易理解,子类继承了父类,拥有了父类和方法,同时还可以定义自己有、而父类没有的方法。这是在继承父类方法的基础上进行功能的扩展,符合里氏替换原则。3)当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。(即只能重载不能重写)LSP里式替换原则我们应当注意,子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap时,只会执行父类的方法,不会执行子类的重载方法。这符合里氏替换原则LSP里式替换原则4)当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。LSP里式替换原则LSP里式替换原则(4)LSP和多态的区别:理解这个原则,我们还要弄明白,里式替换原则跟多态的区别。虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。LOD迪米特法则(1)LOD迪米特法则(LawofDemeter,LOD)定义,只与你的朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应该发生直接的相互调用。可以通过第三方转发该调用。其目的是降低类直接的耦合度,提高模块的相对独立性。(2)问题由来:类与类之间的关系越密切,耦合度越大,当类发生改变时,对另一个类的影响也越大。LOD迪米特法则(3)解决方案:迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则会有两个优点:一是降低了类之间的耦合度,提高了模块的相对独立性;二是由于亲和度降低,从而提高了类的可复用率和系统的扩展性。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合、高内聚。无论面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量降低,才能提高代码的复用率。怎样编程才能做到低耦合呢?那正是迪米特法则要去完成的。LOD迪米特法则举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一个违反迪米特法则的设计。LOD迪米特法则现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友LOD迪米特法则LOD迪米特法则修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。LOD迪米特法则LOD迪米特法则(4)实现方法:从迪米特法则的定义和特点可知,它强调一下两点:一是从依赖者的角度来说,只依赖应该依赖的对象。二是从被依赖者的角度来说,只暴露应该暴露的方法。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。ISP接口隔离原则(1)ISP接口隔离原则(InterfaceSegregationPrinciple,ISP)定义接口隔离原则的描述是:使用多个特定细分的接口比单一的总接口要好,不能强迫用户去依赖他们用不到的接口。客户端不应该强迫依赖它不需要的接口,类之间的依赖关系应该建立在最小的接口上。其中的“客户端”可以理解为接口的调用者或者使用者。理解“接口隔离原则”的重点是理解其中的“接口”二字。该原则中的接口,是一个泛泛而言的接口,还包括其中的抽象类。ISP接口隔离原则ISP接口隔离原则接口隔离原则和单一原则都是为了提高类的内聚性、降低他们的耦合性,体现了封装的思想但两者是不同的:单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要是约束接口,主要针对抽象和程序整体框架的构建。ISP接口隔离原则(2)问题由来:在接口I中定义多个方法(假如A,B,C,D),类C1通过实现接口I来扩展自己需要的方法A、B;类C2通过实现接口I来扩展自己需要的方法C、D;那么接口I对于类C1和类C2来说不是最小接口,则类C1和类C2必须去实现他们不需要的方法。ISP接口隔离原则(3)解决方案:采用接口隔离原则。将臃肿的接口I拆分为独立的几个接口,类C1和类C2分别与他们需要的接口建立依赖关系。接口隔离原则说白了就是,让客户端依赖的接口尽可能地小,接口隔离原则将非常庞大、臃肿的接口拆分成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是解耦,从而容易重构。根据上面的场景,用代码来讲解接口隔离原则的使用。假如有这么一个需求,老师有两个职责,备课和讲课;学生也有两个职责,听课和复习。如果不考虑接口隔离原则,可以这样来实现。ISP接口隔离原则ISP接口隔离原则从结果来看,是没有问题的,但这时老师有意见了:我备好课、讲完课就完事了,你怎么还给我戴两个听课、复习的空帽子???学生也开始喃喃自语了:你备课、讲课!I
don’t
care!ISP接口隔离原则ISP接口隔离原则之所以出现这种问题,分析上面的代码,是因为接口定义的太大,不满足客户端依赖的接口尽可能地小这一原则,导致了代码的臃肿。这时我们可以遵循接口隔离原则对上面的代码进行重构,将上面的接口拆分成为更小的和更具体的接口这样老师和学生只需要知道他们感兴趣的方法。ISP接口隔离原则DIP依赖倒置原则(1)DIP依赖倒置原则(DependenceInversionPrinciple,DIP)定义依赖倒置原则的描述是:上层模块不应该依赖底层模块,他们都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。插头不应该依赖具体某种电线,它只需要有线并且能导电。DIP依赖倒置原则DIP依赖倒置原则(2)问题由来:类A直接依赖与类B,A改为依赖类C,则必须通过修改类A的代码来完成。这种场景下,类A一般是上层模块,负责复杂的业务逻辑,类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。DIP依赖倒置原则(3)解决方案:将类A改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接的与类B和类C发生联系,则会大大降低修改类A的几率。依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构,比以细节为基础搭建起来的架构要稳定的多。在Java中,抽象指的契约,是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好的规范和而不去设计任何具体的操作,把展示细节的任务交给他们的实现类去完成。依赖倒置原则的核心思想是面向接口编程。DIP依赖倒置原则运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物。DIP依赖倒置原则这样修改后,无论以后怎么修改Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表上层模块的Mother类讲负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
DIP依赖倒置原则采用依赖倒置原则给多人并行开发带来了极大的便利比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后则可以同时开工,互不影响,因为Mother类与Book类一点关系都没有。参与协作开发的人越多、项目越庞大,采用依赖倒置原则的意义就越重大。
DIP依赖倒置原则依赖倒置原则总结为四个方面:(1)控制反转:实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。DIP依赖倒置原则(2)依赖注入:依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过new的方式在类内部创建依赖类的对象而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或“注入”)给类来使用。DIP依赖倒置原则(3)依赖注入框架:我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。DIP依赖倒置原则(4)依赖反转原则:依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不需要依赖具体实现细节,具体实现细节依赖抽象。DIP依赖倒置原则SOLID原则做个总结1)单一职责原则是SOLID所有原则的基础和解决问题的思路。2)开闭原则是直接保障代码质量的原则,用来解决设计的脆弱性、僵化、难以阅读、难以复用等问题应用开闭原则的关键在于如何“抽象”。3)里氏替换原则通过确保子类和父类是“is
a"的关系,来帮助实现开闭原则。该原则的使用中,引申出面向对象角度的“is
a"是关于行为的,以及模型的正确性不是内在的,而是由它的客户程序来体现。SOLID原则做个总结4)接口隔离原则提供了一种方案,在不违反里氏替换原则的情况下,如何实现开闭原则。同时接口隔离的思想本身也体现了单一职责原则。5)依赖倒置原则是过程式设计与面向对象设计的分水岭,通过适当的抽象,让高层模块和底层模块同样地可复用和可测试。同时它也被用来指导接口隔离原则。01SOLID02其他原则03可复用模块的演化之路04提高代码复用性的方法KISS保持简单原则KISS原则即:KeepItSimpleandStupidKISS原则算是一个万金油类型的设计原则。它不仅经常用来指导软件开发,还经常用来指导更加广泛的系统设计、产品设计等,比如,冰箱、建筑、iPhone手机的设计等等。我们知道,代码的可读性和可维护性是衡量代码质量非常重要的两个标准。而KISS原则就是保持代码可读和可维护的重要手段。代码足够简单,也就意味着很容易读懂,bug比较难隐藏。即便出现bug,修复起来也比较简单。KISS保持简单原则对于如何写出满足KISS原则的代码,总结下面几条指导原则:不要使用同事可能不懂的技术来实现代码;不要重复造轮子,善于使用已经有的工具类库;不要过度优化。功能:检查输入的字符串ipAddress是否是合法的IP地址。(1)代码行数越少就越“简单”吗?(1)代码行数越少就越“简单”吗?(1)代码行数越少就越“简单”吗?1)第一种实现方式利用的是正则表达式,只用三行代码就把这个问题搞定了。它的代码行数最少,那是不是就最符合KISS原则呢?答案是否定的。虽然代码行数最少,看似最简单,实际上却很复杂。2)第二种实现方式使用了StringUtils类、Integer类提供的一些现成的工具函数,来处理IP地址字符串。从可读性上来说,第二种实现方式的代码逻辑更清晰、更好理解。所以,在这两种实现方式中,第二种实现方式更加“简单”,更加符合KISS原则。(2)如何写出满足KISS原则的代码?1)不要使用同事可能不懂的技术来实现代码。比如前面例子中的正则表达式,还有一些编程语言中过于高级的语法等。2)不要重复造轮子,要善于使用已经有的工具类库。经验证明,自己去实现这些类库,出bug的概率会更高,维护的成本也比较高。3)不要过度优化。不要过度使用一些奇技淫巧(比如,位运算代替算术运算、复杂的条件语句代替if-else、使用一些过于底层的函数等)来优化代码,牺牲代码的可读性。YAGNI不需要原则YAGNI原则的英文全称是:YouAin’tGonnaNeedIt。直译就是:你不会需要它。这条原则也算是万金油了。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。YAGNI原则跟KISS原则并非一回事儿。KISS原则讲的是“如何做”的问题(尽量保持简单)而YAGNI原则说的是“要不要做”的问题(当前不需要的就不要做)。YAGNI不需要原则例子之一是配置文件:目前系统暂时使用Redis来存储配置信息,以后可能使用到ZooKeeper。如果根据YAGNI原则,在未用到ZooKeeper之前,没有必要提前写好这部分代码。当然,我们还是要预留好扩展点,等到需要的时候,再去实现ZooKeeper这部分的代码。第二个例子是开发包:通常,项目中会依赖很多第三方的开发包,而有些开发者嫌每次添加依赖配置较麻烦,往往会添加一个大而全的依赖配置,而将一些项目中根本用不到的第三方类库也添加到项目中去。这样做是违反YAGNI设计原则的。DRY不要重复原则DRY原则即,Don’tRepeatYourself。DRY原则中文描述是:不要重复自己,将它应用在编程中,可以理解为:不要写重复的代码。三种代码重复的情况:实现逻辑重复、功能语义重复、代码执行重复。实现逻辑重复,但功能语义不重复的代码,并不违反DRY原则。实现逻辑不重复,但功能语义重复的代码,也算是违反DRY原则。而代码执行重复也算是违反DRY原则。01SOLID02其他原则03可复用模块的演化之路04提高代码复用性的方法可复用模块的演化之路我们常常遇到这样的场景,在项目A中开发了很多功能,这时在项目B中遇到要开发和A中类似的功能,这时会怎么做?很多人会不假思索的找到原来A项目中相关的功能代码,拷贝到B项目中,然后修改。几乎人人都这么做过,这样做有些什么问题呢?可复用模块的演化之路首先,由于代码常常混杂在一起,并不那么容易拷贝。特别是当代码不是自己写的,往往会一下拷贝了过多的无关内容到新项目中。其次,需要把功能中的非通用部分(只适用于项目A的部分)修改掉,以与当前项目匹配,这对于复杂的功能特别是通用逻辑与专用逻辑交错在一起的模块,也是挺费时的工作。可复用模块的演化之路这种代码难以复用的问题屡见不鲜,我们于是希望能把这些功能做成开箱即用的的可复用模块。所谓可复用模块,就是当你在别的项目中使用它时,不需要改它内部的任何东西,只需要按接口文档配置它的选项、实现它要求的接口,再调用它的接口就可以了。对照这个标准,看看你写的东西有多少可以复用的呢?可复用模块的演化之路那么是不是从一开始设计或编码的时候,就要以可复用的标准来设计函数或模块呢?如果你认为这样才是优秀的设计和良好的习惯,那么你一定要小心别犯过度设计的毛病。很多人读过设计模式,于是在项目中看到大量的factory/strategy/adapter等等。多数设计模式都是为了降低模块耦合,其实就是在设计可复用模块。可复用模块的演化之路然而模块是不是需要复用,更多的是看经验,而不是去套用模式。滥用模式必然导致过度设计。多数时候,一开始并不知道哪些东西需要复用,很多自以为是的拆分往往都被证明并不必要。过度设计和设计不足都会导致代码难以理解,而且过度设计还会在一开始耗费更多的时间。因此,不必试图从一开始就设计可复用模块,因为你常常并不知道哪些是真正需要被复用的。运用重构思维,按需创建可复用模块那么如何生成可复用的模块呢?很简单,当你发现某个功能和已有的一个功能很像,却又不完全一样,这就给你制作可复用模块的信号了;如果这种情况再出现第三次第四次,显然这块功能就是有复用价值的。这就是常见的lazy-loading机制的翻版,不在一开始设计可复用模块,而是在需要复用的时刻,通过重构代码来复用。而到底是在出现第一次复用机会时就重构,还是第二次或第三次时才重构,就得取决于重构的代价与获得好处的权衡了。如果难度不太大,风险也可控,我们一般都是鼓励尽早重构的。运用重构思维,按需创建可复用模块重构并不都是大刀阔斧的把项目大卸八块,更常见的是微重构,往往就是当你发现一块逻辑可以复用,就把它独立成了一个函数,然后函数多了,把它们分类放在一起,便于理解和修改,就形成了可复用模块的雏形。优秀的程序员会如同有洁癖般憎恨重复,遇到重复就会忍不住做代码优化,这其实就是以重构思维,动态地渐近地演化的过程,而这一过程往往就会生产出很多有用的轮子。假设一开始进入某H5应用,需要尝试自动登录到系统,面条式代码如下可复用模块如何演化这时并没有什么问题,运行很好。当写到登录页时,发现登录成功后也要做某些操作,和initApp里那些代码类似,为避免重复,这时就做个微重构,抽出一个共用函数handleLogin来:
可复用模块如何演化之后,又遇到一个逻辑,如果调用后台接口返回“未登录”错误时,可以先尝试一下自动登录,如果能登录成功就重新调用接
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026年沪教版适配高三生物开学摸底卷生命活动调节与遗传标准试卷第206套(含答案解析与可打印作答区)
- 投顾退款协议书
- 快速烹饪家常菜食谱方案
- 科技梦工场:激发对科技的兴趣的小学主题班会课件
- 消费者隐秘保护执行方案
- 2026届南京市高一数学月考质量检测QS01黑白可打印原创仿真卷B1第201套(含答案详解、评分标准与作答空间)
- 2026年雨城区事业编单位人员招聘考试参考题库及答案详解
- 2026年齐齐哈尔市碾子山区事业编单位人员招聘笔试备考试题及答案详解
- 2026年宁夏回族自治区中卫市事业编单位人员招聘考试参考试题及答案详解
- 2026年营口市西市区网格员招聘笔试参考试题及答案详解
- 2020初中物理自制教具-初中物理自制教具大全
- 加油站向周边商户风险告知书
- 预防依托咪酯的课件
- 中外城市建设史(全套课件595P)
- 八年级下册道德与法治全册教案
- MotionView-MotionSolve应用技巧与实例分析
- 2023年1月浙江省普通高中学业水平考试地理试题及答案
- GB/T 9797-2022金属及其他无机覆盖层镍、镍+铬、铜+镍和铜+镍+铬电镀层
- GB/T 4437.1-2015铝及铝合金热挤压管第1部分:无缝圆管
- GB/T 17688-1999土工合成材料聚氯乙烯土工膜
- GB/T 15037-2006葡萄酒
评论
0/150
提交评论