设计模式学习笔记--Visitor 访问者模式

  访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话,接受这个操作的数据结构则可以保持不变。
  打个比喻,好像一棵树子(某种数据结构),以前上面只能挂一种果实,采用一种操作方法。而现在,上面既可以挂苹果,也可以挂梨子,甚至还可以挂香蕉(不同类型的对象),而我们的操作方法即可以用手摘,也可以用挂钩拉,还可以用钳子夹(作用于结构上的不同的操作),当然以后还可以包括其它任何我们想要采取的办法。那么应对此种变化,我们就引入了访问者模式。
   访问者模式适用于数据结构相对未定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。数据结构的每一个节点都可以接受一个访问者的调用,此节点向访问者对象传入节点对象,而访问者对象则反过来执行节点对象的操作。这样的过程叫做"双重分派"。节点调用访问者,将它自己传入,访问者则将某算法针对此节点执行。
   由上可以看到,双重分派是理解访问者模式的重点。而为了理解双重分派,我们要循序渐近地学习以下概念:
   1、静态分派   2、动态分派   3、单重分派  4、双重分派
 首先,我们的程序如下图:

         

 
  一、关于分派
   1、静态分派 
 

     StaticDispatch.cs代码

ContractedBlock.gifExpandedBlockStart.gifCode
using System;
using
 System.Collections.Generic;
using
 System.Linq;
using
 System.Text;

namespace
 MyVisitor
{
    
public class
 Bird { };
    
public class
 Eagle : Bird { };
    
public class
 Swallow : Bird { };

    
public class
 Person
    {
        
public void
 feed(Bird bird)
        {
            Console.WriteLine(
"喂鸟!"
);
        }

        
public void
 feed(Eagle eaglebird)
        {
            Console.WriteLine(
"喂老鹰!"
);
        }

        
public void
 feed(Swallow  swallowbird)
        {
            Console.WriteLine(
"喂燕子!"
);
        }
    }
}

  客户应用代码

ContractedBlock.gifExpandedBlockStart.gifCode
            #region 静态分派
            
//因为重载是静态分派,在编译器执行时,取决于变量的声明类型,
            
//因为eagle与swallow在声明时都是Bird(如下),所以调用的都是fee(Bird bird)的函数

            Console.WriteLine("-----------静态分派---------------");
            Bird eagle 
= new Eagle();  //虽然创建是Eagle,但声明是Bird

            Bird swallow = new Swallow(); //虽然创建是Swallow,但声明是Bird
            Person p = new Person();
            p.feed(eagle);
            p.feed(swallow);
            Console.ReadKey();
            
#endregion

运行效果如下:

   2、动态分派

DynamicDispatch.cs代码如下:

ContractedBlock.gifExpandedBlockStart.gifCode
using System;
using
 System.Collections.Generic;
using
 System.Linq;
using
 System.Text;

namespace
 MyVisitor
{
   
public  class
 Dog
    {
        
public virtual void
 bark()
        {
            Console.WriteLine(
"狗在吠!"
);
        }
    }

    
public class
 WildDog : Dog 
    {
        
public override void
 bark()
        {
            Console.WriteLine(
"野狗在吠!"
);
        }
    }
}

客户应用代码如下:

ContractedBlock.gifExpandedBlockStart.gifCode
            #region 动态分派
            Console.WriteLine(
"-----------静态分派---------------");
            
//
在执行时发生了向下转型,就是动态分派,
            
//所以虽然我们声明的是Dog,但我们new的是WildDog,因此结果不是"狗吠",而是"野狗在吠"

            Dog dog = new WildDog();
            dog.bark();
            Console.ReadKey();
            
#endregion

运行效果如下:

  

3、单重分派

SingleDispatch.cs代码如下: 

ContractedBlock.gifExpandedBlockStart.gifCode
using System;
using
 System.Collections.Generic;
using
 System.Linq;
using
 System.Text;

namespace
 MyVisitor
{
   
public  class
 book
    {
        
public virtual void
 read()
        {
            Console.WriteLine(
"Book reading"
);
        }
    }

    
public class
 LifeBook : book
    {
        
public override void
 read()
        {
            Console.WriteLine(
"LifeBook reading"
);
        }
    }

    
public class
 ScienceBook : book
    {
        
public override void
 read()
        {
            Console.WriteLine(
"Science reading"
);
        }
    }

}

客户应用代码如下: 

ContractedBlock.gifExpandedBlockStart.gifCode

            
#region 单重分派
            
//单分派(single dispatch)就是说我们在选择一个方法的时候仅仅需要根据消息接收者(receiver)的运行时型别(Run time type)。
            
//
实际上这也就是我们经常提到的多态的概念。举一个简单的例子,我们有一个基类book,Book有一个虚方法read(可被子类override),
            
//
LifeBook和ScienceBook是Book的两个子类,在LifeBook和ScienceBook中我们覆写(override)了方法read。
            
//这样我们对消息read的调用,需要根据接收者Book或者Book的子类LifeBook/ScienceBook的具体型别才可以确定具体是调用Book的还是LifeBook/ScienceBook的read方法。

            Console.WriteLine("-----------单重分派---------------");
            book bk1 
= new
 book();
            bk1.read();
            book bk2 
= new
 LifeBook();
            bk2.read();
            book bk3 
= new
 ScienceBook();
            bk3.read();
            Console.ReadKey();
            
#endregion

运行效果如下:
  

4、双重分派

DoubleDispatch.cs代码如下:

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
#region 定义了一个抽象类Animal,它是Human与Cat类的基类

    
public abstract class Animal
    {
        
public virtual void face(Animal animal) { } 
        
public virtual void face(Human human) { }
        
public virtual void face(Cat cat) { }
    }
    
#endregion

    
#region 定义Human类,继承自Animal抽象类
    
public class Human : Animal
    {
        
#region face(animal)
        
public override void face(Animal animal)
        {
            animal.face(
this); //*****把此处解除注释实现双重分派*****注意:是此行,而不是其它行
            Console.WriteLine("Called:Human--face(Animal animal)");
        }
        
#endregion

        
#region face(human)
        
public override void face(Human human)
        {
            
//human.face(this);  //把此处解除注释实现"双重/多重"分派
            Console.WriteLine("Called:Human--face(Human human)");
        }
        
#endregion

        
#region face(cat)
        
public override void face(Cat cat)
        {
            
//cat.face(this);  //把此处解除注释实现"双重/多重"分派
            Console.WriteLine("Called:Human--face(Cat cat)");
        }
        
#endregion
    }
    
#endregion

    
#region 定义Cat类,继承自Animal抽象类
    
public class Cat : Animal
    {
        
#region face(animal)
        
public override void face(Animal animal)
        {
            
//animal.face(this);//把此处解除注释实现"双重/多重"分派
            Console.WriteLine("Called:Cat--face(Cat cat)");
        }
        
#endregion

        
#region face(human)
        
public override void face(Human human)
        {
            
// human.face(this); //把此处解除注释实现"双重/多重"分派
            Console.WriteLine("Called:Cat--face(Human human)");
        }
        
#endregion

        
#region face(cat)
        
public override void face(Cat cat)
        {
            
//cat.face(this); //把此处解除注释实现"双重/多重"分派
            Console.WriteLine("Called:Cat--face(Cat cat)");
        }
        
#endregion
    }
    
#endregion

}

客户应用代码如下:

ContractedBlock.gifExpandedBlockStart.gifCode
            Console.WriteLine("-----------双重分派---------------");
            Animal human 
= new
 Human();
            Animal cat 
= new
 Cat();
            human.face(cat);
            Console.ReadKey();

代码说明: 
            (a)关于face方法   (方法调用者的类型是运行期确定)
            尽管human声明为Animal类型,但human.face调用的是Human类中的override的face函数,这是因为动态分派只会体现在方法的调用者身上
            因为此处face方法的调用者是Human类型,虽然声明的是Animal类型,但new的却是Human类型,所以只会调用Human类中override的face方法
            因此显示了"动态分派"的特性
            (b)关于face方法中的参数  (方法参数的类型是编译期确定)
            接上面,我们知道实际调用的是Human类中的face方法,我们也知道在Human类中我们override了三个face方法,分别对应animal,human,cat三种类型的参数
            现在的问题是,此处我们定义了Animal cat = new Cat(),并把cat作为参数传入human.face(cat),那么,它将激发Human类中的哪一个override的face方法呢
            运行后,我们发现,是调用了public override void face(Animal animal)这个方法。这是因为方法的参数类型会在编译期由编译器决定,所以显示了"静态分派"的特性
            针对Animal cat = new Cat()虽然我们new 的是Cat类型,但声明的是Animal,所以在编译时对于human.face(cat)内的参数cat,类型其实确定为Animal,因此,传到了
            human.face方法中,调用的是public override void face(Animal animal)方法

            根据(a)(b),我们推断:如果参数cat的类型也能够在运行期决定,那么哪个face被调用就由方法调用者和方法参数共同在运行期决定了。
            那么如何实现参数类型在运行期绑定呢?既然方法调用者的类型是运行期才确定的,那么我们就可以反客为主了,将方法参数变成方法调用者。如下:
            face(Cat arg) {arg.face(this);}
            下面,我们把DoubleDispatch类中关于face方法定义代码段中的 //*****把此处解除注释实现双重分派***** 那一排的注释去掉就可以看到双重分派的效果。
            至此,我们应该明白了双重分派的涵义:哪个face最终被调用经过两次运行期类型绑定才确定下来,这样的过程就是双重分派了。进一步,自然可以理解多重分派。

运行效果如下:

二、进入访问者模式
 访问者模式的UML图如下:

        

 其相关角色如下:
 1、抽象访问者(Visitor)角色:声明了一个或者多个访问操作,形成所有的具体元素角色必须实现的接口。
 2、具体访问者(ConcreteVisitor)角色:实现抽象访问者角色所声明的接口,也就是抽象访问者所声明的各个访问操作。
 3、抽象节点(Element)角色:声明一个接受操作,接受一个访问者对象作为一个参量。
 4、具体节点(ConcreteElement)角色:实现了抽象元素所规定的接受操作。
 5、结构对象(ObiectStructure)角色:有如下的一些责任,可以遍历结构中的所有元素;如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;如果需要,可以设计成一个复合对象或者一个聚集,如列(List)或集合(Set)。  
   (一)、访问者模式基本思路示例
  1、抽象访问者(Visitor)角色:

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
#region 抽象访问者(Visitor)角色
    
//声明了一个或者多个访问操作,形成所有的具体元素角色必须实现的接口。
    abstract class Visitor
    {
         
public abstract void VisitConcreteElementA(ConcreteElementA concreteElementA);
         
public abstract void VisitConcreteElementB(ConcreteElementB concreteElementB);
    }
    
#endregion
}

  2、具体访问者(ConcreteVisitor)角色:

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//这时定义了不同的具体访问者,它们实现了抽象访问者角色所声明的接口,也就是抽象访问者所声明的各个访问操作。 
    
//每一次具体访问者都可以访问不同类型的元素节点

    
#region 具体访问者(ConcreteVisitor)角色一
    
class ConcreteVisitor1: Visitor
    {
        
#region 访问ConcreteElementA类型的元素
        
public override void VisitConcreteElementA(ConcreteElementA concreteElementA)
        {
            Console.WriteLine(
"{0}被{1}访问",concreteElementA.GetType().Name,this.GetType().Name);
        }
        
#endregion

        
#region 访问ConcreteElementB类型的元素
        
public override void VisitConcreteElementB(ConcreteElementB concreteElementB)
        {
            Console.WriteLine(
"{0}被{1}访问",concreteElementB.GetType().Name,this.GetType().Name);
        }
        
#endregion
    }
    
#endregion

    
#region 具体访问者(ConcreteVisitor)角色二
    
class ConcreteVisitor2 : Visitor
    {
        
#region 访问ConcreteElementA类型的元素
        
public override void VisitConcreteElementA(ConcreteElementA concreteElementA)
        {
            Console.WriteLine(
"{0}被{1}访问", concreteElementA.GetType().Name, this.GetType().Name);
        }
        
#endregion

        
#region 访问ConcreteElementB类型的元素
        
public override void VisitConcreteElementB(ConcreteElementB concreteElementB)
        {
            Console.WriteLine(
"{0}被{1}访问", concreteElementB.GetType().Name, this.GetType().Name);
        }
        
#endregion
    }
    
#endregion
}

  3、抽象节点(Element)角色:

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
#region 抽象节点(Node)角色
    
//声明一个接受操作,接受一个访问者对象作为一个参量,但具体是哪个访问者对象则在运行时通过双重分派机制来决定

    
abstract  class Element
    {
        
public abstract void Accept(Visitor visitor);
    }
    
#endregion
}

  4、具体节点(ConcreteElement)角色:

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//此处定义了具体节点(Node)角色,它们实现了抽象元素所规定的接受操作。 

    
#region 具体节点角色A
    
class ConcreteElementA : Element
    {
        
public override void Accept(Visitor visitor)
        {
            visitor.VisitConcreteElementA(
this);
        }
        
public void OperationA()
        {

        }
    }
    
#endregion

    
#region 具体节点角色B
    
class ConcreteElementB : Element
    {
        
public override void Accept(Visitor visitor)
        {
            visitor.VisitConcreteElementB(
this);
        }
        
public void OperationB()
        {

        }
    }
    
#endregion
}

  5、结构对象(ObiectStructure)角色:

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
class ObjectStructure
    {
        
//结构对象(ObiectStructure)角色:有如下的一些责任,可以遍历结构中的所有元素;
        
//如果需要,提供一个高层次的接口让访问者对象可以访问每一个元素;
        
//如果需要,可以设计成一个复合对象或者一个聚集,如列(List)或集合(Set)。 

        
//它就像一棵大树(某种数据结构),上面可以挂苹果,也可以挂梨子,甚至可以挂香蕉(不同类型的对象)
        
//而我们即可以用手摘,也可以用挂钩拉,还可以用钳子夹(作用于结构上的不同的操作)

        
private List<Element> _elements = new List<Element>();

        
#region  加入新元素到此结构中
        
//注意:加入的元素可以是不同的具体类型,即:不同类的具体节点角色
        public void Attach(Element element)
        {
            _elements.Add(element);
        }
        
#endregion

        
#region 移除一个元素到此结构中
        
public void Detach(Element element)
        {
            _elements.Remove(element);
        }
        
#endregion

        
public void Accept(Visitor visitor)
        {
            
//遍历结构中的所有元素
            foreach (Element element in _elements)
            {
                element.Accept(visitor);
            }
        }
    }
}

  6、客户应用代码

ContractedBlock.gif ExpandedBlockStart.gif Code
            #region 基本思路示例
            Console.WriteLine(
"-----------基本思路示例---------------");
            
//建立结构
            ObjectStructure o = new ObjectStructure();
            o.Attach(
new ConcreteElementA());
            o.Attach(
new ConcreteElementB());
            
//创建visitor对象
            ConcreteVisitor1 v1 = new ConcreteVisitor1();
            ConcreteVisitor2 v2 
= new ConcreteVisitor2();

            o.Accept(v1);
            o.Accept(v2);
            Console.ReadKey();

            
#endregion

  运行效果如下:

   (二)、雇员节点的访问者模式法例
  1、抽象访问者(Visitor)角色:IEmpVisitor

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//抽象访问者(Visitor)角色:
    interface  IEmpVisitor
    {
        
void Visit(EmpElement element);
    }
}

  2、具体访问者(ConcreteVisitor)角色:EmpConcreteVisitors

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//此处定义了两个具体访问者(ConcreteVisitor)角色:IncomVisitor具体访问者与VacationVisitor具体访问者


    
#region IncomVisitor具体访问者
    
class IncomeVisitor : IEmpVisitor 
    {
        
public void Visit(EmpElement  element)
        {
            Employee employee 
= element as Employee;
            
// Provide 10% pay raise
            employee.Income *= 1.10;
            Console.WriteLine(
"{0} {1}'s new income: {2:C}",
            employee.GetType().Name, employee.Name, employee.Income);
        }
    }
    
#endregion

    
#region VacationVisitor具体访问者
    
class VacationVisitor : IEmpVisitor
    {
        
public void Visit(EmpElement element)
        {
            Employee employee 
= element as Employee;
            
// Provide 3 extra vacation days
            Console.WriteLine("{0} {1}'s new vacation days: {2}",
              employee.GetType().Name, employee.Name, employee.VacationDays);
        }
    }
    
#endregion


}

  3、抽象节点(Element)角色:EmpElement

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//抽象节点(Element)角色:
    abstract  class EmpElement
    {
        
public abstract void Accept(IEmpVisitor visitor);
    }
}

  4、具体节点(ConcreteElement)角色:EmpBaseElement--EmpConcreteElements
EmpBaseElement

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//定义了具体节点(ConcreteElement)角色的基类,由它会派生出几个不同的具体节点角色
    #region  Employee
    
class Employee : EmpElement
    {
        
#region 姓名属性
        
private string _name;

        
public string Name
        {
            
get { return _name; }
            
set { _name = value; }
        }
        
#endregion

        
#region 收入属性
        
private double _income;
        
public double Income
        {
            
get { return _income; }
            
set { _income = value; }
        }
        
#endregion

        
#region 假期属性
        
private int _vacationDays;
        
public int VacationDays
        {
            
get { return _vacationDays; }
            
set { _vacationDays = value; }
        }
        
#endregion 

        
#region 构造函数
        
public Employee(string name, double income, int vacationDays)
        {
            
this._name = name;
            
this._income = income;
            
this._vacationDays = vacationDays;
        }
        
#endregion

        
#region Accept方法
        
public override void Accept(IEmpVisitor visitor)
        {
            visitor.Visit(
this);
        }
        
#endregion

    }
    
#endregion

}

EmpConcreteElements

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//定义了三个具体节点类型,它们均继承自Employee基类,则Employee基类又继承自IEmpVisitor接口
    #region 一般职员 
    
class Clerk : Employee
    {  
// 构造函数
        public Clerk()  : base("Hank"25000.014)
        {
        }
    }
    
#endregion

    
#region 部门领导
    
class Director : Employee
    {  
// 构造函数
        public Director(): base("Elly"35000.016)
        {
        }
    }
    
#endregion

    
#region 总裁
    
class President : Employee
    {  
// 构造函数
        public President() : base("Dick"45000.021)
        {
        }
    }
    
#endregion


}

  5、结构对象(ObiectStructure)角色:EmpStructure

ContractedBlock.gif ExpandedBlockStart.gif Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyVisitor
{
    
//结构对象(ObiectStructure)角色:

    
class EmpStructure
    {
        
private List<Employee> _employees = new List<Employee>();

        
#region 添加节点到结构中
        
public void Attach(Employee employee)
        {
             _employees.Add(employee);
        }
        
#endregion

        
#region 从结构中删除节点
        
public void Detach(Employee employee)
        {
            _employees.Remove(employee);
        }
        
#endregion

        
public void Accept(IEmpVisitor visitor)
        {
            
foreach (Employee e in _employees)
            {
                e.Accept(visitor);
            }
            Console.WriteLine();
        }


    }
}

  6、客户应用代码

ContractedBlock.gif ExpandedBlockStart.gif Code
            #region 雇员结构访问示例
            Console.WriteLine(
"-----------雇员结构访问示例---------------");
            
// Setup employee collection
            EmpStructure e = new EmpStructure();
            e.Attach(
new Clerk());
            e.Attach(
new Director());
            e.Attach(
new President());

            
// Employees are 'visited'
            e.Accept(new IncomeVisitor());
            e.Accept(
new VacationVisitor());

            Console.ReadKey();
            
#endregion

 运行效果如下:

 
 总结

1、 在什么情况下应当使用访问者模式
有意思的是,在很多情况下不使用设计模式反而会得到一个较好的设计。换言之,每一个设计模式都有其不应当使用的情况。访问者模式也有其不应当使用的情况,让我们
先看一看访问者模式不应当在什么情况下使用。

倾斜的可扩展性

访问者模式仅应当在被访问的类结构非常稳定的情况下使用。换言之,系统很少出现需要加入新节点的情况。如果出现需要加入新节点的情况,那么就必须在每一个访问对象里加入一个对应于这个新节点的访问操作,而这是对一个系统的大规模修改,因而是违背"开一闭"原则的。

访问者模式允许在节点中加入新的方法,相应的仅仅需要在一个新的访问者类中加入此方法,而不需要在每一个访问者类中都加入此方法。

显然,访问者模式提供了倾斜的可扩展性设计:方法集合的可扩展性和类集合的不可扩展性。换言之,如果系统的数据结构是频繁变化的,则不适合使用访问者模式。

"开一闭"原则和对变化的封装

面向对象的设计原则中最重要的便是所谓的"开一闭"原则。一个软件系统的设计应当尽量做到对扩展开放,对修改关闭。达到这个原则的途径就是遵循"对变化的封装"的原则。这个原则讲的是在进行软件系统的设计时,应当设法找出一个软件系统中会变化的部分,将之封装起来。

很多系统可以按照算法和数据结构分开,也就是说一些对象含有算法,而另一些对象含有数据,接受算法的操作。如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较合适的,因为访问者模式使得算法操作的增加变得容易。

反过来,如果这样一个系统的数据结构对象易于变化,经常要有新的数据对象增加进来的话,就不适合使用访问者模式。因为在访问者模式中增加新的节点很困难,要涉及到在抽象访问者和所有的具体访问者中增加新的方法。
2、 使用访问者模式的优点和缺点
 访问者模式有如下的优点:

访问者模式使得增加新的操作变得很容易。如果一些操作依赖于一个复杂的结构对象的话,那么一般而言,增加新的操作会很复杂。而使用访问者模式,增加新的操作就意味着增加一个新的访问者类,因此,变得很容易。
访问者模式将有关的行为集中到一个访问者对象中,而不是分散到一个个的节点类中。
访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。迭代子只能访问属于同一个类型等级结构的成员对象,而不能访问属于不同等级结构的对象。访问者模式可以做到这一点。
积累状态。每一个单独的访问者对象都集中了相关的行为,从而也就可以在访问的过程中将执行操作的状态积累在自己内部,而不是分散到很多的节点对象中。这是有益于系统维护的优点。
 访问者模式有如下的缺点:

增加新的节点类变得很困难。每增加一个新的节点都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作。
破坏封装。访问者模式要求访问者对象访问并调用每一个节点对象的操作,这隐含了一个对所有节点对象的要求:它们必须暴露一些自己的操作和内部状态。不然,访问者的访问就变得没有意义。由于访问者对象自己会积累访问操作所需的状态,从而使这些状态不再存储在节点对象中,这也是破坏封装的。

 

前往:设计模式学习笔记清单
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值