设计模式(1)-简单设计与交给子类
Iterator
简介
Iterator意为迭代器,Iterator模式通常用于在数据集合中按照顺序遍历集合。在许多程序语言中都有迭代器的相关实现,当然我们也可以自己利用Iterator模式来构造自己的迭代器。
在Iterator模式中一般有如下角色:
Iterator
:该角色代表迭代器,它负责定义相关API,按照顺序遍历元素- 通常具有
hasNext
、next
方法
- 通常具有
ConcreteIterator
:该角色代表具体的迭代器,它负责实现Iterator角色所定义的API。该角色中包含了遍历集合所必需的信息Aggregate
:该角色代表集合,它其中定义创建Iterator角色的API,该API能够创建出对应的迭代器IteratorConcreteAggregate
:该角色代表具体的集合,用于实现Aggregate角色所定义的API,具体实现如何进行迭代
角色之间的关系如下:
示例程序
这里我们模拟一个书架类,通过Iterator模式来对书架上的书进行遍历。首先我们可以创建Book类,用于模拟书。Book类仅有一个String类型的name属性,以及相应的get、set、构造方法和toString方法,这里就不进行描述了。
之后定义Iterator
接口,这个接口通常情况下具有hasNext
和next
方法,用于判断是否有下一个元素以及获取下一个元素。
1 |
|
Aggregate
接口中定义了iterator
方法,主要用于返回相应的迭代器对象:
1 |
|
之后我们可以定义BookShelf
书架类,这个类应该实现Aggregate
接口。首先按照类本身的逻辑,BookShelf
中需要定义一个数组来保存Book
对象,之后还需要提供相关的添加书籍的方法。同时由于实现了Aggregate
,需要返回一个迭代器。这里的迭代器对象则是另外定义的一个BookShelfIterator
,它实现了Iterator
接口。在BookShelfItreator
类中,需要实现相关的迭代方法的真实逻辑,包括hasNext
以及next
。注意这里BookShelfIterator
的构造需要传入BookShelf
对象。
1 |
|
至此,我们就可以测试相关的迭代方式是否可用,测试代码如下:
1 |
|
说明
通过上面的例子可以看到,利用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 |
|
而目前我们已经有一个完成了类似功能的Banner类,它的实现如下,它的相关方法能够一定程度上达到我们需要的效果:
1 |
|
为了实现这个需求,我们需要使用一个中间适配器Adapter,用于连接需求接口print以及已有代码Banner。我们可以定义一个类PrintBanner,它在继承Banner的同时还实现了Print接口,其中print接口的方法实现则直接使用Banner中的实现,进行一层包装:
1 |
|
然后,我们就可以利用如下代码进行测试。在测试代码中,作为使用者,我们只需要使用我们新定义的Print接口和实现类PrintBanner,而不需要关注Banner这个已有的旧代码,就能够达到需要的效果:
1 |
|
上面这种实现方式就是基于继承实现的Adapter模式。而下面开始介绍基于委托的Adapter模式实现。假设我们现在的需求并不是Print接口,而是一个Printer类,这是一个抽象类,其中也需要具有上面Print接口中的两种方法,抽象类代码如下:
1 |
|
此时我们同样可以使用一个类来进行适配。我们实现一个PrinterBanner类,它直接继承抽象类Printer,不过在其中存储一个Banner类型的成员变量,而抽象父类的方法实现则调用这个成员变量来实现:
1 |
|
同样可以使用类似的测试代码进行测试,也是能够达到相同的效果。
1 |
|
说明
Adapter模式是一个较为简单的设计模式,在日常程序开发的过程中非常常见,常见到很多时候我们往往都忽略了这是一个设计模式。Adapter模式能够帮助我们实现代码的复用,在开发时更加方便。简单来说,就是将我们需要使用的现有类进行包装,无论是以继承的方式还是成员变量的方式,需要达到的目的就是将现有的功能能够帮助到我们目前需求的实现。同时,现有的代码通常是经过了检测和测试的,这也就意味着我们新需求的代码如果出现Bug的时候,进行排查时就可以直接跳过这部分已经接受过检测的代码,减少考虑的范围。
实际上,Adapter模式更多会作为一种好的编程习惯。如果我们有一个好的编程习惯,对于它的使用往往在不经意间就能够完成。
Template Method
简介
Template Mothod,即模板方法。这种模式提供模版功能,组成模板的方法被定义在父类中,父类提供一个模板方法,在模板方法中按照一定的逻辑进行处理,但是这里使用的是抽象方法,只通过父类的代码是无法知道这些方法最终会进行何种处理,唯一能够知道父类是如何调用这些方法的。抽象方法由子类进行实现,不同的实现也就导致实际处理的不同,不过具体的流程还是按照父类中的定义进行的。
在Template Method模式中有如下角色:
AbstractClass
:抽象类,负责实现模板方法,同时负责声明在模板方法中所使用到的抽象方法。这些抽象方法由子类负责实现ConcreteClass
:具体类,负责具体实现在AbstractClass中定义的抽象方法。而方法的具体实现会在模板方法中被调用
简单来说,Template Method模式就是在父类中定义处理流程的框架,在子类中实现具体的处理逻辑。
示例程序
首先准备一个抽象父类AbstractDisplay
,其中的核心方法是display
。在display中,它首先调用了open
方法,然后重复执行五次print
方法,最后执行了close
方法,不过这些被调用的方法都是抽象方法,等待子类实现。
1 |
|
这里我们实现两个子类来观察不同的运行结果,第一个子类CharDisplay
对相关抽象方法有如下的实现:
1 |
|
第二个子类StringDisplay
对相关抽象方法则实现如下:
1 |
|
两个子类对抽象方法的实现各不相同,但是最终在display
中的执行流程还是一样的,都是按照父类已经定义好的流程进行实现的。可以利用相关测试方法来查看不同的效果:
1 |
|
说明
在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 |
|
1 |
|
接下来,我们就需要分别它们对应的子类,IDCard以及IDCardFactory。
1 |
|
IDCard需要继承Product,并且实现对应的方法。注意这里的构造方法是默认的default作用范围,它的作用范围是本包内,外部是无法调用这个构造方法的。这也就能够一定程度上限制其他外部类的行为,它无法直接new得到对象,而只能通过工厂来创建对象。
IDCardFactory则需要继承Factory,同样实现对应的方法:
1 |
|
接下来就可以书写测试程序了。对于这些功能的使用者也就是我们的测试程序来说,它需要获得一个IDCard的实例,只需要通过工厂IDCardFactory进行创建即可,至于其内部的具体逻辑则不需要关心。
1 |
|
说明
Factory Method可以帮助我们对外提供一个简单的获取实例的方式。在一些情况下,某个实例的准备可能需要复杂的过程,但是对于外界来说,可能只需要知道如何能够生成就行。Factory Method就能够提供一个分离的方式获取实例。不过在编写具体类的时候,需要结合抽象父类进行理解,否则会对各个抽象方法实际需要完成的工作不够清楚,导致子类中的实现无法达到效果。
对于Factory中的需要子类实现的方法,我们一般有三种方式来定义。第一种就是直接声明为抽象方法,子类必须实现,否则在编译时会报错。第二种就是在父类中给一个最简单最基础的实现。第三种是默认实现抛出异常,如果在子类中没有实现,则是在执行的以后才会报错。