策略模式和访问者模式有什么区别?

51

我不太理解这两种设计模式。

你可以给我一些上下文信息或示例,这样我就能清楚地了解它们之间的区别并进行比较。

谢谢。

在此输入图片描述

在此输入图片描述


2
也许这些文章可以帮助你:策略模式访问者模式 - beryllium
AS现在添加了两张图片。如果我通过将“Accept”签名更改为Accept(IVehical访问者)来更改策略模式,那么它几乎与访问者模式相同。对吗? - dotnetstep
1
访问者UML不完整,策略UML不准确。这可能是混淆的源头。 - jaco0646
13个回答

117

策略模式就像是一个一对多的关系。当我有一种类型的对象,并且想要对其应用多个操作时,我使用策略模式。例如,如果我有一个Video类封装了一个视频剪辑,我可能想以不同的方式压缩它。所以我创建了一堆策略类:

MpegCompression
AviCompression
QuickTimeCompression

等等等等。

我认为访问者模式就像一个多对多的关系。假设我的应用程序不仅包括视频,还包括音频剪辑,如果我坚持使用策略模式,我必须复制我的压缩类--一个用于视频和一个用于音频:

MpegVideoCompression
MpegAudioCompression

如果我转换到访问者模式,就不必重复策略类。我通过添加方法实现我的目标:

MpegCompressionVisitor::compressVideo(Video object)    
MpegCompressionVisitor::compressAudio(Audio object)

[更新:使用Java] 我在一个Java应用程序中使用了访问者模式。它与上面描述的有些不同。这里是该示例的Java版本。

// Visitor interface
interface Compressor {

  // Visitor methods
  void compress(Video object);
  void compress(Audio object);
}

// Visitor implementation
class MpegCompressor implements Compressor {
  
  public void compress(Video object) {
    // ...
  }

  public void compress(Audio object) {
    // ...
  }
}

现在需要访问的接口和类:

interface Compressible {

  void accept(Compressor compressor);
}

class Video implements Compressible {

  // If the Compressor is an instance of MpegCompressionVisitor,
  // the system prints "Mpeg video compression"
  void accept(Compressor compressor) {
    compressor.compress(this);
}

4
你也可以使用桥接来避免 MpegVideo/MpegAudio。 - Marcin Szymczak
@ahoffer,所以compressVideocompressAudio是我的两个visit函数。Video类将访问compressVideo,而Audio类将访问compressAudio。我理解得对吗? - EmptyData
@EmptyData 是的。compress 方法将是您的访问者方法。我会将 MpegCompression 类描述为访问者。Video 对象将被访问。 - ahoffer

16
策略模式用于将不同的算法暴露给标准化接口,一个典型例子是排序实用程序,允许用户(程序员)通过相同的接口调用不同的排序算法。
访问者模式则存在于不同的层面上。它详细说明了一种机制,即对象可以接受对另一个对象(访问者)的引用,该访问者公开了目标对象可以调用的预定接口。当然,不同的访问者会呈现相同的接口,但具有不同的实现。
回到我们的例子,一组排序算法可以通过策略模式或访问者模式来实现。
使用策略方法时,每个算法都呈现相同的接口,并以目标对象数组作为参数。使用访问者模式时,目标数组将以"访问"算法作为参数。在这种情况下,目标将"接受()"所选的访问者,并在调用目标的排序方法时调用其"访问()"方法。
两面同一枚硬币...
这样说通吗?

我刚刚添加了两张图片,这是为了更好地理解。你能在这个背景下提供一些想法吗? - dotnetstep
1
同一枚硬币的两面 +1 - medunes
远非同一枚硬币的两面。策略模式和访问者模式没有关联。 - jaco0646

13

访问者是一个具有多个方法的策略,并允许双重分派。访问者还允许在运行时两个具体对象之间进行安全绑定。

注意:这是一个Java示例。例如,C#引入了关键字dynamic,因此双重分派的示例在C#中无用。

策略模式

考虑以下示例和输出:

package DesignPatterns;

public class CarGarageStrategyDemo 
{
    public static interface RepairStrategy
    {
        public void repair(Car car);
    }

    public static interface Car
    {
        public String getName();
        public void repair(RepairStrategy repairStrategy);
    }

    public static class PorscheRepairStrategy implements RepairStrategy
    {
        @Override
        public void repair(Car car) {
            System.out.println("Repairing " + car.getName() + " with the Porsche repair strategy");
        }
    }
    
    public static class FerrariRepairStrategy implements RepairStrategy
    {
        @Override
        public void repair(Car car) {
            System.out.println("Repairing " + car.getName() + " with the Ferrari repair strategy");
        }
    }

    public static class Porsche implements Car
    {
        public String getName()
        {
            return "Porsche";
        }

        @Override
        public void repair(RepairStrategy repairStrategy) {
            repairStrategy.repair(this);
        }
    }

    public static void main(String[] args)
    {
        Car porsche = new Porsche();
        porsche.repair(new PorscheRepairStrategy()); //Repairing Porsche with the porsche repair strategy
    }
}

策略模式在策略和主题之间没有直接关系时能够正常工作。例如,我们不希望发生以下情况:

...
    public static void main(String[] args)
    {
        Car porsche = new Porsche();
        porsche.repair(new FerrariRepairStrategy()); //We cannot repair a Porsche as a Ferrari!
    }
...

因此,在这种情况下,我们可以使用访问者模式。

访问者

问题

考虑以下代码:

public class CarGarageVisitorProblem
{
    public static interface Car
    {
        public String getName();
    }

    public static class Porsche implements Car
    {
        public String getName()
        {
            return "Porsche";
        }
    }

    public static class Ferrari implements Car
    {
        public String getName()
        {
            return "Ferrari";
        }
    }

    public void repair(Car car)
    {
        System.out.println("Applying a very generic and abstract repair");
    }

    public void repair(Porsche car)
    {
        System.out.println("Applying a very specific Porsche repair");
    }

    public void repair(Ferrari car)
    {
        System.out.println("Applying a very specific Ferrari repair");
    }

    public static void main(String[] args)
    {
        CarGarageVisitorProblem garage = new CarGarageVisitorProblem();
        Porsche porsche = new Porsche();
        garage.repair(porsche); //Applying a very specific Porsche repair
    }
}

输出结果为 应用非常具体的保时捷修复方法。问题在于这条线不是抽象的,而是具体的:

Porsche porsche = new Porsche();

我们想要将其编写成(或在构造函数中注入Car的实例,我们想要应用“依赖倒转原则”):

Car porsche = new Porsche();

但是当我们改变这一行时,输出将会是:

Applying a very generic and abstract repair

不是我们想要的!

解决方案;使用双重派发(和访问者模式)

package DesignPatterns;

public class CarGarageVisitorExample 
{
    public static interface Car
    {
        public String getName();
        public void repair(RepairVisitorInterface repairVisitor);
    }

    public static class Porsche implements Car
    {
        public String getName()
        {
            return "Porsche";
        }

        public void repair(RepairVisitorInterface repairVisitor)
        {
            repairVisitor.repair(this);
        }
    }

    public static class Ferrari implements Car
    {
        public String getName()
        {
            return "Ferrari";
        }

        public void repair(RepairVisitorInterface repairVisitor)
        {
            repairVisitor.repair(this);
        }
    }

    public static interface RepairVisitorInterface
    {
        public void repair(Car car);
        public void repair(Porsche car);
        public void repair(Ferrari car);
    }

    public static class RepairVisitor implements RepairVisitorInterface
    {
        public void repair(Car car)
        {
            System.out.println("Applying a very generic and abstract repair");
        }

        public void repair(Porsche car)
        {
            System.out.println("Applying a very specific Porsche repair");
        }

        public void repair(Ferrari car)
        {
            System.out.println("Applying a very specific Ferrari repair");
        }
    }

    public static void main(String[] args)
    {
        CarGarageVisitor garage = new CarGarageVisitor();
        Car porsche = new Porsche();
        porsche.repair(new RepairVisitor()); //Applying a very specific Porsche repair
    }
}
由于方法重载,访问者和主题(Car)之间存在具体的绑定关系。 由于使用了方法重载,没有办法像修复法拉利那样修复保时捷。此外,我们通过实现此方法解决了先前解释的问题(即无法使用依赖反转)。
public void repair(RepairVisitorInterface repairVisitor)
{
    repairVisitor.repair(this);
}

this 引用将返回对象的具体类型,而不是抽象类型(Car)。


策略模式通过组合实现,但此示例中未显示。 - jaco0646
1
严格来说,使用组合模式时,您无法在不创建新主题的情况下更改策略/访问者。另一方面,组合模式也可以与访问者模式一起使用。问题是,哪种情况最适合您。Car porsche = new Porsche(new CarGarageVisitor()); porsche.repair(); - Douma
策略可以通过在上下文中设置新策略来改变。访问者模式不使用组合。我已经在下面的回答中添加了GoF UML。OP问题的根源在于其UML是不正确的,因此问题基于错误的前提。 - jaco0646

12
访问者像一夜情 - 当调用接受函数时创建它,然后它们被分离,访问者可以从内存中清除,不会占用使用它的类的任何空间。
策略像婚姻 - 你创建对象,它生存在使用它的类中,占用内存,有一个房间并在早上为自己冲泡咖啡。当然,它们可能会离婚并切换到另一个类,但是该类也将生活在其所有者的上下文中。
希望这有助于您记住 :)

我也是这么认为的。策略是一个对象从一开始就需要履行职责的东西。而访问者是可选的,可以经过但不一定要经过。 - velop

3

区别在于 Visitor 通过运算符重载为元素的子类提供了不同的行为。它知道正在处理或访问的对象类型。

与此相反,策略模式在所有实现中都将保持一致的接口。

访问者用于允许一个对象的子部分使用一种一致的方法来做某事。策略用于允许依赖注入如何做某事。

因此,这将是一个访问者:

class LightToucher : IToucher{
    string Touch(Head head){return "touched my head";}
    string Touch(Stomach stomach){return "hehehe!";}
}

使用另一种相同类型的方法

class HeavyToucher : IToucher{
   string Touch(Head head){return "I'm knocked out!";}
   string Touch(Stomach stomach){return "oooof you bastard!";}
}

我们有一个类,可以使用这个访问者来完成其工作,并根据它进行更改。
class Person{
    IToucher visitor;
    Head head;
    Stomach stomach;
    public Person(IToucher toucher)
    {
          visitor = toucher;

          //assume we have head and stomach
    }

    public string Touch(bool aboveWaist)
    {
         if(aboveWaist)
         {
             visitor.Touch(head);
         }
         else
         {
             visitor.Touch(stomach);
         }
    }
}

如果我们这样做:

var person1 = new Person(new LightToucher());

var person2 = new Person(new HeavyToucher());

其中,person1使用了轻触摸器,person2使用了重触摸器。

        person1.Touch(true); //touched my head
        person2.Touch(true);  //knocked me out!

2
如果我们查看GoF书中这两个模式的UML图,我们会发现它们完全不同。
访问者模式:

Visitor UML


策略:

Strategy UML


从图表中可以看出一些重要的区别。

  • 策略模式基于组合。访问者没有组合关系。
  • 策略模式基于对接口进行编码。访问者基于对实现进行编码。
  • 策略模式以多种方式实现一个操作。访问者实现多个操作。

仅使用UML无法捕捉驱动这些模式的不同动机。

  • 策略模式是面向对象的解决方案,适用于没有一级函数的语言。随着现代语言采用闭包或lambda,策略模式的需求越来越少。
  • 访问者是面向对象的解决方案,适用于没有模式匹配或多重分派的语言。随着现代语言采用这些特性,访问者也变得过时。

2
两种方法都是用于调度(在运行时决定调用哪个函数)。区别在于您是支持算法(访问者)还是对象(策略):
- 使用策略模式,在主对象中注入策略(对象/函数/lambda),每次调用特定方法时,我们将该调用委托给当前活动策略来执行。 - 使用访问者模式,我们不从对象中委派,而是将我们的对象传递给所谓的访问者对象/函数,它根据此参数选择要应用于我们传递的对象的算法(通常通过重载分辨率)。
因此,如果您有一个对象和多个算法(1:n),则使用策略模式很有意义;反之,如果您有多个对象和仅一个算法,则最好使用访问者模式(n:1)。
但是,如果您有多个对象和多个算法呢?
在这种情况下,所选算法将取决于另一个参数,对吧?因此,我们使用一种双重调度方法,它基于我们的对象和附加参数选择算法。这通常也通过重载分辨率完成,因此该模式大多类似于访问者模式(n:n)。
简而言之:
begin(obj); // <-- visitor
obj.begin(); // <-- strategy

(*) 这个比较是基于这样的假设:begin() 方法会根据 obj 中设置的当前策略来改变其行为,而对于 visitor,我们必须假设有不同的 begin() 重载方法存在(但通常打包在一个 visitor 对象中),并且根据我们放入其中的对象选择正确的方法。

2

我认为策略模式是一种将方法/策略注入对象的方式,但通常该方法的签名需要一些值参数并返回结果,因此它与策略的使用者没有耦合关系:

来自维基百科

class Minus : ICalculateStrategy {
    public int Calculate(int value1, int value2) {
        return value1 - value2;
    }
}

双重分派将访问者与用户耦合在一起,并通常保留状态。

这里有一个很好的例子(点击这里),我会从那里复制内容:

public class BlisterPack
{
    // Pairs so x2
    public int TabletPairs { get; set; }
}

public class Bottle
{
    // Unsigned
    public uint Items { get; set; }
}

public class Jar
{
    // Signed
    public int Pieces { get; set; }
}

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int) bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

正如您所看到的,访问者有状态(public int Count),并且它对BlisterPack、Bottle和Jar等已知类型的列表进行操作。因此,如果您想要支持新类型,您需要通过添加该类型来更改所有访问者。

此外,由于"visitor.Visit(this)",它与它所操作的类型耦合在一起。如果我从瓶子中删除或更改"Items"属性会发生什么呢?...所有访问者都会失败。


0

对我来说,第二张图看起来像是访问者模式...因为对于策略模式,类包含的数据结构往往只有一个,没有子类(或者子类保持相同的行为)。策略是针对相同结构上的不同操作。


0

如果您只有一个单一的上下文或元素,并且需要对该上下文执行不同的操作,则可以选择策略模式。这是上面答案中提到的1:M 关系java.util.Comparator是策略设计模式在实际应用中的一个很好的例子。我们可以为相同的集合(上下文或元素)设置不同的排序策略。

另一方面,假设您有多个元素都符合共同的契约,并且需要对每个元素执行不同的操作。例如,考虑一个洗车用例,其中有车身、发动机和轮等部件,每个部件都可以使用蒸汽或水进行清洗。这是访问者模式的一个很好的应用场景。但请确保您的上下文元素保持完整且永远不会更改。如果元素将要更改,例如添加一个Door元素到Car中,则需要更改所有访问者,在每个访问者中添加一个新方法并违反模式的OCP性质。因此,这是上面答案中所述的M:N关系。

如果你对了解这两种设计模式间微妙的差异更深入地感兴趣,我建议你阅读this article

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接