设计模式(1)-简单设计与交给子类

Iterator

简介

Iterator意为迭代器,Iterator模式通常用于在数据集合中按照顺序遍历集合。在许多程序语言中都有迭代器的相关实现,当然我们也可以自己利用Iterator模式来构造自己的迭代器。

在Iterator模式中一般有如下角色:

  • Iterator:该角色代表迭代器,它负责定义相关API,按照顺序遍历元素
    • 通常具有hasNextnext方法
  • ConcreteIterator:该角色代表具体的迭代器,它负责实现Iterator角色所定义的API。该角色中包含了遍历集合所必需的信息
  • Aggregate:该角色代表集合,它其中定义创建Iterator角色的API,该API能够创建出对应的迭代器Iterator
  • ConcreteAggregate:该角色代表具体的集合,用于实现Aggregate角色所定义的API,具体实现如何进行迭代

角色之间的关系如下:

示例程序

这里我们模拟一个书架类,通过Iterator模式来对书架上的书进行遍历。首先我们可以创建Book类,用于模拟书。Book类仅有一个String类型的name属性,以及相应的get、set、构造方法和toString方法,这里就不进行描述了。

之后定义Iterator接口,这个接口通常情况下具有hasNextnext方法,用于判断是否有下一个元素以及获取下一个元素。

1
2
3
4
public interface Iterator {
public abstract boolean hasNext();
public abstract Object next();
}

Aggregate接口中定义了iterator方法,主要用于返回相应的迭代器对象:

1
2
3
public interface Aggregate {
public abstract Iterator iterator();
}

之后我们可以定义BookShelf书架类,这个类应该实现Aggregate接口。首先按照类本身的逻辑,BookShelf中需要定义一个数组来保存Book对象,之后还需要提供相关的添加书籍的方法。同时由于实现了Aggregate,需要返回一个迭代器。这里的迭代器对象则是另外定义的一个BookShelfIterator,它实现了Iterator接口。在BookShelfItreator类中,需要实现相关的迭代方法的真实逻辑,包括hasNext以及next。注意这里BookShelfIterator的构造需要传入BookShelf对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class BookShelf implements Aggregate{
private final ArrayList<Book> books;

public BookShelf(){
this.books = new ArrayList<>();
}

public void addBook(Book book){
this.books.add(book);
}
public ArrayList<Book> getBooks() {
return books;
}

@Override
public BookShelfIterator iterator() {
return new BookShelfIterator(this);
}
}


public class BookShelfIterator implements Iterator{
private BookShelf bookShelf;
private int index;

public BookShelfIterator(BookShelf bookShelf){
this.index = 0;
this.bookShelf = bookShelf;
}

@Override
public boolean hasNext() {
return index < bookShelf.getBooks().size();
}

@Override
public Object next() {
return this.bookShelf.getBooks().get(index++);
}
}

至此,我们就可以测试相关的迭代方式是否可用,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testIterator(){
BookShelf bookShelf = new BookShelf();
bookShelf.addBook(new Book("book1"));
bookShelf.addBook(new Book("book2"));
bookShelf.addBook(new Book("book3"));
bookShelf.addBook(new Book("book4"));

BookShelfIterator iterator = bookShelf.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

说明

通过上面的例子可以看到,利用Iterator模式我们完成了一个数组或者集合的遍历。与通过下标index和for循环直接遍历相比,这种方式似乎更加繁琐,但在Iterator模式中,我们的遍历只使用到了Iterator接口中的方法,而不会依赖实际BookShelf的实现。也就是说,在BookShelf中,无论是使用数组,或者其他集合例如LinkedList来管理Book对象,只要BookShelf中的iterator方法能够正确地返回Iterator的实例,上面的while循环就是可以正常工作的,这段测试代码是不用变动的。而测试代码实际上就相当于其他对于BookShelf的调用者,这种仅依赖Iterator的特性使得调用者变得非常方便,因为他无需关心BookShelf的具体实现,只需要拿到一个正确的Iterator实例即可。

这里也体现了设计模式的一个非常重要的思想,就是优先使用抽象类和接口来进行编程,这样能够弱化类之间的耦合,进而使得类能够更加容易地作为组件被再次利用。

Adapter

简介

Adapter意为适配器,它位于实际情况和需求之间,填补两者之间的差异。在程序开发的过程中,经常会存在现有的程序无法直接使用,需要进行适当的变换之后才能够使用的情况。这种用于填补“现有的程序”与“所需的程序”之间差异的设计模式就是Adapter模式。

Adapter模式也被称为Wrapper模式,即包装器。通过对现有的程序进行包装,使得其能够达到我们需要的效果。Adapter模式主要分为使用继承实现的Adapter以及使用委托实现的Adapter。

在Adapter模式中,主要有以下角色:

  • Target:该角色中定义了我们最终需要得到的方法,它需要能够处理我们现有的需求
  • Client:请求者角色。通常是Main函数,它调用Target角色来使用相关特性。这相当于是客户,是服务的调用者
  • Adaptee:被适配角色。表示现有的程序,这个程序在我们现有的需求中虽然不能直接使用,但是能够被复用
  • Adapter:适配角色。Adapter模式的核心角色,连接了Target以及Adaptee,使得Target对于需求的实现能够借助现有的Adaptee

角色之间的关系如下:

示例程序

假设我们需要完成一个Print接口,这个接口中有如下的方法,其中printWeak输出两端加上括号的结果;printStrong输出两端加上星号的结果。这就是我们的需求。

1
2
3
4
public interface Print {
void printWeak();
void printStrong();
}

而目前我们已经有一个完成了类似功能的Banner类,它的实现如下,它的相关方法能够一定程度上达到我们需要的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Banner {
private String string;

public Banner(String string) {
this.string = string;
}

public void showWithParen() {
System.out.println("(" + string + ")");
}

public void showWithAster() {
System.out.println("*" + string + "*");
}
}

为了实现这个需求,我们需要使用一个中间适配器Adapter,用于连接需求接口print以及已有代码Banner。我们可以定义一个类PrintBanner,它在继承Banner的同时还实现了Print接口,其中print接口的方法实现则直接使用Banner中的实现,进行一层包装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PrintBanner extends Banner implements Print{
public PrintBanner(String string) {
super(string);
}

@Override
public void printWeak() {
showWithParen();
}

@Override
public void printStrong() {
showWithAster();
}
}

然后,我们就可以利用如下代码进行测试。在测试代码中,作为使用者,我们只需要使用我们新定义的Print接口和实现类PrintBanner,而不需要关注Banner这个已有的旧代码,就能够达到需要的效果:

1
2
3
4
5
6
@Test
public void test1(){
Print print = new PrintBanner("Hello");
print.printStrong();
print.printWeak();
}

上面这种实现方式就是基于继承实现的Adapter模式。而下面开始介绍基于委托的Adapter模式实现。假设我们现在的需求并不是Print接口,而是一个Printer类,这是一个抽象类,其中也需要具有上面Print接口中的两种方法,抽象类代码如下:

1
2
3
4
public abstract class Printer {
public abstract void printWeak();
public abstract void printStrong();
}

此时我们同样可以使用一个类来进行适配。我们实现一个PrinterBanner类,它直接继承抽象类Printer,不过在其中存储一个Banner类型的成员变量,而抽象父类的方法实现则调用这个成员变量来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PrinterBanner extends Printer{
private final Banner banner;

public PrinterBanner(String string){
this.banner = new Banner(string);
}
@Override
public void printWeak() {
this.banner.showWithParen();
}

@Override
public void printStrong() {
this.banner.showWithAster();
}
}

同样可以使用类似的测试代码进行测试,也是能够达到相同的效果。

1
2
3
4
5
6
@Test
public void test2(){
Printer printer = new PrinterBanner("hello");
printer.printStrong();
printer.printWeak();
}

说明

Adapter模式是一个较为简单的设计模式,在日常程序开发的过程中非常常见,常见到很多时候我们往往都忽略了这是一个设计模式。Adapter模式能够帮助我们实现代码的复用,在开发时更加方便。简单来说,就是将我们需要使用的现有类进行包装,无论是以继承的方式还是成员变量的方式,需要达到的目的就是将现有的功能能够帮助到我们目前需求的实现。同时,现有的代码通常是经过了检测和测试的,这也就意味着我们新需求的代码如果出现Bug的时候,进行排查时就可以直接跳过这部分已经接受过检测的代码,减少考虑的范围。

实际上,Adapter模式更多会作为一种好的编程习惯。如果我们有一个好的编程习惯,对于它的使用往往在不经意间就能够完成。

Template Method

简介

Template Mothod,即模板方法。这种模式提供模版功能,组成模板的方法被定义在父类中,父类提供一个模板方法,在模板方法中按照一定的逻辑进行处理,但是这里使用的是抽象方法,只通过父类的代码是无法知道这些方法最终会进行何种处理,唯一能够知道父类是如何调用这些方法的。抽象方法由子类进行实现,不同的实现也就导致实际处理的不同,不过具体的流程还是按照父类中的定义进行的。

在Template Method模式中有如下角色:

  • AbstractClass:抽象类,负责实现模板方法,同时负责声明在模板方法中所使用到的抽象方法。这些抽象方法由子类负责实现
  • ConcreteClass:具体类,负责具体实现在AbstractClass中定义的抽象方法。而方法的具体实现会在模板方法中被调用

简单来说,Template Method模式就是在父类中定义处理流程的框架,在子类中实现具体的处理逻辑。

示例程序

首先准备一个抽象父类AbstractDisplay,其中的核心方法是display。在display中,它首先调用了open方法,然后重复执行五次print方法,最后执行了close方法,不过这些被调用的方法都是抽象方法,等待子类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class AbstractDisplay {
public abstract void open();

public abstract void print();

public abstract void close();

public final void display() {
open();
for (int i = 0; i < 5; i++) {
print();
}
close();
}
}

这里我们实现两个子类来观察不同的运行结果,第一个子类CharDisplay对相关抽象方法有如下的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CharDisplay extends AbstractDisplay{
@Override
public void open() {
System.out.print("<<");
}

@Override
public void print() {
System.out.print("H");
}

@Override
public void close() {
System.out.print(">>");
}
}

第二个子类StringDisplay对相关抽象方法则实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringDisplay extends AbstractDisplay{
@Override
public void open() {
System.out.println("+--------+");
}

@Override
public void print() {
System.out.println("+Template+");
}

@Override
public void close() {
System.out.println("+--------+");
}
}

两个子类对抽象方法的实现各不相同,但是最终在display中的执行流程还是一样的,都是按照父类已经定义好的流程进行实现的。可以利用相关测试方法来查看不同的效果:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testCharDisplay(){
AbstractDisplay display = new CharDisplay();
display.display();
}

@Test
public void testStringDisplay(){
AbstractDisplay display = new StringDisplay();
display.display();
}

说明

在Template Method模式中,父类的模版方法内部定义了算法或者流程,这样就无需在每个子类内部重复编写算法。这种方式集中在父类内部管理核心流程,保证了关键代码的稳定,而具体的实现由不同的子类来完成,又可以适应多种不同的情况。在该类模式中,父类和子类之间是相互联系的,我们只有参考理解父类中模版方法的流程,才能够很好地在子类中实现相关的抽象方法。

Factory Method

简介

在Template Method模式中,我们在父类中规定处理的流程,而让子类来实现具体的处理。将这种思想应用于实例生成,则是Factory Method模式。在Factory Method模式中,父类将决定实例生成的流程和方式,但是具体的处理则交给子类来完成,如此,就可以将生成实例的框架和实际负责实例生成的类进行解耦。

在Factory Method中有如下角色:

  • Product:产品类,该角色属于框架这一方,属于抽象类。该角色定义了在Factory Method模式中生成的实例所需要有的接口
  • Creator:创建者,同样属于框架这一方,属于抽象类,负责生成Product角色,生成过程可以安排,但是具体的处理由子类决定
  • ConcreteProduct:继承Product,属于具体的产品
  • ConcreteCreator:继承Creator,属于具体的创建者

示例程序

考虑这样的场景,我们使用Factory Method模式来完成ID卡的生成。ID卡属于产品Product,我们需要有一个工厂Factory来生成产品。首先定义Product类和Factory类,它们属于框架提供的架构,也就是父类。在Product类中声明产品有一个使用方法,而在Factory中定义了产品的生产流程,关键步骤使用抽象方法,需要由子类进行实现。

1
2
3
public abstract class Product {
public abstract void use();
}
1
2
3
4
5
6
7
8
9
10
public abstract class Factory {
public final Product create(String owner){
Product product = createProduct(owner);
registerProduct(product);
return product;
}

protected abstract Product createProduct(String owner);
protected abstract void registerProduct(Product product);
}

接下来,我们就需要分别它们对应的子类,IDCard以及IDCardFactory。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IDCard extends Product {
private String owner;

IDCard(String owner) {
System.out.println("generate the ID card of " + owner);
this.owner = owner;
}

@Override
public void use() {
System.out.println("using the ID card of " + owner);
}

public String getOwner() {
return owner;
}
}

IDCard需要继承Product,并且实现对应的方法。注意这里的构造方法是默认的default作用范围,它的作用范围是本包内,外部是无法调用这个构造方法的。这也就能够一定程度上限制其他外部类的行为,它无法直接new得到对象,而只能通过工厂来创建对象。

IDCardFactory则需要继承Factory,同样实现对应的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IDCardFactory extends Factory {
private final List<String> owners = new ArrayList<>();

@Override
protected Product createProduct(String owner) {
return new IDCard(owner);
}

@Override
protected void registerProduct(Product product) {
owners.add(((IDCard) product).getOwner());
}
}

接下来就可以书写测试程序了。对于这些功能的使用者也就是我们的测试程序来说,它需要获得一个IDCard的实例,只需要通过工厂IDCardFactory进行创建即可,至于其内部的具体逻辑则不需要关心。

1
2
3
4
5
6
7
8
9
10
@Test
public void test() {
Factory factory = new IDCardFactory();
Product card1 = factory.create("A");
Product card2 = factory.create("B");
Product card3 = factory.create("C");
card1.use();
card2.use();
card3.use();
}

说明

Factory Method可以帮助我们对外提供一个简单的获取实例的方式。在一些情况下,某个实例的准备可能需要复杂的过程,但是对于外界来说,可能只需要知道如何能够生成就行。Factory Method就能够提供一个分离的方式获取实例。不过在编写具体类的时候,需要结合抽象父类进行理解,否则会对各个抽象方法实际需要完成的工作不够清楚,导致子类中的实现无法达到效果。

对于Factory中的需要子类实现的方法,我们一般有三种方式来定义。第一种就是直接声明为抽象方法,子类必须实现,否则在编译时会报错。第二种就是在父类中给一个最简单最基础的实现。第三种是默认实现抛出异常,如果在子类中没有实现,则是在执行的以后才会报错。


设计模式(1)-简单设计与交给子类
http://example.com/2023/03/16/设计模式-1-简单设计与交给子类/
作者
EverNorif
发布于
2023年3月16日
许可协议