对象 替换_Java架构师学习之路之面向对象设计原则2

本文详细介绍了面向对象设计的几个重要原则,包括里氏替换原则、依赖倒转原则、接口隔离原则。里氏替换原则强调子类可以替换父类在任何地方使用而不会产生错误或异常;依赖倒转原则提倡依赖抽象而非具体类,以提高系统的可扩展性;接口隔离原则建议将大的接口拆分成小接口,每个接口只包含客户端需要的方法。文章通过实例解析了这些原则在实际开发中的应用和重要性。
摘要由CSDN通过智能技术生成
3.⾥式替换原则

⾥⽒替换原则由2008年图灵奖得主、美国第⼀位计算机科学⼥博⼠Barbara Liskov教授和卡内基·梅隆⼤学Jeannette Wing教授于1994年提出。其严格表述如下:

如果对每⼀个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的⾏为没有变化,那么类型S是类型T的⼦类型。这个定义⽐较拗⼝且难以理解,因此我们⼀般使⽤它的另⼀个通俗版定义:

⾥⽒替换原则(Liskov Substitution Principle, LSP):所有引⽤基类(⽗类)的地⽅必须能透明地使⽤其⼦类的对象。

⾥⽒替换原则告诉我们,在软件中将⼀个基类对象替换成它的⼦类对象,程序将不会产⽣任何错误和异 常,反过来则不成⽴,如果⼀个软件实体使⽤的是⼀个⼦类对象的话,那么它不⼀定能够使⽤基类对象。例如:我喜欢动物,那我⼀定喜欢狗,因为狗是动物的⼦类;但是我喜欢狗,不能据此断定我喜欢   动物,因为我并不喜欢⽼⿏,虽然它也是动物。

例如有两个类,⼀个类为BaseClass,另⼀个是SubClass类,并且SubClass类是BaseClass类的⼦类,那么⼀个⽅法如果可以接受⼀个BaseClass类型的基类对象base的话,如:method1(base),  那么它必然可以接受⼀个BaseClass类型的⼦类对象sub,method1(sub)能够正常运⾏。反过来的换不成⽴,如⼀个⽅法method2接受BaseClass类型的⼦类对象sub为参数:method2(sub), 那么⼀般⽽⾔不可以有method2(base),除⾮是重载⽅法。

⾥⽒替换原则是实现开闭原则的重要⽅式之⼀,由于使⽤基类对象的地⽅都可以使⽤⼦类对象,因此在程序中尽量使⽤基类类型来对对象进⾏定义,⽽在运⾏时再确定其⼦类类型,⽤⼦类对象来替换⽗类对   象。

在使⽤⾥⽒替换原则时需要注意如下⼏个问题:

1)⼦类的所有⽅法必须在⽗类中声明,或⼦类必须实现⽗类中声明的所有⽅法。根据⾥⽒替换原则,为了保证系统的扩展性,在程序中通常使⽤⽗类来进⾏定义,如果⼀个⽅法只存在⼦类中,在⽗类中不提供相应的声明,则⽆法在以⽗类定义的对象中使⽤该⽅法。

2)我们在运⽤⾥⽒替换原则时,尽量把⽗类设计为抽象类或者接⼝,让⼦类继承⽗类或实现⽗接⼝,并实现在⽗类中声明的⽅法,运⾏时,⼦类实例替换⽗类实例,我们可以很⽅便地扩展系统的功能,同时⽆须修改原有⼦类的代码,增加新的功能可以通过增加⼀个新的⼦类来实现。⾥⽒替换原则是开闭原   则的具体实现⼿段之⼀。

3)Java语⾔中,在编译阶段,Java编译器会检查⼀个程序是否符合⾥⽒替换原则,这是⼀个与实现⽆关的、纯语法意义上的检查,但Java编译器的检查是有局限的。

示例:

在Sunny软件公司开发的CRM系统中,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供⼀个发送Email的功能,原始设计⽅案如图1所示:

3fa804788853a729299075c610f261a7.png

图1原始结构图

在对系统进⾏进⼀步分析后发现,⽆论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说 两个send()⽅法中的代码重复,⽽且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使⽤⾥⽒替换原则对其进⾏重构。

在本实例中,可以考虑增加⼀个新的抽象客户类Customer,⽽将CommonCustomer和VIPCustomer类作为其⼦类,邮件发送类EmailSender类针对抽象客户类Customer编程,根据⾥⽒代换原则,能够接受基类对象的地⽅必然能够接受⼦类对象,因此将EmailSender中的send()⽅法的参数类型改为Customer,如果需要增加新类型的客户,只需将其作为Customer类的⼦类即可。重构后的结构如图2所示:

c3930af80f9e349c5668173abfdbb86b.png

图2 重构后的结构图

⾥⽒替换原则是实现开闭原则的重要⽅式之⼀。在本实例中,在传递参数时使⽤基类对象,除此以外,在定义成员变量、定义局部变量、确定⽅法返回类型时都可使⽤⾥⽒替换原则。针对基类编程,在程序 运⾏时再确定具体⼦类。

4.依赖倒转原则

如果说开闭原则是⾯向对象设计的⽬标的话,那么依赖倒转原则就是⾯向对象设计的主要实现机制之⼀,它是系统抽象化的具体实现。依赖倒转原则是Robert C. Martin在1996年为“C++Reporter”所写的专栏Engineering Notebook的第三篇,后来加⼊到他在2002年出版的经典著作“Agile Software Development, Principles, Patterns, and Practices”⼀书中。依赖倒转原则定义如下:

依赖倒转原则(Dependency InversionPrinciple, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换⾔之,要针对接⼝编程,⽽不是针对实现编程。

依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引⽤层次⾼的抽象层类,即使⽤接⼝和抽象类进⾏变量类型声明、参数类型声明、⽅法返回类型声明,以及数据类型的转换等,⽽不要⽤具体类来做这些事情。为了确保该原则的应⽤,⼀个具体类应当只实现接⼝或抽象类中声明过的⽅法,⽽不要给出多余的⽅法,否则将⽆法调⽤到在⼦类中增加的新⽅法。

在引⼊抽象层后,系统将具有很好的灵活性,在程序中尽量使⽤抽象层进⾏编程,⽽将具体类写在配置⽂件中,这样⼀来,如果系统⾏为发⽣变化,只需要对抽象层进⾏扩展,并修改配置⽂件,⽽⽆须修改   原有系统的源代码,在不修改的情况下来扩展系统的功能,满⾜开闭原则的要求。

在实现依赖倒转原则时,我们需要针对抽象层编程,⽽将具体类的对象通过依赖注⼊(DependencyInjection, DI)的⽅式注⼊到其他对象中,依赖注⼊是指当⼀个对象要与其他对象发⽣依赖关系时,通过抽象来注⼊所依赖的对象。常⽤的注⼊⽅式有三种,分别是:构造注⼊,设值注⼊(Setter注⼊)和接⼝注⼊。构造注⼊是指通过构造函数来传⼊具体类的对象,设值注⼊是指通过Setter⽅法来传⼊具体类的对象,⽽接⼝注⼊是指通过在接⼝中声明的业务⽅法来传⼊具体类的对象。 这些⽅法在定义时使⽤的是抽象类型,在运⾏时再传⼊具体类型的对象,由⼦类对象来覆盖⽗类对象。

下⾯通过⼀个简单实例来加深对依赖倒转原则的理解:

Sunny软件公司开发⼈员在开发某CRM系统时发现:该系统经常需要将存储在TXT或Excel⽂件中的客户信息转存到数据库中,因此需要进⾏数据格式转换。在客户数据操作类中将调⽤数据格式转换类的⽅法   实现格式转换和数据库插⼊操作,初始设计⽅案结构如图1所示:

e4e162a033f37a6e9e3c94e8a03b8d8a.png

图1 初始设计⽅案结构图

在编码实现图1所示结构时,Sunny软件公司开发⼈员发现该设计⽅案存在⼀个⾮常严重的问题,由于每  次转换数据时数据来源不⼀定相同,因此需要更换数据转换类,如有时候需要将TXTDataConvertor改为ExcelDataConvertor,此时,需要修改CustomerDAO的源代码,⽽且在引⼊并使⽤新的数据转换类时    也不得不修改CustomerDAO的源代码,系统扩展性较差,违反了开闭原则,现需要对该⽅案进⾏重   构。

在本实例中,由于CustomerDAO针对具体数据转换类编程,因此在增加新的数据转换类或者更换数据 转换类时都不得不修改CustomerDAO的源代码。我们可以通过引⼊抽象数据转换类解决该问题,在引⼊抽象数据转换类DataConvertor之后,CustomerDAO针对抽象类DataConvertor编程,⽽将具体数据  转换类名存储在配置⽂件中,符合依赖倒转原则。根据⾥⽒代换原则,程序运⾏时,具体数据转换类对象将替换DataConvertor类型的对象,程序不会出现任何问题。更换具体数据转换类时⽆须修改源代码,只需要修改配置⽂件;如果需要增加新的具体数据转换类,只要将新增数据转换类作为DataConvertor的⼦类并修改配置⽂件即可,原有代码⽆须做任何修改,满⾜开闭原则。重构后的结构  如图2所示:

feddba85cbf5cb8ca2de499586c28c12.png

图2重构后的结构图

在上述重构过程中,我们使⽤了开闭原则、⾥⽒代换原则和依赖倒转原则,在⼤多数情况下,这三个设计原则会同时出现,开闭原则是⽬标,⾥⽒代换原则是基础,依赖倒转原则是⼿段,它们相辅相成,相互补充,⽬标⼀致,只是分析问题时所站⻆度不同⽽已。

5.接口分离原则

接⼝隔离原则定义如下:

接⼝隔离原则(Interface Segregation Principle, ISP):使⽤多个专⻔的接⼝,⽽不使⽤单⼀的总接⼝,即客户端不应该依赖那些它不需要的接⼝。

根据接⼝隔离原则,当⼀个接⼝太⼤时,我们需要将它分割成⼀些更细⼩的接⼝,使⽤该接⼝的客户端    仅需知道与之相关的⽅法即可。每⼀个接⼝应该承担⼀种相对独⽴的⻆⾊,不⼲不该⼲的事,该⼲的事都要⼲。这⾥的“接⼝”往往有两种不同的含义:⼀种是指⼀个类型所具有的⽅法特征的集合,仅仅是⼀种逻辑上的抽象;另外⼀种是指某种语⾔具体的“接⼝”定义,有严格的定义和结构,⽐如Java语⾔中的 interface。对于这两种不同的含义,ISP的表达⽅式以及含义都有所不同:

当把“接⼝”理解成⼀个类型所提供的所有⽅法特征的集合的时候,这就是⼀种逻辑上的概念,接⼝的 划分将直接带来类型的划分。可以把接⼝理解成⻆⾊,⼀个接⼝只能代表⼀个⻆⾊,每个⻆⾊都有它特   定的⼀个接⼝,此时,这个原则可以叫做“⻆⾊隔离原则”。

如果把“接⼝”理解成狭义的特定语⾔的接⼝,那么ISP表达的意思是指接⼝仅仅提供客户端需要的⾏  为,客户端不需要的⾏为则隐藏起来,应当为客户端提供尽可能⼩的单独的接⼝,⽽不要提供⼤的总接⼝。在⾯向对象编程语⾔中,实现⼀个接⼝就需要实现该接⼝中定义的所有⽅法,因此⼤的总接⼝使⽤起来不⼀定很⽅便,为了使接⼝的职责单⼀,需要将⼤接⼝中的⽅法根据其职责不同分别放在不同的⼩   接⼝中,以确保每个接⼝使⽤起来都较为⽅便,并都承担某⼀单⼀⻆⾊。接⼝应该尽量细化,同时接⼝    中的⽅法应该尽量少,每个接⼝中只包含⼀个客户端(如⼦模块或业务逻辑类)所需的⽅法即可,这种   机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接⼝。

下⾯通过⼀个简单实例来加深对接⼝隔离原则的理解:

Sunny软件公司开发⼈员针对某CRM系统的客户数据显示模块设计了如图1所示接⼝,其中⽅法dataRead()⽤于从⽂件中读取数据,⽅法transformToXML()⽤于将数据转换成XML格式,⽅法createChart()⽤于创建图表,⽅法displayChart()⽤于显示图表,⽅法createReport()⽤于创建⽂字报表,⽅法displayReport()⽤于显示⽂字报表。

09ae0dbab476e4b3a08ec7b3ba3f82b6.png

图1 初始设计⽅案结构图

在实际使⽤过程中发现该接⼝很不灵活,例如如果⼀个具体的数据显示类⽆须进⾏数据转换(源⽂件本   身就是XML格式),但由于实现了该接⼝,将不得不实现其中声明的transformToXML()⽅法(⾄少需要提供⼀个空实现);如果需要创建和显示图表,除了需实现与图表相关的⽅法外,还需要实现创建和显   示⽂字报表的⽅法,否则程序编译时将报错。现使⽤接⼝隔离原则对其进⾏重构。

在图1中,由于在接⼝CustomerDataDisplay中定义了太多⽅法,即该接⼝承担了太多职责,⼀⽅⾯导致该接⼝的实现类很庞⼤,在不同的实现类中都不得不实现接⼝中定义的所有⽅法,灵活性较差,如果 出现⼤量的空⽅法,将导致系统中产⽣⼤量的⽆⽤代码,影响代码质量;另⼀⽅⾯由于客户端针对⼤接⼝编程,将在⼀定程序上破坏程序的封装性,客户端看到了不应该看到的⽅法,没有为客户端定制接⼝。因此需要将该接⼝按照接⼝隔离原则和单⼀职责原则进⾏重构,将其中的⼀些⽅法封装在不同的⼩    接⼝中,确保每⼀个接⼝使⽤起来都较为⽅便,并都承担某⼀单⼀⻆⾊,每个接⼝中只包含⼀个客户端(如模块或类)所需的⽅法即可。

通过使⽤接⼝隔离原则,本实例重构后的结构如图2所示:

5a6b09c393096a2be74877ed7ba8b499.png

图2 重构后的结构图

在使⽤接⼝隔离原则时,我们需要注意控制接⼝的粒度,接⼝不能太⼩,如果太⼩会导致系统中接⼝泛滥,不利于维护;接⼝也不能太⼤,太⼤的接⼝将违背接⼝隔离原则,灵活性较差,使⽤起来很不⽅便。⼀般⽽⾔,接⼝中仅包含为某⼀类⽤户定制的⽅法即可,不应该强迫客户依赖于那些它们不⽤的⽅  法。

6.合成复⽤原则

合成复⽤原则⼜称为组合/聚合复⽤原则(Composition/Aggregate Reuse Principle, CARP),其定义如下:

合成复⽤原则(Composite Reuse Principle, CRP):尽量使⽤对象组合,⽽不是继承来达到复⽤的⽬的。

合成复⽤原则就是在⼀个新的对象⾥通过关联关系(包括组合关系和聚合关系)来使⽤⼀些已有的对象,使之成为新对象的⼀部分;新对象通过委派调⽤已有对象的⽅法达到复⽤功能的⽬的。简⾔之:复⽤时要尽量使⽤组合/聚合关系(关联关系),少⽤继承。

在⾯向对象设计中,可以通过两种⽅法在不同的环境中复⽤已有的设计和实现,即通过组合/聚合关系或 通过继承,但⾸先应该考虑使⽤组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,⼀个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使⽤继承时,需要严格遵循⾥⽒ 代换原则,有效使⽤继承会有助于对问题的理解,降低复杂度,⽽滥⽤继承反⽽会增加系统构建和维护   的难度以及系统的复杂度,因此需要慎重使⽤继承复⽤。

通过继承来进⾏复⽤的主要问题在于继承复⽤会破坏系统的封装性,因为继承会将基类的实现细节暴露 给⼦类,由于基类的内部细节通常对⼦类来说是可⻅的,所以这种复⽤⼜称“⽩箱”复⽤,如果基类发⽣改变,那么⼦类的实现也不得不发⽣改变;从基类继承⽽来的实现是静态的,不可能在运⾏时发⽣改变,没有⾜够的灵活性;⽽且继承只能在有限的环境中使⽤(如类没有声明为不能被继承)。

由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳⼊到新对象中,使之成为新对象的⼀部   分,因此新对象可以调⽤已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可⻅,所以这种复⽤⼜称为“⿊箱”复⽤,相对继承关系⽽⾔,其耦合度相对较低,成员对象的变化对新对象的影响不⼤,可以在新对象中根据实际需要有选择性地调⽤成员对象的操作;合成复⽤可以在运⾏时动态进⾏,新对象可以动态地引⽤与成员对象类型相同的其他对象。

⼀般⽽⾔,如果两个类之间是“Has-A”的关系应使⽤组合或聚合,如果是“Is-A”关系可使⽤继承。"Is-A"是     严格的分类学意义上的定义,意思是⼀个类是另⼀个类的"⼀种";⽽"Has-A"则不同,它表示某⼀个⻆⾊ 具有某⼀项责任。

案例讲解

Sunny软件公司开发⼈员在初期的CRM系统设计中,考虑到客户数量不多,系统采⽤MySQL作为数据库,与数据库操作有关的类如CustomerDAO类等都需要连接数据库,连接数据库的⽅法getConnection()封装在DBUtil类中,由于需要重⽤DBUtil类的getConnection()⽅法,设计⼈员将CustomerDAO作为DBUtil类的⼦类,初始设计⽅案结构如图1所示:

9572b6898ae64c46524269113c9f4875.png

图1 初始设计⽅案结构图

随着客户数量的增加,系统决定升级为Oracle数据库,因此需要增加⼀个新的OracleDBUtil类来连接Oracle数据库,由于在初始设计⽅案中CustomerDAO和DBUtil之间是继承关系,因此在更换数据库连接⽅式时需要修改CustomerDAO类的源代码,将CustomerDAO作为OracleDBUtil的⼦类,这将违反开  闭原则。【当然也可以修改DBUtil类的源代码,同样会违反开闭原则。】现使⽤合成复⽤原则对其进⾏重构。

根据合成复⽤原则,我们在实现复⽤时应该多⽤关联,少⽤继承。因此在本实例中我们可以使⽤关联复⽤来取代继承复⽤,重构后的结构如图2所示:

3f2582c93ec003eb71bdcf1aa5566b1f.png

图2 重构后的结构图

在图2中,CustomerDAO和DBUtil之间的关系由继承关系变为关联关系,采⽤依赖注⼊的⽅式将DBUtil 对象注⼊到CustomerDAO中,可以使⽤构造注⼊,也可以使⽤Setter注⼊。如果需要对DBUtil的功能进⾏扩展,可以通过其⼦类来实现,如通过⼦类OracleDBUtil来连接Oracle数据库。由于CustomerDAO    针对DBUtil编程,根据⾥⽒代换原则,DBUtil⼦类的对象可以覆盖DBUtil对象,只需在CustomerDAO中   注⼊⼦类对象即可使⽤⼦类所扩展的⽅法。例如在CustomerDAO中注⼊OracleDBUtil对象,即可实现Oracle数据库连接,原有代码⽆须进⾏修改,⽽且还可以很灵活地增加新的数据库连接⽅式。

7.迪米特原则

迪⽶特法则来⾃于1987年美国东北⼤学(Northeastern University)⼀个名为“Demeter”的研究项⽬。迪⽶特法则⼜称为最少知识原则(LeastKnowledge Principle, LKP),其定义如下:

迪⽶特法则(Law ofDemeter, LoD):⼀个软件实体应当尽可能少地与其他实体发⽣相互作⽤。

如果⼀个系统符合迪⽶特法则,那么当其中某⼀个模块发⽣修改时,就会尽量少地影响其他模块,扩展 会相对容易,这是对软件实体之间通信的限制,迪⽶特法则要求限制软件实体之间通信的宽度和深度。  迪⽶特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。

迪⽶特法则还有⼏种定义形式,包括:不要和“陌⽣⼈”说话、只与你的直接朋友通信等,在迪⽶特法则   中,对于⼀个对象,其朋友包括以下⼏类:

当前对象本身(this);

以参数形式传⼊到当前对象⽅法中的对象;

当前对象的成员对象;

如果当前对象的成员对象是⼀个集合,那么集合中的元素也都是朋友;

当前对象所创建的对象。

任何⼀个对象,如果满⾜上⾯的条件之⼀,就是当前对象的“朋友”,否则就是“陌⽣⼈”。在应⽤迪⽶特法则时,⼀个对象只能与直接朋友发⽣交互,不要与“陌⽣⼈”发⽣直接交互,这样做可以降低系统的耦合度,⼀个对象的改变不会给太多其他对象带来影响。

迪⽶特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发⽣任何直接的相互作⽤,如果其中的⼀个对象需要调⽤另⼀个对象的某个⽅法的话,可以通过第三者转发这个调⽤。简⾔之,就是通过引⼊⼀个合理的第三者来降低现有对象之间的耦合度。

在将迪⽶特法则运⽤到系统设计中时,要注意下⾯的⼏点:在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复⽤,⼀个处在松耦合中的类⼀旦被修改,不会对关联的类造成太⼤波及;在类的结构设计上,每⼀个类都应当尽量降低其成员变量和成员函数的访问权限;在类的设计上,只要有可能,⼀个类型应当设计成不变类;在对其他类的引⽤上,⼀个对象对其他对象的引⽤应当 降到最低。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值