11 面向可复用性和可维护性的设计模式
创建模式:工厂方法
结构模式
- 适配器模式:具有不兼容接口的类可以通过将其自己的接口包装在现有类的接口周围来协同工作
- 装饰器模式
行为模式:
- 策略模式:允许在运行时选择一系列算法中的一个
- 模板模式:规定抽象逻辑,实现细节需要实现
- 迭代器模式:顺序访问元素,不会表示泄漏
- 访问者模式:将算法和对象结构分开
为什么需要可复用性:
除了类本身,设计模式更强调多个类/对象之间的关系和交互过程—比接口/类复用的粒度更大
11.1 创建型模式
工厂方法模式:虚拟构造器
当client不知道/不确定要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。
定义一个用于创建对象的接口,让该接口的子类型来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
相比于通过构造器(new)构建对象:
- 静态工厂方法可具有指定的更有意义的名称
- 不必在每次调用的时候都创建新的工厂对象
- 可以返回原返回类型的任意子类型
eg:
以上为不使用工厂方法的情况
以上为工厂方法模式
以上为简单工厂模式
静态工厂方法既可以单独作为一个类,也可以存在于某个具体的实现类中
三大工厂方法的优缺点https://blog.csdn.net/Fly_as_tadpole/article/details/88326807
11.2 结构化模式
11.2.1 适配器模式
主要解决类之间接口不兼容的问题。
通过增加一个接口,将已存在的子类封装起来,client面向接口编程,从而隐藏了具体子类。
主要有两种类型:继承/委托
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
UML类图:
eg:
其中,shape为目标接口,rectangle为适配器类,legacyrectangle为适配者类
11.2.2 装饰器模式
问题引出:当有许多子类需要自由组合的时候,Java无法多继承,也不适合顺序继承,因此需要一种机制可以为对象增加不同侧面的特性。
解决方法:对每一个特性构造子类,通过委派机制增加到对象上。装饰器同时使用子类型和委派。
装饰(Decorator)模式中的角色:
- 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(Concrete Decorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
装饰者模式的核心是装饰类,这个类是个抽象类,是抽象构件的子类,同时与抽象构件具有委托关系,即在其内部调用了抽象构件。
UML类图如下:
eg:
装饰类中需要有被装饰的对象字段(表现为委托关系),声明是对哪个对象进行装饰,以及对应的getter和setter方法。之后还要有对应的有参构造方法,具体要求为需要传入声明的对象字段,对于接口中定义的字段直接使用super关键字,而新定义的字段使用this关键字。代码示例如下:
public abstract class Garnish extends FastFood {
private FastFood fastFood;
public FastFood getFastFood() {
return fastFood;
}
public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}
public Garnish(FastFood fastFood, float price, String desc)
{//构造方法
super(price,desc);
this.fastFood = fastFood;
}
}
public abstract class StackDecorator implements Stack
{
protected final Stack stack;
public StackDecorator(Stack stack) //构造方法
{
this.stack = stack;
}
public void push(Item e)
{
stack.push(e);
}
public Item pop()
{
return stack.pop();
}
...
}
具体装饰类中除了需要进行必要的重写之外,还需要使用有参构造。参数是其抽象类中的字段,即将参数赋值给抽象类中对应的字段。而方法内部直接调用父类有参构造方法,即装饰类的有参构造方法,使用关键字super。
代码如下:
public class Bacon extends Garnish
{
public Bacon(FastFood fastFood) //表示在快餐fastFood中加入装饰“培根”
{//构造方法
super(fastFood,2,"培根");
}
@Override
public float cost() {
return getPrice() + getFastFood().getPrice();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
public class UndoStack extends StackDecorator
{
private final UndoLog log = new UndoLog();
public UndoStack(Stack stack) //表示在栈的基础上增加装饰“UndoStack”
{
super(stack);
}
public void push(Item e)
{
super.push(e);
log.append(UndoLog.PUSH, e);
}
public void undo()
{
//implement decorator behaviors on stack
}
...
}
调用时,对于参数可以直接使用之前构造出来的,也可以new一个新的。但整体表现效果就是通过一层一层的装饰来实现的。
//点一份炒饭
FastFood food = new FriedRice();
//点一份加鸡蛋的炒饭
FastFood food1 = new FriedRice();
food1 = new Egg(food1);
//点一份加培根的炒面
FastFood food2 = new FriedNoodles();
food2 = new Bacon(food2);
//To construct a plain stack:
Stack s = new ArrayStack();
//To construct an undo stack:
Stack t = new UndoStack(new ArrayStack());
// To construct a secure synchronized undo stack:
Stack t
= new SecureStack(
new SynchronizedStack(
new UndoStack(s)));
t.push(e);
装饰器和继承的区别与联系:
联系:都是创建一个新的类来加强原有类的功能
区别:
- 装饰有许多协作对象(其抽象类的实现类),而继承则是生成一个类型明确的对象
- 装饰可以多装饰,但是继承只能单继承
- 装饰在运行时才起作用,继承在编译时就起作用
Java.util.Collections
中也使用了许多装饰器,如unmodifiable、synchronized等
11.3 行为模式
11.3.1 策略模式
某个方法在运行时需要client根据需要动态切换算法,而不是写死在代码里,这就需要用到策略模式。
实现方法:为不同的实现算法构造抽象接口,利用委托,运行时动态传入client倾向的算法类实例
优点:易于扩展以实现新算法;将算法与客户端上下文分离
策略模式中的主要角色:
- 抽象策略类:通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口
- 具体策略类:抽象策略类的实现,提供具体的算法
- 环境类(context):持有一个策略类的引用,最终给客户端调用
UML类图如下:
eg:
//定义接口,抽象策略类:
public interface Strategy {
void show();
}
//定义一些具体策略:
//策略A
public class StrategyA implements Strategy {
public void show() {
System.out.println("策略A");
}
}
//策略B
public class StrategyB implements Strategy {
public void show() {
System.out.println("策略B");
}
}
//定义环境类,用于连接用户端和服务端,将用户端选择的策略传递给服务端进行挑选、实现
public class Context {
//持有抽象策略角色的引用
private Strategy strategy;
//有参构造方法,将用户输入的策略作为context的私有字段
public Context(Strategy strategy) {
this.strategy = strategy;
}
//通过委派机制展示具体策略
public void ContextShow(){
strategy.show();
}
}
11.3.2 模版(Template )模式
问题:多个客户端共享相同的算法,但在细节上有所不同,即算法由个性部分和共性部分组成。子类中不应重复公共步骤,但需要重用。
解决方法:共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现
实现方法:使用继承和重写实现模版模式;相较之下,策略模式使用委托来改变整个算法
模板模式包含以下主要角色:
-
抽象类:负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
-
模板方法(Template Method):定义了算法的骨架,按某种顺序调用其包含的基本方法。
-
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分,一般为抽象方法,需要实现类。
-
-
具体子类:实现抽象类中所定义的方法。
UML类图:
eg:
//定义抽象类
public abstract class CarBuilder {
protected abstract void BuildSkeleton();
protected abstract void InstallEngine();
protected abstract void InstallDoor();
//模版方法:
public void BuildCar() {
BuildSkeleton();
InstallEngine();
InstallDoor();
}
}
//具体实现:
//保时捷
public class PorcheBuilder extends CarBuilder {
protected void BuildSkeleton() {
System.out.println("Building Porche Skeleton");
}
protected void InstallEngine() {
System.out.println("Installing Porche Engine");
}
protected void InstallDoor() {
System.out.println("Installing Porche Door");
}
}
//甲壳虫
public class BeetleBuilder extends CarBuilder {
protected void BuildSkeleton() {
System.out.println("Building Beetle Skeleton");
}
protected void InstallEngine() {
System.out.println("Installing Beetle Engine");
}
protected void InstallDoor() {
System.out.println("Installing Beetle Door");
}
}
//客户端代码
public static void main(String[] args) {
CarBuilder c = new PorcheBuilder();
c.BuildCar();//直接调用父类方法
c = new BeetleBuilder();
c.BuildCar();
}
运行结果:
注意,具体实现类中可以不必重写父类中除模板方法外的所有方法,可以根据需要灵活选择重写几个
此外,模版模式经常用于白盒框架
eg:将Application继承到子类calculator和ping上
11.3.3 迭代器模式
问题:客户端希望对放入容器/集合类的一组ADT对象进行遍历访问,而无需关心容器的具体类型,也就是说,不管对象被放进哪里,都应该提供同样的遍历方式
解决方法:使用迭代器模式
迭代器模式包含以下主要角色:
- 抽象聚合(Abstract Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
- 具体聚合(Concrete Aggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
- 抽象迭代器(Abstract Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。
- 具体迭代器(Concrete Iterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
UML类图:
eg:定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现,涉及到的类如下:
//定义迭代器接口,声明hasNext方法和next方法
public interface StudentIterator {
boolean hasNext();
Student next();
}
//定义具体的迭代器类,重写所有的抽象方法
public class StudentIteratorImpl implements StudentIterator {
private List<Student> list;
private int position = 0;
public StudentIteratorImpl(List<Student> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return position < list.size();
}
@Override
public Student next() {
Student currentStudent = list.get(position);
position ++;
return currentStudent;
}
}
//定义抽象容器类,包含添加元素,删除元素,获取迭代
public interface StudentAggregate {
void addStudent(Student student);
void removeStudent(Student student);
StudentIterator getStudentIterator();
}
//定义具体的容器类,重写所有方法
public class StudentAggregateImpl implements StudentAggregate {
// 创建一个学生列表
private List<Student> list = new ArrayList<Student>();
@Override
public void addStudent(Student student) {
this.list.add(student);
}
@Override
public void removeStudent(Student student) {
this.list.remove(student);
}
@Override
public StudentIterator getStudentIterator() {
return new StudentIteratorImpl(list);//返回一个迭代器的实例
}
}
Java源码中迭代器模式的使用:
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
//list.iterator()方法返回的是Iterator接口的子实现类对象
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
- List:抽象聚合类
- ArrayList:具体的聚合类
- Iterator:抽象迭代器
- list. iterator():返回的是实现了 Iterator 接口的具体迭代器
注意:当我们在使用JAVA开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现
java.util.Iterable
并实现其中的iterator()方法使其返回一个java.util.Iterator
的实现类就可以
11.3.4 访问者模式
访问者模式:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作
本质上:将数据和作用于数据上的某种/些特定操作分离开来。
观察者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
- 具体访问者(Concrete Visitor)角色:给出对每一个元素类访问时所产生的具体行为。
- 抽象元素(Element)角色:定义了一个接受访问者的方法(accept ),其意义是指,每一个元素都要可以被访问者访问。
- 具体元素(Concrete Element)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element ),并且可以迭代这些元素,供访问者访问。
eg:[给宠物喂食] 现在养宠物的人特别多,以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以。
//创建访问者接口
public interface Person {
void feed(Cat cat);
void feed(Dog dog);
}
//创建不同的具体访问者角色(主人和其他人),都需要实现Person接口
public class Owner implements Person {
@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}
public class Someone implements Person {
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}
//定义抽象节点 -- 宠物
public interface Animal {
void accept(Person person);
}
//定义实现Animal接口的具体节点(元素)
public class Dog implements Animal {
@Override
public void accept(Person person) {
//将具体的访问功能委托给外部传入的visitor
person.feed(this);//这里的访问操作就是feed
System.out.println("好好吃,汪汪汪!!!");
}
}
public class Cat implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,喵喵喵!!!");
}
}
//定义对象结构,此案例中就是主人的家
public class Home {
private List<Animal> nodeList = new ArrayList<Animal>();
public void action(Person person) {
for (Animal node : nodeList) {//需要能够遍历被访问者
node.accept(person);//被访问者被(谁)访问
}
}
//添加操作
public void add(Animal animal) {
nodeList.add(animal);
}
}
//测试
public class Client {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());
Owner owner = new Owner();
home.action(owner);
Someone someone = new Someone();
home.action(someone);
}
}
只要更换visitor的具体实现,即可切换算法
迭代器和访问者的比较:
- 迭代器:行为模式,用于按顺序访问聚合,而不公开其底层表示。因此,可以在迭代器后面隐藏列表、数组或类似的聚合。以遍历的方式访问集合数据而无需暴露其内部表示,将“遍历”这项功能delegate到外部的iterator对象
- Visitor:行为模式,用于在元素结构上执行操作,而不改变元素本身的实现。在特定ADT上执行某种特定操作,但该操作不在ADT内部实现,而是delegate到独立的visitor对象,客户端可灵活扩展/改变visitor的操作算法,而不影响ADT
策略模式和访问者模式的比较:
二者都是通过delegation建立两个对象的动态联系
-
但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。
-
而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。
区别:visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作),strategy则是站在内部ADT的角度,灵活变化对其内部功能的不同配置。