你可能还记得,Liskov Substitution原则是关于承诺和合同的。但究竟是什么承诺?这是关于保证子类型的安全性。这意味着子类型必须保证有人可以从超类型中合理地推断出来。它必须具有传递关系。在数学中,我们说a a,b,c∈X,如果是aRb和bRc,那么aRc。在面向对象的编程中,子类化(现在,在本文中关于子类化,我指的是子类型)意味着子类型,但不是正确的方式。我们必须确保我们不会破坏我们继承的超类承诺,并且我们不依赖于可能因我们无法控制而改变的东西。如果它发生变化,其他对象可能会受到影响(因为它们是不可变的)。实际上,子类化甚至可能是项目中的错误源。
为什么安全子类型
实际上,子类化实际上是一种特殊类型的子类型,它允许子类型重用超类型实现(目的是防止在超类型中进行小修改的所有内容的重新实现)。我们可以说子类化是子类型,但不是子类型是子类化。子类化带来两件事:子类型(多态)和代码重用。子类型具有最高的冲击力; 超类的公共级别和受保护级别的任何更改都将影响其子类。子类型不是Is-A关系,有时它是Is-A关系。实际上,子类型是代码重用的过程技术和动态多态的工具。
子类化仅涉及实现的内容和方式 - 而不是承诺的内容。如果你违反了基类的承诺,会发生什么?有没有保证可以确保它兼容?即使您的编译器也不会理解这个错误,您将面临代码中的错误,例如:
class DoubleEndedQueue {
void insertFront(Node node) {
// ...
// insert node infornt of queue
}
void insertEnd(Node node) {
// ...
// insert a node at the end of queue
}
void deleteFront(Node node) {
// ...
// delete the node infront of queue
}
void deleteEnd(Node node) {
// ...
// delete the node at the end of queue
}
}
class Stack extends DoubleEndedQueue {
// ...
}
如果类Stack想要以代码重用为目的使用Subtyping,它可能会继承违反其主体的行为,例如 insertFront。我们还看到另一个代码示例:
public class DataHashSet extends HashSet {
private int addCount = 0;
public function DataHashSet(Collection collection) {
super(collection);
}
public function DataHashSet(int initCapacity, float loadFactor) {
super(initCapacity, loadFactor);
}
public boolean function add(Object object) {
addCount++;
return super.add(object);
}
public boolean function addAll(Collection collection) {
addCount += collection.size();
return super.addAll(collection);
}
public int function getAddCount() {
return addCount;
}
}
我只是重新实现 HashSet随着 DataHashSet类以便跟踪插入。事实上, DataHashSet继承并且是。的子类型 HashSet。我们可以,而不是HashSet,只是通过 DataHashSet(如果可能的话,用Java)。另外,我确实覆盖了基类的一些方法。Liskov Substitution原则是否合法?由于我没有对基类的行为进行任何更改,只是添加一个轨道来插入操作,这似乎是完全合法的。但是,我认为这显然是冒险的子类型和错误的代码。首先,我们应该看看添加的方法究竟会做什么。它向相关属性添加一个单元并调用父类方法。这段代码有溜溜球的问题。看着那(这 addAll方法。首先,它将收集大小添加到相关属性,然后调用addAll在父母,但父母究竟是什么 addAll做?它会多次调用add方法(循环遍历集合),但是会调用哪个add?它会在当前类或父类中添加吗?第一个是正确的。因此,计数的大小将被添加两次。当你调用addAll 时,这将发生一次;当父类调用子类中的add方法时,这将发生; 这就是我们称之为溜溜球问题的原因。这是另一个证明子类型存在风险的例子,想象一下这个例子情景:
class A {
void foo(){
...
this.bar();
...
}
void bar(){
...
}
}
class B extends A {
//override bar
void bar(){
...
}
}
class C {
void bazz(){
B b = new B();
// which bar would be called?
B.foo();
}
}
如你所见,当我们调用bazz方法时,会调用哪个栏?当然,第二个--B级中的栏将被调用。但问题是什么呢?问题是A类中的foo方法对B类中bar方法的重写一无所知。然后,你的不变量可能会被违反并且它会破坏封装,因为foo可能会期望bar方法的唯一行为自己的班级,而不是被覆盖的东西。这个问题也称为脆弱的基类问题。
实现子类型的一个更关键的问题是耦合 - 程序的一部分对另一部分(紧耦合)的不合需要的依赖。全局变量提供了强耦合导致问题的经典示例。例如,如果更改全局变量的类型,则使用该变量的所有函数(即耦合到变量)可能会受到影响,因此必须检查,修改和重新测试所有这些代码。而且,使用变量的所有函数都通过变量相互耦合。也就是说,如果变量的值在尴尬时改变,则一个函数可能会错误地影响另一个函数的行为。这个问题在多线程程序中特别棘手。
如何安全地进行子类?
子类最安全的方法是避免子类型化。如果您的类不是为子类设计的,它会通过将构造函数设为私有或向您的类添加final关键字来阻止子类化。但是如果我们想在代码中使用子类,那么我们可以创建一个包装类作为子类型的代码重用替代方法。在这种情况下,我们需要有关于代码重用的模块化推理。这涉及在不了解每个实现细节的情况下重用代码的能力。有几种方法可以处理 - 我将在这里介绍其中的一些方法。一种方法是通过将可覆盖的功能限制在少数受保护的方法中来避免可覆盖功能的自我使用。例如,使用语言机制或规范来防止覆盖其他方法。在里面DataHashSetclass,避免addAll 方法调用add。此外,我们可以通过避免在类中使用可覆盖的方法来最小化对覆盖的其他功能的直接影响。让我用前面的例子说明一下:
class A {
void foo(){
...
this.insideBar();
...
}
void insideBar(){
...
}
void bar(){
this.insideBar();
}
}
class B extends A {
//override bar
void bar(){
...
}
}
class C {
void bazz(){
B b = new B();
B.foo();
}
}
正如您在上面的代码中看到的,我只是添加了 insideBar方法,以防止由于覆盖导致的不必要的变化,并解决问题。大多数情况下,创建包装类是降低子类化风险的好方法。我的意思是,比Subtyping更喜欢组合或委托
有些地方我们必须不惜一切代价避免分类。如果我们有多种方法来进行子类型化,那么最好做委托,或者当父类中有一些无用的方法时,这意味着不需要使用扩展类(Liskov替换原则)。课程的故事是一样的; 我的意思是在重用一个类时,每当使用共享类时都不应该使用它。
超过子类型的委派
子类型模式包括一个定义实例形状的类,它充当模板。每个实例都具有类及其属性的行为,但不具有值,因为类(及其子类)的所有实例都使用存储在类中的属性的定义,并且对类中存储的属性的任何更改都将更改所有实例。
所有超类和子类都组合在一个实例中; 有线性链向上(在Java中,不是像C ++这样的语言,你可以有多个子类型)。但是,值存储在实例中,而不是存储在类中,并且不会共享它们。在子类型中,实例是独立的; 更改一个实例的状态(值)不会影响任何其他实例,并且每个实例都有自己的父对象。
委托意味着对象使用另一个对象进行方法的调用。在委托实例中,没有用于共享属性或行为的类,通常它们调用没有类的实例。对于每个类重用,您可以使用一个实例; 想象你有一个区域计算器类,它接受不同形状的区域并返回确切的区域。您只需要创建一个区域计算器对象并调用不同的区域类型类。但是在子类型中,对于每种类型的区域,您必须创建一个具有自己父级的sperate对象。
如果对象将方法或变量委托给原型,那么对这些属性或其值的任何更改都将影响对象和原型。以这种方式,委托层次结构中的对象可以彼此依赖。在委托中,您需要启动多个对象,对象可以来自不同的类型和组(与子类型相反)。此外,您需要以正确的方式组合实例以满足类需求。
此外,没有父类,因此您无法直接使用调用的对象属性。在子类型中,子类可以使用父属性或方法而无需实现,但在委派中,您必须定义一个方法来访问该属性或方法。
在委托中,重用类可以重用多个重用类。您只需要指向这些类的链接。所有这些都在同一个例子中。但是在zubtyping中,重用类是其他重用类的子类(直到下行线性链)。让我们解决问题吧DataHashSet与代表团的方法:
public class DataHashSet implements Set {
private int addCount = 0;
private Set set;
public
function DataHashSet(Set set) {
This.set = set;
}
public boolean
function add(Object object) {
addCount++;
return This.set.add(object);
}
public boolean
function addAll(Collection collection) {
addCount += collection.size();
return This.set.addAll(collection);
}
public int
function getAddCount() {
return addCount;
}
}
什么是骨骼实施?
骨架实现提供了子类型的优点而不会丧失灵活性。对于每个接口,您提供一个实现接口的抽象类,并保留未指定的基本方法。这意味着它们将方法保留为抽象并由子类实现,并且它定义了非常非原始的方法。然后,希望使用该接口的开发人员将实现该接口,然后他们通常使用骨架实现。它不如使用包装类灵活,例如组合或委托。为了使其更灵活,您可以使用一个包装类,该类将调用委托给骨架实现的匿名子类的对象。
另外本人从事在线教育多年,将自己的资料整合建了一个公众号,对于有兴趣一起交流学习java可以微信搜索:“程序员文明”,里面有大神会给予解答,也会有许多的资源可以供大家学习分享,欢迎大家前来一起学习进步!
网友评论