关于面向对象“多态”的理解

谈到多态肯定是和继承结合在一起的,本质是子类通过覆盖或重载(覆重)父类的方法,来使得对同一类对象同一方法的调用产生不同的结果。需要辨析的地方即:同一类对象指的是继承层级再上一层的对象,更加泛化。

实际采用多态的时候有四种情况:

  1. 父类有部分public的方法是不需要,也不允许子类覆重
  2. 父类有一些特别的方法是必须要子类去覆重的,在父类的方法其实是个空方法
  3. 父类有一些方法是可选覆重的,一旦覆重,则以子类为准
  4. 父类有一些方法即便被覆重,父类原方法还是要执行的

这四种情况在大多数支持多态的语言里未做很好的原生限制,在程序规模逐渐变大的时候,会给维护代码的程序员带来各种各样的坑。

父类有部分public的方法是不需要,也不允许子类覆重

  对于客户程序员来说,他们是有动机去覆重那些不需要覆重的方法的,比如需要在某个方法调用的时候做UserTrack,或者希望在方法调用之前做一些额外的事情,但是又找不到外面应该在哪儿做,于是就索性覆重一个了。这样做缺点在于使得一个对象引入了原本不属于它的业务逻辑。若在引入的这些额外逻辑中又对其他模块产生依赖,那么这个对象在将来的代码复用中就会面临一个艰难选择:

  • 是把这些不必要的逻辑删干净然后移过去?
  • 还是所以把依赖连带着这个对象一起copy过去?

前者太累,后者太蠢。

  如果是要针对原来的对象进行功能拓展,但拓展的时候发现是需要针对原本不允许覆重的函数进行操作,那么这时候就有理由怀疑父类当初是不是没有设计好了。

父类有一些特别的方法是必须要子类去覆重的,在父类的方法其实是个空方法

  这非常常见,由于逻辑的主要代码在父类中,若要跑完整个逻辑,则需要调用一些特定的方法来基于不同的子类获得不同的数据,这个特定的方法最终交由子类通过覆重来实现。如果不在父类里面写好这个方法吧,父类中的代码在执行逻辑的时候就调用不到。如果写了吧,一个空函数放在那儿十分难看。

  也有的时候客户程序员会不知道在派生之后需要覆重某个方法才能完成完整逻辑,因为空函数在那儿不会导致warning或error,只有在发现程序运行结果不对的时候,才会感觉哪儿有错。如果这时候程序员发现原来是有个方法没覆重,一定会拍桌子骂娘。

总结一下,其实就是代码不好看,以及有可能忘记覆重。

父类有一些方法是可选覆重的,一旦覆重,则以子类为准

  这是大多数面向对象语言默认行为。设计可选覆重的动机其中有一个就是可能要做拦截器,在每个父类方法调用时,先调一个willDoSomething(),然后调用完了再调一个didFinishedSomething(),由子类根据具体情况进行覆重。

  一般来说这类情况若正常应用,不会有什么问题,就算有问题,也是前面提到的容易使得一个对象引入原本不属于它的业务逻辑

父类有一些方法即便被覆重,父类原方法还是要执行的

  这个是经典的坑,尤其是交付给客户程序员的时候是以链接库的模式交付的。父类的方法是放在覆重函数的第一句调用呢还是放在最后一句调用?这是个值得深思的问题。更有甚者索性就直接忘记调用了,各种傻傻分不清楚。



解决方案:

面向接口编程(Interface Oriented Programming, IOP)是解决这类问题比较好的一种思路。

通过IOP,能做好两件事:

  1. 将子类与可能被子类引入的不相关逻辑剥离开来,提高了子类的可重用性,降低了迁移时可能的耦合。
  2. 接口实际上是子类头上的金箍,规范了子类哪些必须实现,哪些可选实现。那些不在接口定义的方法列表里的父类方法,事实上就是不建议覆重的方法。
<ManagerInterface> : APIName()        定义一个ManagerInterface接口,这个接口里面含有原本需要被覆重的方法。
<Interceptor> : willRun(), didRun()    再定义一个Interceptor的接口,它用来做拦截器。

BaseManager.child<ManagerInterface>    在BaseManager里添加一个属性child,这要求这个child必须要满足<ManagerInterface>这个接口,但BaseManager不需要满足<ManagerInterface>这个接口。

BaseManager.init() {
    ...

    self.child = self                       在init的时候把child设置成自己

    # 如果语言支持反射,那么我们可以这么写:
    if self.child implemented <ManagerInterface> {
        self.child = self
    }
    # 如上的写法就能够保证我们的子类能够基于这些接口有对应的实现

    self.interceptor = self  #interceptor可以是自己,也可在初始化的时候设为别的对象,这都可以根据需求不同而决定
    ...
}

BaseManager.run() {

    self.interceptor.willRun()
    ...

    apiName = self.child.APIName()      #原本是self.APIName(),然后这个方法是需要子类覆重的,现在可以改为self.child.APIName()了,就不需要覆重了。
    request with apiName

    ...
    self.interceptor.didRun()
}

通过引入这样面向接口编程的做法,就能相对好地解决上面提到的困境,下面我来解释一下是如何解决困境的

父类有部分public的方法是不需要,也不允许子类覆重:

  由于子类必须要遵从<ManagerInterface>,架构师可以跟客户程序员约定所有的public方法在一般情况下都是不需要覆重的。除非特殊需要,则可以覆重,其他情况都通过实现接口中定义的方法解决。由于这是接口方法,所以即便引入了原本不需要的逻辑,也能很容易将其剥离

父类有一些特别的方法是必须要子类去覆重的,在父类的方法其实是个空方法

  因为引入了child,父类不再需要摆一个空方法在那儿了,直接从child调用即可,因为child是实现了对应接口的,所以可以放心调用。空方法就消灭了。

父类有一些方法是可选覆重的,一旦覆重,则以子类为准

  可以通过在接口中设置哪些方法是必须要实现,哪些方法是可选实现来处理对应的问题。这本身倒不是缺陷,正是多态希望的样子

父类有一些方法即便被覆重,父类原方法还是要执行的

  由于通过接口规避了多态,那么这些其实是可以通过在接口中定义可选方法来实现的,由父类方法调用child可选方法,调用时机就可以由父类决定。这两个方法不必重名,因此也不存在多态时,不能分辨调用时机或是否需要调用父类方法的情况。



什么时候使用多态?

多态和继承紧密地结合在一起,我们假设父类是架构师去设计,子类由客户程序员去实现,那么这个问题实际上是这样的两个问题:

  1. 作为架构师,    何时要为多态提供接入点?
  2. 作为客户程序员,  何时要去覆重父类方法?

这本质上需要程序员针对对象建立一个角色的概念。

举例:

  当一个对象的主要业务是搜索,那么它在整个程序里面扮演的角色是搜索者的角色。在基于搜索派生出的业务中,会做一些跟搜索无关的事情,如搜索后进行排序,搜索前进行关键词分词(假设分词方案根据不同的派生类而不同)。那么这时候如果采用多态的方案,就是由子类覆重父类关于重排列表的方法,覆重分词方法。若在编写子类的程序员忘记这些必要的覆重或者覆重了不应该覆重的方法,就会进入上面提到的四个困境。

  所以此时需要提供一套接口,规范子类去做覆重,从而避免之前提到的四种困境:

Search : { search(), split(), resort()}

采用多态的方案:(不好)
Search -> ClothSearch : { [ search ], @split(), @resort() }

function search() {

    ...

    self.split()   #若子类没有覆重这个方法而父类提供的只是空方法,这里就容易出问题。若子类在覆重的时候引入了其他不相关逻辑,那么这个对象就显得不单纯,角色复杂了。

    ...

    self.resort()

    ...
}

采用IOP的方案:(推崇)
<SearchManager> : {split(), resort()}
Search<SearchManager> : { search(), assistant<SearchManager> }  #也可以是这样:Search : { search(), assistant<SearchManager> },这么做的话,则要求子类必须实现<SearchManager>

function search() {

    ...

    self.assistant.split()  # self.assistant可以就是self,也可以由初始化时候指定为其他对象,将来进行业务剥离的时候,只要将assistant里面的方法剥离或者将assistant在初始化时指定为其他对象也好。

    ...

    self.assistant.resort()

    ...
}

Search -> ClothSearch<SearchManager> : { [ Search ], split(), resort() }    # 由于子类被接口要求必须实现split()和resort()方法,因而规避了前文提到的风险,在剥离业务的时候也能非常方便。

外面使用对象时:ClothSearch.search()

如果示例中不同的子类对于search()方法有不同的实现,那么这个时候就适用多态。

Search : { search() }

ClothSearch : { [Search], @search() }

此时适用多态,外面使用对象时:ClothSearch.search()

总结是否决定应当使用多态的两个要素:

  • 如果引入多态之后导致对象角色不够单纯,那就不应当引入多态,如果引入多态之后依旧是单纯角色,那就可以引入多态
  • 如果要覆重的方法是角色业务的其中一个组成部分,例如split()和resort(),那么就最好不要用多态的方案,用IOP,因为在外界调用的时候其实并不需要通过多态来满足定制化的需求。

其实这是一个角色问题,越单纯的角色就越易维护。还有一个就是区分被覆重的方法是否需要被外界调用的问题。现在回到这一节前面提出的两个问题:

  何时引入接入点?

  何时采用覆重?

针对第一个问题:架构师一定要分清角色,在保证角色单纯的情况下可以引入多态。另外一点要考虑被覆重的方法是否需要被外界使用,还是只是父类运行时需要子类通过覆重提供中间数据的。如果是只要子类通过覆重提供中间数据的,一律应当采用IOP而不是多态

针对第二个问题,在必须要覆重的场合下就采取覆重的方案好了,主要是可覆重可不覆重的情况下,客户程序员主要还是要遵守:

  • 覆重的方法本身是跟逻辑密切相关的,不要在覆重方法里做跟这个方法本意不相关的事情
  • 如果要覆重一系列的方法,那么就要考虑角色问题和外界是否需要调用的问题,这些方法是不是这个对象的角色应当承担的任务

比如说不要在一个原本要跑步的函数里面去做吃饭的事情,如果真的要吃饭,父类又没有,实在不行的时候,就需要在覆重的方法里面启用IOP,在子类里面弥补架构师的设计缺陷。把这个不属于跑步的事情IOP出去,负责实现对应接口的可以是self,也可以是别人。只要不是强耦合地去覆重,这样在代码迁移的时候,由于IOP的存在,使得代码接收方也可以接受并实现对应的interface,从而不影响整体功能,又能提供迁移的灵活性。



关于多态的总结:

  多态在面向对象程序中的应用相当广泛,只要有继承的地方,或多或少都会用到多态。然而多态比起继承来,更容易被不明不白地使用,一切看起来都那么顺其自然。在客户程序员这边,一般是只要多态是可行方案的一种,到最后大部分都会采用多态的方案来解决问题。

  然而多态正如它名字中所示,它有非常大的潜在可能引入不属于对象初衷的逻辑,巨大的灵活性也导致客户程序员在面对问题的时候不太愿意采用其他相对更优的方案,比如IOP。在决定是否采用多态时,我们要有一个清晰的角色概念,做好角色细分,不要角色混乱。该是拦截器的,就给他制定一个拦截器接口,由另一个对象(逻辑上的另一个对象,当然也可以是自己)去实现接口里的方法集。不要让一个对象在逻辑上既是拦截器又是业务模块。这才方便未来的维护。另外也要注意被覆重方法的作用,如果只是单纯为了提供父类所需要的中间数据的,一律都用IOP,这是比直接采用多态更优的方案。

  IOP的好处不止文中写到的这些,它在其他场合也有非常好的应用,它最主要的好处就在于分离了定义和实现,并且能够带来更高的灵活性,灵活到既可以对语言过高的自由度有一个限制,也可以灵活到允许同一接口的不同实现能够合理地组合。在架构设计方面是个非常重要的思想。

参考文献:http://casatwy.com/tiao-chu-mian-xiang-dui-xiang-si-xiang-er-duo-tai.html

时间: 2024-08-29 09:45:59

关于面向对象“多态”的理解的相关文章

面向对象 多态的理解

/** * 面向对象的特征:多态性的使用 * * 1.多态性:可以理解为一个事物的多种形态. * * 2.广义上多态性的体现: * ①方法的重载 和 方法的重写 * ②子类对象的多态性 * * 3.狭义上多态性的体现:子类对象的多态性 * * 4.子类对象的多态性:父类的引用指向子类的对象.(子类的对象赋给父类的引用 * * 5.多态的应用场景:虚拟方法调用:编译时,认为调用的方法是父类的,但是当运行时,实际执行的是子类重写父类的方法 * * 说明:多态中方法的调用:编译看左边,运行看右边! *

Java中的继承、封装、多态的理解

Java中的继承.封装.多态 继承的理解: 1.继承是面向对象的三大特征之一,也是实现代码复用的重要手段.Java的继承具有单继承的特点,每个子类只有一个直接父类. 2.Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类称为父类(有的也称其为基类.超类),父类和子类的关系,是一种一般和特殊的关系.就像是水果和苹果的关系,苹果继承了水果,苹果是水果的子类,水果是苹果的父类,则苹果是一种特殊的水果. 3.Java使用extends作为继承的关键字,extends关键字在

09.面向对象多态的概述及其代码体现

09.01_面向对象(多态的概述及其代码体现) A:多态(polymorphic [,p?l?'m??f?k])概述 事物存在的多种形态 B:多态前提 a:要有继承关系. b:要有方法重写. c:要有父类引用指向子类对象. C:案例演示 代码体现多态 案例: class Demo1_Polymorphic { public static void main(String[] args) { Cat c = new Cat();//猫是一只猫 c.eat(); Animal a = new Cat

java中对于多态的理解

面向对象: 类:具体的对象抽象而来,可以抽象的表示一类具有相同特征和行为的对象 对象:是由类的实例化而来,对象是类的一个实例 java中关于多态的理解 打个比方 父亲person有行为这个方法,里面包括几个动作:吃饭,睡觉,走路 父亲有三个儿子,三个儿子都继承了父亲的行为方法,所以三个儿子都有吃饭,睡觉,走路这些动作,但是三个儿子又分别有自己的动作--大儿子A会弹吉他,二儿子B会唱歌,三儿子C会打鼓 ... 1.Person person = new A(); 不是父类对象指向子类引用而是父类引

C#面向对象-多态

面向对象的三大特性(封装,继承,多态)大多少人都应该知道.前两个的话比较容易理解.本文主要说一下面向对象中的多态. 什么是多态?不同的对象对同一操作,产生不同的执行结果.这就是多态.那么多态又包含了:重载,重写,虚方法,抽象方法. 1,重载什么叫重载?在同一个作用域的两个或多个方法函数名相同,参数列表不同的方法叫做重载.重载有三个特点:1.1:方法名必须相同.1.2:参数列表必须不同.1.3:返回值类型可以不相同. 列如: 1 public void Animal() 2 { 3 Console

关于java的this、继承、多态的理解

今天主要学习的内容是关于this.继承.多态的理解,并对其作出总结. 1.This 就目前来说,从个人的实验来看,this主要是对于一个类的对象的引用.它出现的位置主要是以它出现的形式决定的. ①.this(参数类型)这是在在构造方法中调用重写的构造方法. ②.作为某个类的对象来调用类中属性或方法.(用在某个类中的成员方法,因为某个类的对象就可以通过.来调用) 2.extends 继承之前已经了解了一些,接下来就是进一步的总结. ①.protected类型的变量是适合子类和父类之间的使用的,即使

转:OC面向对象—多态

OC面向对象—多态 一.基本概念 多态在代码中的体现,即为多种形态,必须要有继承,没有继承就没有多态. 在使用多态是,会进行动态检测,以调用真实的对象方法. 多态在代码中的体现即父类指针指向子类对象. Animal类的声明 Animal类的实现 Dog类继承自Animal类 Dog类的实现 测试程序: 二.使用注意 代码分析: Dog *d=[[Animal  alloc] init];   动物是一条狗?语义正确吗? NSString *str=[Dog  new];     狗是一个字符串?

面向过程和面向对象的本质理解

http://kiritor.blog.51cto.com/7400479/1226762/ 面向过程和面向对象的本质理解 面向过程就是分析出解决问题所需的步骤,面向对象则是把构成问题的事物分解成对象,抽象 出对象的目的并不在于完成某个步骤,而是描述其再整个解决问题的步骤中的行为. 面向过程的思维方式是分析综合,面向对象的思维方式是构造. 例如c语言解决问题时,一般是先定义数据结构,然后在构造算法.而是用Java面向对象求解时 则是先抽象出对象,构造一个"封闭"的环境,这个环境中有定义

OC面向对象-多态

OC面向对象-多态 一.基本概念 1>多态在代码中的体现,即为多种形态,没有继承就没有多态 2>在使用多态时,会进行动态检测,以调用真实的对象方法 3>多态在代码中的体现即父类指针指向子类对象 Animal类的声明 #import <Foundation/Foundation> //声明一个Animal类,该类继承自NSObject @interface Animal : NSObject //在Animal类中声明一个eat的对象方法 -(void) eat; @end A