第五章 面向可复用的构造

Programming for reuse

 白盒复用:源代码可见,可修改和扩展


 黑盒复用:源代码不可见,不能修改

Behavioral subtyping

子类型需要实现抽象 类型中的所有未实现方法
子类型中重写的方法 必须有相同或子类型的返回值
子类型中重写的 方法必须使用同样类型的参数
子类型中重写的方 法不能抛出额外的异常

– Same or stronger invariants 更强的不变量
 – Same or weaker preconditions 更弱的前置条件 
– Same or stronger postconditions 更强的后置条件

Preconditions cannot be strengthened in a subtype. 前置条件不能强化 
– Postconditions cannot be weakened in a subtype.    
 后置条件不能弱化 – Invariants of the supertype must be preserved in a subtype. 
 不变量要保持 – Contravariance of method arguments in a subtype 
子类型方法参数:逆变 – Covariance of return types in a subtype. 
子类型方法的返回值:协变

Covariance (协变)

父类型子类型:越来越具体specific 返回值类型:不变或变得更具体 异常的类型:也是如此。

Contravariance (反协变、逆变)

父类型子类型:越来越具体specific 参数类型:要相反的变化,要不变或越来 越抽象

Consider LSP for generics 泛型中的LSP

扫描二维码关注公众号,回复: 1663515 查看本文章


Box<Integer> is not a subtype of Box<Number> even though Integer is a subtype of Number
Given two concrete types A and B (for example, Number and Integer), MyClass<A> has no relationship to MyClass<B>, regardless of whether or not A and B are related. The common parent of MyClass<A> and MyClass<B> is Object

1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>.
  
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据. 

2. 固定上边界的通配符(Upper Bounded Wildcards):

  使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据. 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类. 

3. 固定下边界的通配符(Lower Bounded Wildcards):
  使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据. 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界. 注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界.

 List<Number> is a subtype of List<? extends Object> 

 List<Object> is a subtype of List<? super String>

Interface Comparator<T>


Interface Comparable<T>

与使用Comparator的区别:不需要构建新的Comparator类,比较代 码放在ADT内部。


 Delegation and Composition

class level.“委托” 发生在objet层面,而“继承”发生在class层面

委派的几种类型归纳

  • Use (A use B)

  • Composition/aggregation (A owns B)

  • Association (A has B

Dependency(依赖): 临时性的delegation

  • 在这种关系中,一个类使用另一个类而不将其作为一个属性。
  • 两类之间的这种关系称为“uses-a”关系。例如,它可以是一个参数,或者在一个方法中本地使用,参考下面的代码:

首先新建一个课程类:

public class Course {}
  • 1

在课表类总使用课程类:

public class CourseSchedule {
    List<Course> courses = new ArrayList<>();
    public void add (Course c) {
        courses.add(c);
    }
    public void remove (Course c) {
        courses.remove(c);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在这里,并没有将Course类作为CourseSchedule类的属性来使用,而是作为迭代器中的元素和方法中的参数来使用。

Association(关联): 永久性的delegation

  • 关联是类之间的持久关系,允许一个对象实例让另一个对象实例代表它自己做其他事。
  • 这种关系属于has-a的关系,是结构化的,因为它指定了一种对象与另一种对象相连接,并且不代表行为,即不在该类的方法中使用另一个类的方法,只是简单的将不同的对象连接起来。

下面展示一下关联的代码:

class Teacher {
    private Student [] students; 
}
  • 1
  • 2
  • 3
class Student {
    private Teacher teacher;
    Course[] selectedCourses;
}
  • 1
  • 2
  • 3
  • 4

可以看到,在两个类中(Student和Teacher)互相都有彼此的实例,而且没有使用继承,就直接将这几个不同的类相连接,这就是利用了Association方式。

Composition: 更强的delegation

  • 组合是将简单对象或数据类型组合成更复杂的方法的一种方法。
  • 这种关系是a-part-of关系,一个类有另一个属性或实例变量——实现了一个对象包含另一个对象。

let`s show 代码:

class Heart {}
  • 1
class Person { 
    private Heart heart = new Heart();
    public void operation () { 
        heart.operation(); 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 这种方式理解起来就很简单了,直接在该类中实例化一个其他类,然后该调用方法调用方法,对这个实例想怎么用怎么用,十分灵活。
  • 不过需要注意的是:
  • 这里的实例是private的,也就是说,外界访问不到,这样的话,更改其值只能在该方法中;而且每次创建该类的对象时,就已经创建好这个类中的实例;也就是说一旦创建好该类的对象,其中的属性指向便已经创建好。

Aggregation

  • 聚集:对象存在于另一个之外,是在外部创建的,所以它作为一个参数传递给构造函数。
  • 这种关系是has-a的关系,区别于

    让我们看一个这个例子的代码:

class Student {}
  • 1
class Course { 
    private Student[] students; 
    public addStudent (Student s) { 
        studtents.append(s); 
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

可以看到,在这里,内部的属性是可以在外部指定的,而不是完全依赖该类。

Composition vs. Aggregation

组合和聚集是最常用的两种delegation方式,可以说,其中的使用包括了依赖和关联方式,并在其上做了进一步的扩展。二者很相似,但又有很多不同之处,这里举一个例子看一下二者的最大不同之处:

public class WebServer { 
    private HttpListener listener; 
    private RequestProcessor processor; 
    public WebServer(HttpListener listener, RequestProcessor processor) {
        this.listener = listener;
        this.processor = processor;
    } 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
public class WebServer { 
    private HttpListener listener; 
    private RequestProcessor processor; 
    public WebServer() { 
        this.listener = new HttpListener(80); 
        this.processor = new RequestProcessor(“/www/root”);
    } 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

可以看到,第一个代码是聚集的方式,第二个代码是组合的方式,这是为什么呢?

  • 在组合中,当拥有的对象被销毁时,所包含的对象也是如此。
    比如:
    一所大学拥有不同的部门,每个部门都有许多教授,因为是大学创建了这些部门,所以如果大学关闭,这些部门将不复存在,但这些部门的教授将继续存在。

  • 但是在聚集中,这并不一定是正确的:
    大学可以被看作是部门的组成,而院系则是教授的集合。一个教授可以在一个以上的部门工作,但是一个部门不能成为一所大学的一部分,部门是在大学外创建的,故大学倒闭,部门还在。

合成(Composition)和聚合(Aggregation)都是关联(Association)的特殊种类。聚合表示整体和部分的关系,表示"拥有";合成则是一种更强的"拥有",部分和整体的生命周期一样。合成的新的对象完全支配其组成部分,包括它们的创建和销毁等。一个合成关系的成分对象是不能与另一个合成关系共享的。

在面向对象设计中,有两种基本的办法可以实现复用:

第一种是通过合成/聚合,即合成复用原则,含义是指,尽量使用合成/聚合,而不是使用继承。

第二种就是通过继承。

要正确地选择合成/复用和继承的方法是,只有当以下的条件全部被满足时,才应当使用继承关系:

子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分"Has-A"和"Is-A"。只有"Is-A"关系才符合继承关系,"Has-A"关系应当用聚合来描述。

永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。

子类具有扩展超类的责任,而不是具有置换掉(override)或注销掉(Nullify)超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。

只有在分类学角度上有意义时,才可以使用继承。不要从工具类继承。

错误的使用继承而不是合成/聚合的一个常见原因是错误地把"Has-A"当成了"Is-A"。"Is-A"代表一个类是另外一个类的一种;"Has-A"代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。

白盒框架的原理与实现

白盒框架是基于面向对象的继承机制。之所以说是白盒框架,是因为在这种框架中,父类的方法对子类而言是可见的。子类可以通过继承或重写父类的方法来实现更具体的方法。

虽然层次结构比较清晰,但是这种方式也有其局限性,父类中的方法子类一定拥有,要么继承,要么重写,不可能存在子类中不存在的方法而在父类中存在。

软件构造课程中有关白盒框架的例子:

public abstract class PrintOnScreen {
    public void print() { 
        JFrame frame = new JFrame(); 
        JOptionPane.showMessageDialog(frame, textToShow());
        frame.dispose();
    } 
    protected abstract String textToShow(); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
public class MyApplication extends PrintOnScreen {
@Override protected String textToShow() {
        return "printing this text on " + "screen using PrintOnScreen " + "white Box Framework"; 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

黑盒框架的原理与实现

黑盒框架时基于委派的组合方式,是不同对象之间的组合。之所以是黑盒,是因为不用去管对象中的方法是如何实现的,只需关心对象上拥有的方法。

这种方式较白盒框架更为灵活,因为可以在运行时动态地传入不同对象,实现不同对象间的动态组合;而继承机制在静态编译时就已经确定好。

黑盒框架与白盒框架之间可以相互转换,具体例子可以看一下,软件构造课程中有关黑盒框架的例子,更改上面的白盒框架为黑盒框架:

public interface TextToShow { 
    String text(); 
}
  • 1
  • 2
  • 3
public class MyTextToShow implements TextToShow {
    @Override 
    public String text() { 
        return "Printing"; 
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
public final class PrintOnScreen {
    TextToShow textToShow;   
    public PrintOnScreen(TextToShow tx) { 
        this.textToShow = tx; 
    }
    public void print() { 
        JFrame frame = new JFrame(); 
        JOptionPane.showMessageDialog(frame, textToShow.text());
        frame.dispose(); 
    }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

猜测下面代码的结果:

PrintOnScreen m = new PrintOnScreen(new MyTextToShow()); 
m.print();

二者对比

白盒框架利用subclassing:

  • 允许扩展每一个非私有方法
  • 需要理解父类的实现
  • 一次只进行一次扩展
  • 通常被认为是开发者框架

黑盒框架使用委派中的组合composition:

  • 允许在接口中对public方法扩展
  • 只需要理解接口
  • 通常提供更多的模块
  • 通常被认为是终端用户框架,平台

Design Patterns for Reuse

 除了类本身,设计模式更强调多 个类/对象之间的关系和交互过程---比接口/类复用的粒度更大。

   Adapter(适配器模式)

    通过增加一个接口,将已存在的子类封装起来,client面向接口编程,从而隐藏了具体子类。




Decorator (装饰器模式)

为对象增加不同侧面的特性 对每一个特性构造子类,通过委派机制增加到对象上
1、Component 是基类。通常是一个抽象类或者一个接口,定义了属性或者方法,方法的实现可以由子类实现或者自己实现。通常不会直接使用该类,而是通过继承该类来实现特定的功能,它约束了整个继承树的行为。比如说,如果Component代表人,即使通过装饰也不会使人变成别的动物。
2、ConcreteComponent 是Component的子类,实现了相应的方法,它充当了“被装饰者”的角色。
3、Decorator 也是Component的子类,它是装饰者共同实现的抽象类(也可以是接口)。比如说,Decorator代表衣服这一类装饰者,那么它的子类应该是T恤、裙子这样的具体的装饰者。
4、ConcreteDecorator 是Decorator的子类,是具体的装饰者,由于它同时也是Component的子类,因此它能方便地拓展Component的状态(比如添加新的方法)。每个装饰者都应该有一个实例变量用以保存某个Component的引用,这也是利用了组合的特性。在持有Component的引用后,由于其自身也是Component的子类,那么,相当于ConcreteDecorator包裹了Component,不但有Component的特性,同时自身也可以有别的特性,也就是所谓的 装饰

Step 1、创建Component基类

因为总体对象是人,所以我们可以把人抽象为基类,新建Person.java:

public abstract class Person {
    String description = "Unkonwn";

    public String getDescription()
    {
        return description;
    }

    public abstract double cost(); //子类应该实现的方法
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Step 2、创建被装饰者——ConcreteComponent

客户分为很多种,有儿童、青少年、成年人等,因此我们可以创建不同的被装饰者,这里我们创建青少年的被装饰者,新建Teenager.java

public class Teenager extends Person {

    public Teenager() {
        description = "Shopping List:";
    }

    @Override
    public double cost() {
        //什么都没买,不用钱
        return 0;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Step 3、创建Decorator

由于不同的部位有不同的衣物,不能混为一谈,比如说,衣服、帽子、鞋子等,那么这里我们创建的Decorator为衣服和帽子,分别新建ClothingDecorator.javaHatDecorator.java:

public abstract class ClothingDecorator extends Person {

    public abstract String getDescription();
}
  • 1
  • 2
  • 3
  • 4
public abstract class HatDecorator extends Person {

    public abstract String getDescription();

}
  • 1
  • 2
  • 3
  • 4
  • 5

Step 4、创建ConcreteDecorator

上面既然已经创建了两种Decorator,那么我们基于它们进行拓展,创建出不同的装饰者,对于Clothing,我们新建Shirt.java,对于Hat,我们新建Casquette,其实可以根据不同类型的衣物创建更多不同的装饰者,这里只是作为演示而创建了两种。代码如下所示:

public class Shirt extends ClothingDecorator {

    //用实例变量保存Person的引用
    Person person;

    public Shirt(Person person)
    {
        this.person = person;
    }

    @Override
    public String getDescription() {
        return person.getDescription() + "a shirt  ";
    }

    @Override
    public double cost() {
        return 100 + person.cost(); //实现了cost()方法,并调用了person的cost()方法,目的是获得所有累加值
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
public class Casquette extends HatDecorator {

    Person person;

    public Casquette(Person person) {
        this.person = person;
    }
    @Override
    public String getDescription() {
        return person.getDescription() + "a casquette  "; //鸭舌帽
    }

    @Override
    public double cost() {
        return 75 + person.cost();
    }

}

最后我们在测试类内测试我们的代码:

public class Shopping {

    public static void main(String[] args) {
        Person person = new Teenager();

        person = new Shirt(person);
        person = new Casquette(person);

        System.out.println(person.getDescription() + " ¥ " +person.cost());
    }

}

Facade(外观模式)

提供一个统一的接口来取代一系列小接口调用,相当于对复杂 系统做了一个封装,简化客户端使用。 客户端需要通过一个简化的接口来访问复杂系统内的功能。


在软件开发中,有时候为了完成一项较为复杂的功能,一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出现,由于涉及到的类比较多,导致使用时代码较为复杂,此时,特别需要一个类似服务员一样的角色,由它来负责和多个业务类进行交互

 Strategy(策略设计模式)

 

Template Method(模板设计模式)


 使用继承和重写实现模板模式。

Iterator

Iterator pattern:让自己的集合类实现Iterable接口,并实现自己的 独特Iterator迭代器(hasNext, next, remove),允许客户端利用这 个迭代器进行显式或隐式的迭代遍历:






























猜你喜欢

转载自blog.csdn.net/weixin_41145325/article/details/80717595
今日推荐