【问题标题】:Is this code too brittle?这段代码太脆弱了吗?
【发布时间】:2010-10-19 01:54:33
【问题描述】:

我需要创建一个策略模式,其中用户从二十或三十个独特策略对象的列表中选择四个策略。策略列表将随着项目的成熟而扩展,用户可以随时更改他们选择的策略。

我打算将他们选择的策略名称存储为字符串,然后使用类似这样的方法来加载与他们选择的字符串对应的策略类。

class StrategyManager { // simplified for the example
    public $selectedStrategies = array();
    public function __construct($userStrategies) {
        $this->selectedStrategies = array(
            'first'  => new $userStrategies['first'],
            'second' => new $userStrategies['second'],
            'third'  => new $userStrategies['third'],
            'fourth' => new $userStrategies['fourth']
        );
    }

    public function do_first() {
        $this->selectedStrategies['first']->execute();
    }

    public function do_second() {
        $this->selectedStrategies['second']->execute();
    }

    public function do_third() {
        $this->selectedStrategies['third']->execute();
    }

    public function do_fourth() {
        $this->selectedStrategies['fourth']->execute();
    }
}

我试图避免使用大的 switch 语句。我担心这似乎有点Stringly Typed。有没有更好的方法可以在不使用条件语句或大型 switch 语句的情况下实现这一目标?

BTW:用户在选择四种策略时没有输入字符串。我需要维护一个字符串列表以在选择框中呈现给用户,并在添加新策略对象时将新字符串添加到列表中。

解释
ircmaxell 对我正在尝试做什么表示了一些困惑。在上面的例子中,用户从一个列表中选择了四个策略,并将它们作为字符串数组传递给 StrategyManager 构造函数。相应的策略对象被创建并存储在一个内部数组中,$this->selectedStrategies

“first”、“second”、“third”和“fourth”是四种不同选择策略的内部数组的数组键。建立 StrategyManager 对象后,应用程序在流程生命周期的不同时刻使用四种策略的execute 方法。

因此,简而言之...每次应用程序需要执行策略编号“一”的方法时,它都会执行此操作,并且结果会根据用户为策略“一”选择的策略而有所不同

【问题讨论】:

  • 我很困惑。 “第一”、“第二”、“第三”和“第四”是不同的可能策略,还是它们是所选策略的一系列命令(在构造管理器之前选择)。如果是这样,Chain of ResponsibilityCommand 模式会更好吗?你能解释一下你到底想做什么(以及代码做什么,为什么存在不同的策略)?
  • 总是有4种策略吗?它们总是按顺序执行吗?或者它们是您试图一起管理的四种其他不相关的策略?
  • 用户将始终选择四种策略(有一个为他们预先选择的默认策略,他们可以更改它)。它们并不总是按顺序执行,但它们确实需要通过数组键来识别(并非所有策略都同时用于应用程序的每个方面)。我选择的数组键是任意的。

标签: php string strategy-pattern


【解决方案1】:

嗯,嗯,我不认为它太脆。你不需要字符串。您可以简单地使用有序数组,因为无论如何命名都对应于 0,1,2,3。如果您担心提供的策略或类无效,您可以在管理器中进行一些验证。

public function __construct() {
    $this->selectedStrategies = array(
        /* could add some default strategies */
    );
}
public function load(array $userStrategies) {
    for( $i=0; $i<3; $i++ ) {
        try {
            $rc = new ReflectionClass($userStrategies[$i]);
            if( $rc->implementsInterface('Iterator') ) {
                $this->selectedStrategies[$i] = new $userStrategies[$i];
            } else {
                throw new InvalidArgumentException('Not a Strategy');
            }
        } catch(ReflectionException $e) {
            throw new InvalidArgumentException('Not a Class');
        }
    }
}

而不是使用关联键调用策略,您只需

$this->selectedStrategies[0]->execute();

等等。


另一种方法是使用

class StrategyCollection
{
    protected $strategies;

    public function __construct() {
        $this->strategies = new SplFixedArray(4);
    }

    public function add(IStrategy $strategy) {
        $this->strategies[] = $strategy;
        return $this;
    }
}

然后从外部填充 Manager/Collection。使用IStrategy 的类型提示,您可以确定只有实现策略接口的类最终会出现在管理器中。这可以在创建策略时为您节省一些昂贵的反射调用。 SplFixedArray 确保当您尝试添加超过四个策略时出现运行时异常。


在旁注中,不要相信来自选择框的输入。仅仅因为选择框提供了固定选项,并不意味着恶意用户无法修改请求。所有请求数据都必须经过清理和仔细检查。

【讨论】:

  • +1 用于旁注和 StrategyCollection 类,它简单且构建良好。
【解决方案2】:

根据您的 cmets 和更新,我不认为这段代码太脆弱。如果您更改策略类型(do_one、do_two 等)的调用链或添加策略,将更难维护。我建议使用abstract factory 来创建“策略”。然后,在你需要策略的代码中,获取策略对象本身...

我更喜欢这种方法的原因有两个。首先,它只根据需要创建策略,因此您不会构建不需要的对象。其次,它封装了用户的选择,因为这是唯一需要查找它的地方(您可以使用依赖注入来构建它,但您也需要其他地方来管理构建)。

class StrategyFactory {

    protected $strategies = array();

    //If you like getter syntax
    public function __call($method, $arguments) {
        $method = strtolower($method);
        if (substr($method, 0, 3) == 'get') {
            $strategy = substr($method, 3);
            return $this->getStrategy($strategy);
        }
        throw new BadMethodCallException('Unknown Method Called');
    }

    public function getStrategy($strategy) {
        if (isset($this->strategies[$strategy])) {
            return $this->strategies[$strategy];
        } elseif ($this->makeStrategy($strategy)) {
            return $this->strategies[$strategy];
        }
        throw new LogicException('Could not create requested strategy');
    }

    protected function makeStrategy($name) {
        //pick strategy from user input
        if ($strategyFound) {
            $this->strategies[$name] = new $strategy();
            return true;
        } else {
            return false;
        }
    }
}

然后,像这样使用:

$strategy = $factory->getSomeStrategyName();
$strategy->execute();

或什至有变化:

$factory->getSomeStrategyName()->execute();

或者没有魔法方法:

$factory->getStrategy('strategyName')->execute();

【讨论】:

  • 我喜欢你在这里提供的工厂实现。谢谢!
【解决方案3】:

如果策略函数不需要状态,您可以切换到函数式编程风格并将整个类替换为:call_user_func($strategy['first']);(第二个等)。如果关心全局命名空间,它们的函数可以存储为类的静态成员 - 即call_user_func(array('Strategies', $strategy['first'])); 然后您可以使用get_class_methods('Strategies'); 获取所有有效策略的列表(用于生成和测试选择框),这可以简化通过只有一个有效策略的全局列表来编写代码。

如果您确实需要使用策略函数存储状态 - 我可能会使用某种缓存调用函数 - 类似于

function doStrategy($class) {
    static $selectedStrategies = array();

    if (!isset($selectedStrategies[$class])) {
        $selectedStrategies[$class] = new $class;
    }

    $Strategy = $selectedStrategies[$class];
    $Strategy->execute(); // some versions of PHP require two lines here
}

当然,您仍然可以在函数上使用类来执行此操作:P。

“Stringly Typed”这一称号不适用于 PHP,因为它既是弱类型的,又已经在内部使用字符串来存储符号(类和函数名、变量等)。因此,对于反射,字符串数据类型通常是最合适的。我们不会深入探讨这对整个语言意味着什么。

【讨论】:

    猜你喜欢
    • 2017-03-11
    • 1970-01-01
    • 1970-01-01
    • 2017-07-12
    • 2018-07-02
    • 2022-10-22
    • 2017-06-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多