第五章 代码的可复用性——复用性的结构

1.行为子类型与LSP(Liskov Substitution Principle)

行为子类型:

子类型多态:客户端可用统一的方式处理不同类型的对象。

栗子!


在java中编译器关于这部分有以下规则(静态检查实现):

  • 子类型可以增加方法,但不可删。
  • 子类型需要实现抽象类型中的所有方法
  • 子类型重写的方法中必须有相同或子类型的返回值
    这里有点拗口解释一下,比如重写方法中超类返回值为Animal,那么子类型可以是Animal或者Animal的子类如Cat,CodeDog等……
  • 子类型中重写的方法必须使用相同类型的参数
  • 子类型重写的方法不能抛出额外的异常

另外LSP也适用于指定的方法:

  • 更强的不变量
  • 更弱的前置条件
  • 更强的后置条件

荔枝1:



荔枝2:



LSP是一种对子类型关系的特殊限制,称为强行为子类型化:

其中包含以下几种限制:

  1. 前置条件不能强化
  2. 后置条件不能弱化
  3. 不变量还需保持
  4. 子类型方法参数:逆变
  5. 子类型方法的返回值:协变
  6. 异常的类型:协变(这部分会放到第七章来进行详细说明)

协变:父类型到子类型:越来越具体的spec,返回值类型:不变或者变的更加具体,异常类型也是如此。

这么说太晦涩了。简而言之就是上面讲的内容。对于子类重写方法返回值要么相同,要么返回他的子类型。抛出的异常同样肯定是父类中抛出异常的子类,就这么点干货==

嘛,还是来看两个荔枝吧:


子类型重写方法,返回值变成了父类型方法返回值的子类型,任何对象都是Object子类型呦……


第七章我们会讲到,所有抛出的异常都会继承Throwable这个超类。怎么样很简单吧。


逆变(也叫反协变):父类型到子类型:越来越具体的spec(这点与协变相同,重点在后面),对于参数类型,要采取相反的变化,要么不变,要么越来越抽象。

这点也解释一下吧,其实干货也少的可怜,对于参数类型,如果满足逆变,那么子类型方法中的参数类型要么与父类型相等,要么是父类型参数的抽象类。

形象点说吧,如果有个CodeDog对象继承person,他可以码代码,还能找个女朋友,那么我们就把这个找女朋友抽象为一个方法,传入一个person类型的参数,这时候我们传入一个person类型girl,那么显然方法可以运行呀。那么我们传入一个CodeDog类型的codeQueen,显然也可以呀,不都是妹子嘛,管她是干嘛的嘛……然而你要传入一个Dog类型的cuteDog,你觉得这个方法会执行吗?说行的怕不是个变态。

上面的栗子中CodeDog是Person子类型,而Dog不是所以不行,这就是反协变,怎么样也很简单吧。

看个例子吧(哪有我的变态形象)


怎么样是不是感觉自己理解本质了?java太简单了吧!然而却想多了……

这种反协变看起来很符合逻辑,但是!!!!!!!!!!!!!!!!!!!

这种反协变java中不允许!!!!!!!!!!!(因为他会让重载规则复杂化)

另外再次提醒一下,如果你在编译器中打出上面栗子的那个代码发现没有发生静态检查报错,看看是否在重写方法前加上了@Override,没加的话编译器会按重载规则来判断……


总结:


另外说一下数组是协变的,也就是说:


最后一步不会发生静态检查报错,其会在动态检查发生。


然后说一下泛型中的LSP:

注意,泛型是类型不变的,什么意思?举个例子:

ArrayList<String>是List<String>子类型,List<String>不是List<Object>子类型。

这是为什么?在此需要提的是,编译完成后,编译器会丢弃类型参数的类型信息,因此这种类型的信息在运行中是不可用的,这个过程称为类型擦除(type erasure),所以泛型不是协变的。

类型擦除的一个例子:


其实根本就没有什么泛型这种类型,所谓泛型也就是存在java内部的一种机制,在将其在编译过程中会将泛型转化为用户需要的类型,如果您足够屌可以去看一下编译后的汇编指令,就会发现在定义的时候泛型就已经被转化为了用户所需的类型了。

关于这部分记住这张图就好啦:



然后说一下泛型的通配符:

很简单的小东西,我是这么理解的,通配符顾名思义,就是什么都可以替代,你可以在用法上将?理解为Object类型,但是和它有本质不同,object类型是一切对象的父类,然而?可以有<? super XXX>或者<? extends XXX>用法来代表其取代类在继承等方面的性质。


委派和组合(Delegation and Composition):

首先我们先来了解一下java中的比较器(Comparator):

栗子:



如果ADT需要比较大小,或者要放到Arrays或Collections里进行排序,可以实现Comparator接口,并且重写里面的compare方法即可。



当然还有一种方法,就是让你的ADT去实现Comparable接口,然后重写其中的compareTo方法。其与上者的区别在于他不需要构建新的Comparator类,比较代码放在ADT内部。



好,现在来谈一下委派(Delegation):

其定义是一个对象请求另一个对象的功能。

委派是复用的一种常见的形式,其主要分为两种:

  • 显式委派:通过传递一个对象给另一个对象实现。
  • 隐式委派:通过方法内部的成员变量来实现。



概念和了解起来都不难,码点代码的人都知道这些事情,先给出一个流程图来说明:


下面来说一下委派与继承的区别:

继承可以理解为通过新操作来扩展基类或者覆盖父类操作,而委派则是调用一个对象的操作发送给另一个对象。

都是作为复用的手段,没有决定的优劣,根据具体情况决定使用哪个,很多设计模式都是使用委派和继承的组合。


注意:如果只需要复用父类的一小部分方法,可以通过委派机制实现。一个类不需要继承另一个的全部方法,可以通过委派机制调用部分方法。




复合继承原则:又称复合复用原则(Composite Reuse Principle)CRP原则。类应该通过其组合(通过包含实现所需功能的其他类的实例)实现多态行为和代码重用,而不是从基类或父类继承。

委托可以理解为发生在object层面上,而继承则发生在class层面上。

关于委派和继承的各种组合关系这里涉及到很多java的设计模式,由于篇幅等问题,这里不再赘余,如果各位想要了解,之后会有专门介绍java设计模式的章节(本章末)


其中可将委派的用法分为以下几种:

  1. Dependency:临时性的委派

    可以理解为委派的对象是作为一个参数传进方法中,其只在该方法内代码域有效,是临时的。

  2. Association:永久性的delegation

    可看出委派对象为内部的一个属性,具有永久性。

  3. Composition:更强的delegation

    这个相比于上着,在使用者出生时就有一个专属的委派对象,可以理解为委派对象在抽象角度来讲是使用者的一部分。

  4. Aggregation:

    或许你会问这不就是第三个Composition嘛?!其实是不一样的,第二个中当拥有对象被破坏时,委派对象也会被破坏,而第四个聚合中对象存在于另一个之外,如果拥有者被破坏,被包含者也不会被破坏。第三个可以理解为专属,而第四个并不是。


最后讲一下框架:

其分为两种:

  1. 白盒框架:
    通过子类化和重写方法拓展功能、通常采用模板方法作为设计模式,子类会给出主函数,但是会给框架加以控制。
  2. 黑盒框架:
    需要通过实现插件接口的方法进行功能拓展,主要采用的策略模式与观察者模式作为设计模式,插件加载机制加载插件并对框架进行控制。

猜你喜欢

转载自blog.csdn.net/qq_37549266/article/details/80713378