使用策略模式在 PHP 中的优势

17

我似乎无法理解策略模式提供的优势。请看下面的例子。

//Implementation without the strategy pattern
class Registry {

    public function Func1(){
         echo 'called function 1';
    }

    public function Func2(){
         echo 'called function 2';
    }

}

$client = new Registry();
$client->Func1();
$client->Func2();

//Implementation with strategy pattern
interface registry {
     public function printMsg();
}

class Func1 implements registry {
     public function printMsg(){
         echo 'called function 1';
    }
}

class Func2 implements registry {
     public function printMsg(){
         echo 'called function 2';
    }
}

class context {

      public function printMsg(Registry $class){
          $class->printMsg();
      }
}

$client = new context();
$client->printMsg(new Func1());
$client->printMsg(new Func2());
在上述两个示例中,策略模式将提供哪些优势,并且它比第一种方法更好?为什么应该使用策略模式?
上面的示例代码可能包含错误,请忽略代码。
6个回答

35
策略模式的意图是:
定义一组算法,将每个算法封装起来,使它们可以互换。策略模式让算法独立于使用它的客户端而变化。[GoF:349]
要理解这是什么意思,你需要(我强调):
考虑在设计中应该是可变的内容。这种方法与专注于重新设计原因相反。不是考虑什么可能会迫使设计发生变化,而是考虑不需要重新设计就可以进行更改的内容。这里的重点是封装变化的概念,这是许多设计模式的主题。[GoF:29]
换句话说,策略是相关的代码片段,您可以在运行时将其插入客户端(另一个对象)以更改其行为。这样做的一个原因是防止每次添加新行为时都必须触及客户端(参见开放封闭原则(OCP)受保护的变化)。此外,当您拥有足够复杂的算法时,将它们放入自己的类中有助于遵循单一职责原则(SRP)
我认为您问题中的示例不太适合理解策略模式的有用性。注册表不应该有printMsg()方法,我对该示例的意义无法理解。一个更简单的示例是我在我可以将代码包含到PHP类中吗?中提供的示例(其中我谈论策略的部分大约在答案的中间位置)。

无论如何,在您的第一段代码中,注册表实现了Func1和Func2方法。由于我们假设它们是相关算法,所以让我们假装它们真的是persistToDatabase()persistToCsv(),以便更好地理解。同时,想象一下,在应用程序请求结束时,您从shutdown handler(客户端)调用其中一个方法。

但是,调用哪个方法取决于您的配置,相应的标志显然存储在注册表中。因此,在客户端中,您最终得到类似以下内容的东西

switch ($registry->persistStrategy)
{
    case 'database':
        $registry->persistToDatabase();
    case 'csv':
        $registry->persistToCsv();
    default:
        // do not persist the database
}

但是像这样的switch语句很糟糕(参见CleanCode:37ff)。想象一下,您的客户请求您添加一个persistToXml()方法。现在不仅需要更改Registry类以添加另一个方法,还需要更改客户端以适应该新功能。这是两个类需要更改,而OCP告诉我们,我们的类应该关闭修改。为了改善这种情况,一种方法是在Registry上添加通用的persist()方法,并将switch/case移动到其中,这样客户端只需要调用此通用方法即可。
$registry->persist();

这样做已经好了一些,但是它仍然需要我们使用switch/case,并且每次添加新的持久化方式时都需要修改注册表。

现在想象一下,如果你的产品是一个被许多开发者使用的框架,并且他们想出了自己的持久化算法,那该怎么办呢?他们必须扩展你的类,但是他们还必须替换框架中所有使用你的类的地方。或者他们只是将它们写入你的类中,但是每次你提供新版本时,他们都必须打补丁来更新类。所以这就是一堆麻烦事。

策略模式来拯救。由于持久化算法是变化的内容,我们将对其进行封装。由于您已经知道如何定义一组算法,因此我将跳过该部分,仅显示结果客户端:

class Registry
{
    public function persist()
    {
        $this->persistable->persist($this->data);
    }
    public function setPersistable(Persistable $persistable)
    {
        $this->persistable = $persistable
    }
    // other code …

很好,我们使用多态性重构了条件语句。现在你和其他开发者都可以将任何Persistable设置为所需的策略:
$registry->setPersistable(new PersistToCloudStorage);

这就是全部内容了。不再使用switch/case和注册表修改,只需创建一个新类并设置即可。策略模式使算法能够独立于使用它的客户端而变化。
另请参见:
- 策略模式是如何工作的? - https://softwareengineering.stackexchange.com/questions/187378/context-class-in-strategy-pattern - http://sourcemaking.com/design_patterns/strategy 以获得更多解释。 - https://www.youtube.com/watch?v=-NCgRD9-C6o 注:
[GoF] Gamma, E., Helm, R., Johnson, R., Vlissides, J., Design Patterns: Elements of Reusable Object­Oriented Software, Reading, Mass.: Addison­Wesley, 1995.

[CleanCode] 马丁,罗伯特C.《代码整洁之道:敏捷软件工艺宝典》。新泽西州,上沙德尔河: 普林斯顿大学出版社,2009年。印刷版。


9
基本上,策略模式是将功能分组到多个类中的一种方法。哦,你的代码中有一个错别字。
class context {

      public function printMsg(registry $class){
          $class->printMsg();
      }
}

使用类型提示接口的名称。 为了更清晰,让我给你举个例子。 想象一下你有一部iPhone和一部Android手机。 它们的共同点是什么? 一个共同点是它们都是手机。 你可以创建一个像这样的接口:

interface Telephone {
    public function enterNumber();
    public function call();
    public function sentTextMessage();
}

并在您的每个电话上实现接口:

class Iphone implements Telephone {
     // implement interface methods
}

class Android implement Telephone {

}

您可以将服务附加到所有手机上,就像汽车上的GPS一样:

只要插入一个电话(通常使用蓝牙接口),您就可以执行以下操作:

class carGps{
   public function handFreeCall(Telephone $tel){
       $this->amplifyVolume($tel->call());
   }
}


$tomtom = new CarGps();
$tomtom->handFreeCall(new Iphone());
//or if you like to:
$tomtom->handFreeCall(new Android());

作为一名应用程序开发者,您可以在实现电话接口的每个手机上使用HandFreeCall,而不会破坏您的代码,因为您知道该电话有打电话的能力。
希望我能帮到您。

谢谢,这个例子很好地解释了策略模式。您能指出我在第一个示例代码中没有使用此模式时可能遇到的潜在问题吗?我也可以使用抽象类和接口进行分组和代码组织。例如,该模式的某些变体将接口与抽象上下文类结合使用,这样做有什么好处呢? - Jay Bhatt
2
在使用策略之前,我们很难理解它。这实际上取决于您希望代码有多可扩展。 策略的好处在于可以通过共同点将不同的类分组。 当我们想要循环处理对象时,它的效果非常好。 想象一下,在同一页上有您的Facebook或LinkedIn消息等提要。 如果您希望以相同的方式处理每个消息(显示消息、评论),则可以像上面的示例替换一样做:foreach($feedItems as $i){ printMessage($i); } 而不是循环遍历所有Facebook,然后是Twitter等。 - Abdoul Sy
基本上,您将面临的问题是,如果您必须创建一个行为几乎与您的类完全相似但不完全相同的类,则没有策略,编写代码,理解或测试代码将更加困难。继承和策略的组合主要用于在这些类之间共享属性或方法,其中代码非常接近。 - Abdoul Sy
我明白你的意思,基本上使用策略来分组类将使我能够有效地访问它们。 - Jay Bhatt
不仅如此, 人类相关的例子是门, 门是一个接口, 无论是哪种类型的门,它都能让你通过并且可以被锁定和具有尺寸,如果你想在你的地方安装一扇门,你需要让它适合通行。你是门的制造商,当你编写一个接口时; 你希望所有其他制造商:当他们建造一扇门时,将创建一扇你能够在你的王国中使用的门。 当你作为制造商团队编写代码时,这是非常有效的,这样他们就不会通过编写烂掉的门来破坏你的代码,这些门在按下把手时会爆炸。 - Abdoul Sy

2
策略模式有助于抽象出特定工作的算法方法。假设您想通过不同的算法对数字数组进行排序,在这种情况下,最好应用策略模式,而不是将算法与代码紧密耦合。
在实例化由策略组成的类的类中,通过将策略类引用传递给组合类的构造函数来实例化策略类引用。
这样我们就是按接口编程而不是实现,因此在任何时候都可以向策略类层次结构添加更多的类,并将其传递给由策略组成的类。
请仔细阅读以下 link

您的讲话比较笼统,我们暂时不用担心向任何客户交付。如果您能给出与示例代码相关的示例,我会很感激。即在示例代码中不使用策略模式可能存在的问题(现在和将来)以及改进措施。使用该方法而不使用模式的一个明显优点是易于实现和理解,任何新人都应该能够快速掌握它,而不是理解各种类和接口。 - Jay Bhatt
此外,为了提供像抽象化代码组织这样的好处,我可以使用继承、接口和抽象类。我试图表达的观点是,设计模式是为了解决问题而创建的,因此我真正关注的是策略模式解决的问题。 - Jay Bhatt
我肯定不会发布项目代码。但是我不知道你是否遇到过客户提出的更改请求,需要修改大量源代码。由于设计不良,我们曾经遇到过这种情况。如果应用了适当的模式,您只需在最少的地方进行更改。无论如何,我将向您提供 SWT GUI 的链接,其中使用了自定义小部件和许多模式,并且在每次更改请求时,我们不必做太多修改,我可以向您保证。 - Sanyam Goel
我不需要代码,我可以自己编写。而你提到的东西正是我想要的。你说你不得不改变很多文件,但使用这种模式可以减少需要改变的文件数量。但是怎么做呢?在上面的例子中,如果我想要改变某些东西,无论采用哪种方法,我都必须改变同样数量的文件。 - Jay Bhatt
我明白你想表达的观点。但是如果不麻烦的话,能否请你举一个代码的例子呢?可以使用任何字母,如A、B等,不必是真正的代码或在线内容。 - Jay Bhatt
显示剩余5条评论

0

0
Gang of Four 设计模式中,策略模式的几个重要特点如下:
  1. 它没有条件语句(所以请不要使用 switch、if 等条件语句)
  2. 它有一个 Context 参与者

大多数关于策略模式的建议都忽略了 Context 参与者。你可以在这里找到五个不同的 PHP 实现策略模式的例子: http://www.php5dp.com/category/design-patterns/strategy/


0

通常,示例有些奇怪,描述鸭子、猫或其他东西。

以下是策略模式在显示警报时的示例(扩展了戈登的回答):

1. 方法接口,即变化的方法(例如,在此情况下为警报格式):

require_once("initialize/initialize.php");
interface alert{
public function alert($message);
};

2. 实现警报接口的方法。

class alertBorderBullet implements alert{
public function alert($message){
$alert = "<p style='border:1px solid #eee; padding:4px; padding-left:8px; padding-right:8px; border-left:4px solid #FC0; margin-top:8px; margin-bottom:8px; color:#888'>".$message."</p>";
return $alert;
}
};

class alertOrangeBgnd implements alert{
public function alert($message){
$alert = "<p style='color:#fff; background-color:#ff9c3a; padding:4px; padding-left:8px; padding-right:8px; margin-top:8px; margin-bottom:8px; border-left:4px solid #e471bd;'>".$message."</p>";
return $alert;
}
};

class alertRed implements alert{
public function alert($message){
$alert = "<p style='color:#c11; background-color:#efefef; padding:4px; padding-left:12px; padding-right:8px; margin-top:8px; margin-bottom:8px;'>".$message."</p>";
return $alert;
}
};

3. Messenger,将警报方法设置和获取与项目中的其他对象分开。

class alertMessenger{
protected $_alert;
public function setAlertType(alert $alert){$this->_alert = $alert;}
public function returnAlert($message){return $this->_alert->alert($message);}
};

4. 一个随机的项目对象,将以不同的方式使用“警报”。

class randomObject{
public $alert;
public function __construct(){
    $this->alert = new alertMessenger;
}
// More code here...
};

$randomObject = new randomObject;
$randomObject->alert->setAlertType(new alertRed);
echo $randomObject->alert->returnAlert($message="Red text for critical info");
$randomObject->alert->setAlertType(new alertBorderBullet);
echo $randomObject->alert->returnAlert($message="Border, bullet and pale-color text");
echo $randomObject->alert->returnAlert($message="Repeat, check style permanence");
$randomObject->alert->setAlertType(new alertOrangeBgnd);
echo $randomObject->alert->returnAlert($message="Again, another redefined message style");

randomObject在初始化时(对于此情况)会自动创建一个alertMessanger实例并使其方法可用。可以设置行为并回显消息。可以通过setAlertType和then returnAlert创建其他警报格式并在必要时使用。


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