关于设计模式:设计模式学习笔记二十六访问者模式

1次阅读

共计 9872 个字符,预计需要花费 25 分钟才能阅读完成。

1 概述

1.1 引言

患者就医时,医生开具处方后通常由药房工作人员筹备药品,由划价人员依据药品数量计算总价,这里,能够将处方看作是一个药品信息的汇合,外面蕴含了一种或多种不同类型的药品信息,不同类型的工作人员在操作同一个药品信息汇合时将提供不同的解决形式,而且可能还会减少新类型的工作人员来操作处方单。

在软件开发中,有时候也须要解决像处方单这样的汇合构造,在该对象构造中存储了多个不同类型的对象信息,而且对同一对象构造中的元素的操作形式不惟一,可能须要提供多种不同的解决形式,还有可能减少新的解决形式。这时候能够应用访问者模式进行解决。

访问者模式是一种较为简单的行为型设计模式,它蕴含访问者与被访问者两个次要组成部分,这些被拜访的元素通常具备不同的类型,且不同的访问者能够对它们进行不同的拜访操作。访问者模式使得用户能够在不批改现有零碎的状况下扩大零碎的性能,为这些不同类型的元素减少新的操作。

1.2 定义

访问者模式:提供一个作用于某对象构造中的各元素的操作示意,它使得能够在不扭转各元素的类的前提下定义作用于这些元素的新操作。

访问者模式是一种对象行为型模式。

1.3 结构图

1.4 角色

  • Visitor(形象访问者):为每一个具体元素类申明一个具体访问者的操作
  • ConcreteVisitor(具体访问者):实现形象访问者中的操作
  • Element(形象元素):接口 / 抽象类,定义一个 accept 办法示意承受访问者的拜访
  • ConcreteElement(具体元素):实现了 accept 办法,在 accept 中调用访问者的拜访办法实现对具体元素的拜访
  • ObjectStructure(对象构造):形象元素的汇合,用于寄存形象元素对象,提供了遍历外部元素的办法

2 典型实现

2.1 步骤

  • 定义形象元素:申明一个 accept 办法示意承受访问者拜访,由具体元素实现具体拜访操作
  • 定义具体元素:实现形象元素中的 accept 办法,同时定义拜访属性的办法供访问者调用
  • 定义对象构造:应用 ListSet等存储形象元素汇合,蕴含治理汇合元素的办法,同时也蕴含 accept 办法,该办法会遍历元素并调用每个元素的 accept 办法
  • 定义形象访问者:申明 visit 办法,作为对具体元素的拜访办法,个别应用重载实现,也就是一个具体元素对应一个visit
  • 定义具体访问者:实现形象访问者中的拜访具体元素办法

2.2 形象元素

interface Element
{void accept(Visitor visitor);
}

这里实现为一个接口,蕴含一个 accept 办法,示意承受访问者的拜访。

2.3 具体元素

class ConcreteElementA implements Element
{
    @Override
    public void accept(Visitor visitor)
    {visitor.visit(this);
    }

    public void show1()
    {System.out.println("用第一种形式拜访具体元素 A");
    }

    public void show2()
    {System.out.println("用第二种形式拜访具体元素 A");
    }
}

class ConcreteElementB implements Element
{
    @Override
    public void accept(Visitor visitor)
    {visitor.visit(this);
    }

    public void show1()
    {System.out.println("用第一种形式拜访具体元素 B");
    }

    public void show2()
    {System.out.println("用第二种形式拜访具体元素 B");
    }
}

这里定义了两个具体元素,重点是其中的 accept 办法,通过参数 visitor,将本身(具体元素类)作为参数调用visit 办法,以示意该访问者(visitor)拜访该元素(this)。这里波及到了 ” 双分派 ”,简略来说就是运行时确定形象访问者(visitor)以及形象元素(this)的具体类型,上面会有一大节具体阐明分派的概念。

2.4 对象构造

class ObjectStructure
{private List<Element> list = new ArrayList<>();
    public void accept(Visitor visitor)
    {list.forEach(t->t.accept(visitor));
    }

    public void add(Element element)
    {list.add(element);
    }
}

应用一个汇合存储所有的形象元素,同时提供治理办法以注入或删除具体元素,也蕴含 accept 办法,接管一个形象访问者参数,示意承受该访问者拜访这个对象构造外面的所有具体元素。

2.5 形象访问者

interface Visitor
{void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
}

这里重载了 visit 实现对不同具体元素的拜访。留神一个具体元素类对应一个 visit 办法。

2.6 具体访问者

class ConcreteVisitorA implements Visitor
{
    @Override
    public void visit(ConcreteElementA element)
    {element.show1();
    }

    @Override
    public void visit(ConcreteElementB element)
    {element.show1();
    }
}

class ConcreteVisitorB implements Visitor
{
    @Override
    public void visit(ConcreteElementA element)
    {element.show2();
    }

    @Override
    public void visit(ConcreteElementB element)
    {element.show2();
    }
}

实现形象访问者中的拜访办法,获取具体元素对象后,通过该元素对象的私有办法获取其中的外部数据,或者间接调用具体元素对象的某些私有办法。

2.7 客户端

public static void main(String[] args)
{Element elementA = new ConcreteElementA();
    Element elementB = new ConcreteElementB();
    ObjectStructure elements = new ObjectStructure();
    elements.add(elementA);
    elements.add(elementB);
    Visitor visitor = new ConcreteVisitorA();
    elements.accept(visitor);
    visitor = new ConcreteVisitorB();
    elements.accept(visitor);
}

客户端只须要针对形象元素以及形象访问者进行编程,通过对象构造对元素进行对立的治理,增加具体元素到对象构造后,动静注入不同的访问者以不同的形式拜访对象构造中的所有元素。

输入如下:

3 层次结构

访问者模式中对象构造存储了不同类型的元素对象,以供不同访问者拜访。访问者模式包含两个层次结构:

  • 访问者层次结构:提供了形象访问者以及具体访问者
  • 元素层次结构:提供了形象元素以及具体元素

雷同的访问者能够以不同的形式拜访不同的元素,雷同的元素能够承受不同访问者以不同形式的拜访。

在访问者模式中:

  • 新增具体访问者不便:继承 / 实现形象访问者即可,同时定义拜访不同具体元素的不同办法
  • 新增具体元素类麻烦:减少新的具体元素类须要进行大幅度的批改,首先须要新增形象访问者中对新具体元素的拜访办法,其次,原有的具体访问者都须要对新办法进行实现,批改量极大

3.1 新增访问者

新增具体访问者很容易,在下面例子的根底上,只须要实现新增一个类实现形象访问者接口即可:

class ConcreteVisitorC implements Visitor
{
    @Override
    public void visit(ConcreteElementA element)
    {element.show1();
    }

    @Override
    public void visit(ConcreteElementB element)
    {element.show2();
    }
}

对于客户端只须要在对象构造中在 accept 中注入新的访问者即可:

public static void main(String[] args)
{Element elementA = new ConcreteElementA();
    Element elementB = new ConcreteElementB();
    ObjectStructure elements = new ObjectStructure();
    elements.add(elementA);
    elements.add(elementB);
    Visitor visitor = new ConcreteVisitorC();
    elements.accept(visitor);
}

3.2 新增具体元素

新增具体元素会导致大量源码的批改,在下面例子的根底上,首先减少一个实现形象元素接口的具体元素:

class ConcreteElementC implements Element
{
    @Override
    public void accept(Visitor visitor)
    {visitor.visit(this);
    }

    public void show1()
    {System.out.println("用第一种形式拜访具体元素 C");
    }

    public void show2()
    {System.out.println("用第二种形式拜访具体元素 C");
    }
}

这时 IDE 应该会提醒 visitor.visit(this) 这行报错,因为形象访问者接口没有针对新的具体元素类型的 visit 办法,也就是说此时须要批改形象访问者,减少拜访新的具体元素类型的 visit 办法:

interface Visitor
{void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
    void visit(ConcreteElementC element);// 新增
}

然而此时 IDE 又会提醒具体访问者有谬误,因为这是形象访问者是一个接口,而所有的具体访问者都实现了该接口,也就是下一步须要批改所有的具体访问者,减少新的接口办法:

class ConcreteVisitorA implements Visitor
{
    @Override
    public void visit(ConcreteElementA element)
    {element.show1();
    }

    @Override
    public void visit(ConcreteElementB element)
    {element.show1();
    }

    @Override
    public void visit(ConcreteElementC element) // 新增
    {element.show1();
    }
}

class ConcreteVisitorB implements Visitor
{
    @Override
    public void visit(ConcreteElementA element)
    {element.show2();
    }

    @Override
    public void visit(ConcreteElementB element)
    {element.show2();
    }

    @Override
    public void visit(ConcreteElementC element) // 新增
    {element.show2();
    }
}

对于客户端来说毋庸批改太多代码,同样创立具体元素后增加到对象构造中:

public static void main(String[] args)
{Element elementA = new ConcreteElementA();
    Element elementB = new ConcreteElementB();
    Element elementC = new ConcreteElementC();
    ObjectStructure elements = new ObjectStructure();
    elements.add(elementA);
    elements.add(elementB);
    elements.add(elementC);
    elements.accept(new ConcreteVisitorA());
}

3.3 扩大总结

新增访问者步骤如下:

  • 新建一个实现 / 继承形象访问者的具体访问者类
  • 客户端中将新的具体访问者传入对象构造的拜访办法中

新增元素步骤如下:

  • 新建一个实现 / 继承形象元素类的具体元素类
  • 形象访问者新增拜访该具体元素的办法
  • 原有的所有具体访问者新增拜访该元素的办法
  • 客户端中创立新元素对象后增加到对象构造中

总的来说,这和形象工厂模式有点相似,对 OCP(开闭准则)的反对具备歪斜性,新增访问者(产品族)很容易,新增元素(产品等级构造)须要批改大量代码。

4 实例

设计一个员工信息管理子系统,包含正式员工以及临时工,管理人员是人力资源部以及财务部的人员,两个部门的人员进行的操作不同,应用访问者模式进行设计。

设计如下:

  • 形象元素:Employee
  • 具体元素:FulltimeEmployee+ParttimeEmployee
  • 对象构造:EmployeeList
  • 形象访问者:Department
  • 具体访问者:FADepartment+HRDepartment

首先是形象元素的代码:

interface Employee
{void accept(Department department);
}

只有一个 accept 示意承受形象访问者拜访的办法。

具体元素:

class FulltimeEmployee implements Employee
{
    private String name;
    public FulltimeEmployee(String name)
    {this.name = name;}
    public String getName()
    {return name;}
    @Override
    public void accept(Department department)
    {department.visit(this);
    }
}

class ParttimeEmployee implements Employee
{
    private String name;
    public ParttimeEmployee(String name)
    {this.name = name;}
    public String getName()
    {return name;}
    @Override
    public void accept(Department department)
    {department.visit(this);
    }
}

实现其中的 accept 办法,在外面调用形象访问者的 visit 办法,将本身作为参数。

对象构造如下:

class EmployeeList
{private List<Employee> list = new ArrayList<>();
    public void add(Employee employee)
    {list.add(employee);
    }

    public void accept(Department department)
    {list.forEach(t->t.accept(department));
    }
}

accept会遍历元素汇合,实现访问者对每一个具体元素的拜访。

形象访问者如下:

interface Department
{void visit(FulltimeEmployee employee);
    void visit(ParttimeEmployee employee);
}

两个参数不同的visit,别离示意对这两个不同具体元素的拜访操作。

具体访问者:

class FADepartment implements Department
{
    @Override
    public void visit(FulltimeEmployee employee)
    {System.out.println("财务部拜访全职员工"+employee.getName());
    }
    @Override
    public void visit(ParttimeEmployee employee)
    {System.out.println("财务部拜访兼职员工"+employee.getName());
    }
}

class HRDepartment implements Department
{
    @Override
    public void visit(FulltimeEmployee employee)
    {System.out.println("人力资源部拜访全职员工"+employee.getName());
    }
    @Override
    public void visit(ParttimeEmployee employee)
    {System.out.println("人力资源部拜访兼职员工"+employee.getName());
    }
}

对于不同的具体元素,不同的具体访问者有不同的解决办法,这里简略解决只是进行输入。

测试:

public static void main(String[] args)
{Employee fulltimeEmployee = new FulltimeEmployee("A");
    Employee parttimeEmployee = new ParttimeEmployee("B");
    EmployeeList list = new EmployeeList();
    list.add(fulltimeEmployee);
    list.add(parttimeEmployee);
    list.accept(new HRDepartment());
    list.accept(new FADepartment());
}

客户端针对形象元素以及形象访问者编程,创立具体元素后增加到对象构造中,接着将具体访问者作为参数传入对象构造的拜访办法中。

输入:

5 分派

在访问者模式中波及到了“伪动静双分派”的概念,首先看一下什么是分派。

5.1 定义

变量被申明时的类型叫动态类型,变量所援用的类型叫理论类型。比方:

List<String> list = new ArrayList<>();

中,list的动态类型为List,理论类型为ArrayList

依据对象的类型对办法进行的抉择,就是分派。

分派依照分派的形式能够分为:

  • 动态分派
  • 动静分派

依照分派基于的宗量,能够分为:

  • 单分派
  • 多分派

先来看一下静 / 动静分派。

5.2 静 / 动静分派

5.2.1 动态分派

动态分派:产生在编译期间,分派依据动态类型信息产生,比方办法重载

比方上面的例子:

public class Test 
{public static void main(String[] args) 
    {test(Integer.valueOf(1));
        test("1");
    }

    public static void test(String s)
    {System.out.println("String");
    }

    public static void test(Integer i)
    {System.out.println("Integer");
    }
}

对于 test 办法,会依据动态类型抉择办法版本,根据 test 办法的参数类型和参数数量能够确定惟一一个重载办法版本。

5.2.2 动静分派

动静分派:产生在运行期间,动静置换掉某个办法,比方面向对象的多态个性

与动态分派相同,动静分派在运行时确定具体方法,比方:

public class Test 
{public static void main(String[] args) 
    {A b = new B();
        A c = new C();
        b.test();
        c.test();}
}

interface A
{void test();
}

class B implements A
{
    @Override
    public void test()
    {System.out.println("B 办法");
    }
}

class C implements A
{
    @Override
    public void test()
    {System.out.println("C 办法");
    }
}

例子的 test 办法,无奈依据对象的动态类型去判断,因为都是同一接口,而是在运行时判断,这就是动静分派,运行时获取到对象的具体援用类型,再确定具体的办法。

5.3 单 / 多分派

在理解单 / 多分派之前,先理解一下宗量。

一个办法所属的对象叫做办法的接收者,办法的接收者与办法的参数统称为办法的宗量。

比方上面的 Test 类:

public class Test
{public void print(String str){}}

print()属于 Test 对象,所以接收者就是 Test 对象,print()有参数 str,类型为String。所以print 的宗量有两个:

  • 接收者Test
  • 参数String str

依据分派基于多少种宗量,能够将分派划分为单分派与多分派:

  • 单分派依据一个宗量的类型对办法进行抉择
  • 多分派依据多个宗量的类型对办法进行抉择(双分派是多分派的一种模式,依据两个宗量的类型对办法进行抉择)

5.4 Java 语言个性

Java 是动态多分派,动静单分派语言。

理由如下:

  • 动态多分派:从下面办法重载的例子能够看到,动态时确定办法,而且抉择办法的根据是多个宗量(办法接收者,办法参数,参数数量,参数程序),因而能够说的动态多分派
  • 动静单分派:从下面动静分派的例子能够晓得,Java 中动静分派仅仅思考到办法的接收者,也就是只依据一个宗量(办法接收者)去抉择办法,所以说是动静单分派

5.5 伪动静双分派

一个办法依据两个宗量的类型来决定执行不同的代码,这就是双分派。Java 是动静单分派语言,也就是不反对动静双分派。然而应用访问者模式能够达到一种“动静双分派”的成果。因为这不是真正的动静双分派,所以加上了一个“伪”,这种“伪动静双分派”其实是通过两次“动静单分派”来实现。

访问者模式的双分派中,不仅要依据被访问者的运行时区别,还要依据访问者的运行时区别,在客户端中将具体访问者作为参数传递给被访问者(具体元素):

@Override
public void accept(Department department)
{department.visit(this);
}

因为 department 是形象访问者,运行时确定具体调用哪一个具体访问者的visit,这里实现第一次动静单分派。

另外 visit 承受形象元素作为参数,把具体元素(this)作为参数传递,依据 办法接收者宗量 抉择相应的 visit 办法,在这里实现第二次动静分派。

也就是说,访问者模式是首先依据访问者的动静单分派,再依据具体元素(被访问者)的动静单分派,来达到“动静双分派”的成果,因为这不是真正的动静双分派,而且 Java 是动静单分派语言,因而这种机制也叫“伪动静双分派”。

6 次要长处

  • 新增拜访操作不便:应用访问者模式,减少新的拜访操作就意味着减少一个新的具体访问者类,实现简略,合乎开闭准则
  • 集中拜访行为:将无关元素对象的拜访行为集中到一个访问者对象中,而不是扩散在一个个的元素类中,类的职责更加清晰,有利于对象构造中元素对象的复用,雷同的对象构造能够供多个不同的访问者拜访

7 次要毛病

  • 新增元素类艰难:访问者模式中每新增一个元素类认为着形象访问者角色须要减少一个新的形象操作,并在每一个具体访问者类中减少相应的具体操作,违反了开闭准则
  • 毁坏封装:访问者模式要求访问者对象拜访并调用每一个元素对象的操作,这意味着元素对象有时候必须裸露一些本人的外部操作和外部状态,否则无奈供访问者拜访

8 实用场景

  • 一个对象构造中蕴含多个类型的对象,心愿对这些对象施行一些依赖其具体类型的操作。在访问者中针对每一种具体类型都提供了一个拜访操作,不同类型的对象能够有不同的拜访操作
  • 须要对一个对象构造中的对象进行很多不同的并且不相干的操作,而且须要防止让这些操作“净化”这些对象的类,也不心愿在新增操作时批改这些类。访问者模式将相干的拜访操作集中起来定义在访问者类中,对象构造能够被多个不同的访问者者类所应用,将对象自身于对象的拜访操作拆散
  • 对象构造中对象对应的类很少扭转,但常常须要在此对象构造上定义新的操作

9 总结

如果感觉文章难看,欢送点赞。

同时欢送关注微信公众号:氷泠之路。

正文完
 0