Strategy 策略模式

设计模式 2020-04-09 5086 字 1189 浏览 点赞

起步

策略模式属于行为型,常用来避免冗长的分支判断。

概念与使用都极为简单,这里就用维基百科中的一句话诠释:the strategy pattern enables selecting an algorithm at runtime

初探策略模式

现在假设项目需要解析三种类型的配置文件,分别是 json 文件、xml 文件、yaml 文件。于是我们最快能想到的代码实现如下:

class ConfigSource(object):
    def load(self, file):
        _, ext = os.path.splitext(file)
        if ext == ".json":
            res = json_parse(file)
        elif ext == ".xml":
            res = xml_parse(file)
        elif ext == ".yaml":
            res = yaml_parse(file)
        else:
            raise NotImplementedError
        return res

假设哪天我们需要解析 toml 格式的配置文件,就不得不去修改 load 方法:

def load(self, file):
    _, ext = os.path.splitext(file)
    ...
    elif ext == ".yaml":
        res = yaml_parse(file)
    elif ext == ".toml":
        res = toml_parse(file)
    ...

依次类推,不但是违背了封闭原则,还使得 if ... else 分支冗长。而策略模式认为,不要直接去实现一种算法(这里可以认为是功能),而要实现一组算法,在运行时选择其中一种

同时从设计原则来看,高层模块不要依赖底层模块,高层与底层共同依赖抽象。所以作为客户端的 load 方法,不应该直接去选择解析方法(json_parse, xml_parse...),而是要借助抽象模块与解析方法建立一个沟通的桥梁。先优化这组解析方法:

import abc

class ParseStrategy(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def parse(self, file):
        pass

class JsonStrategy(ParseStrategy):
    def parse(self, file):
        return ...

class XmlStrategy(ParseStrategy):
    def parse(self, file):
        return ...

class YamlStrategy(ParseStrategy):
    def parse(self, file):
        return ...

客户端不应该、也没必要知道子系统的代码逻辑,那么如何选择策略这个难题我们可以拿之前说过的简单工厂来解决。

class StrategyFactory(object):
    @classmethod
    def create_strategy(cls, ext):
        if ext == ".json":
            strategy = JsonStrategy()
        elif ext == ".xml":
            strategy = XmlStrategy()
        elif ext == ".yaml":
            strategy = XmlStrategy()
        else:
            raise NotImplementedError
        return strategy

class ConfigSource(object):
    def load(self, file):
        _, ext = os.path.splitext(file)
        # 获取策略
        strategy = StrategyFactory.create_strategy(ext)
        # 解析配置
        return strategy.parse(file)

你应该注意 StrategyFactory 和 ConfigSource 的不同。事实上,设计原则与设计模式并不为了消灭复杂,只能够转移复杂。if ... else 分支虽然还存在,但不属于客户端(接口使用者)的知识负担了。

再探策略模式

if ... else 分支虽然转嫁到接口开发者身上,但毕竟没有被除掉,不是说策略模式可以避免 if ... else 冗余吗?现在这样说不过去吧!在 Python 中,我们是可以借助 dict 或者 list 来根除判断分支的冗余的。

具体修改在工厂类中。只需要提前创建好策略,放到一个字典中即可:

class StrategyFactory(object):

    strategys = {
        ".json": JsonStrategy(),
        ".xml": XmlStrategy(),
        ".yaml": YamlStrategy(),
    }

    @classmethod
    def create_strategy(cls, ext):
        strategy = cls.strategys.get(ext)
        if strategy is None:
            raise NotImplementedError
        return strategy

基于列表的实现原理差不多,但需要遍历出每个元素,再判断是否是目标算法,如果是就取出来用,如果不是就继续遍历。当元素个数较少,或者内存资源紧张时,采用 list + 遍历 代替字典也 ok 的。

总结

不知道你有没有这种感觉,基于 Python 学习策略模式会很迷惑,觉得这模式卵用,感觉不到意义。我甚至不能在《Learning Python Design Patterns Second Edition》一书中找到策略模式。我以为这种的困惑是 Python 没有强类型约束导致的,比如 JsonStrategy 的实例 json_strategy 可以被赋值为 XmlStrategy 的实例,你感觉不到这里边的差别。但在 C++ 中区别就大了。

没有使用策略模式之前,你需要单独创建解析器对象,单独去调用 parse 方法。

class Config {};

class JsonParser {
public:
    Config parse(string file) {
        cout << "parse config file.json" << endl;
        return Config();
    }
};

class XmlParser {
public:
    Config parse(string file) {
        cout << "parse config file.xml" << endl;
        return Config();
    }
};

int main() {
    string ext = ".xml";
    
    if (ext == ".json") {
        JsonParser *jsonParser = new JsonParser();
        jsonParser->parse("xxx.json");
    } else if (ext == ".xml") {
        XmlParser *xmlParser = new XmlParser();
        xmlParser->parse("xxx.xml");
    }

    // 错误操作, 不同类型之间的指针是不允许直接赋值的
    // jsonParser = xmlParser
    return 0;
}

策略模式之后:

class Config {};

class ParseStrategy {  // 接口
public:
    virtual Config parse(string file) = 0;
};

class JsonStrategy: public ParseStrategy {
public:
    virtual Config parse(string file) {
        cout << "parse config file.json" << endl;
        return Config();
    }
};

class XmlStrategy: public ParseStrategy {
public:
    virtual Config parse(string file) {
        cout << "parse config file.xml" << endl;
        return Config();
    }
};

int main() {
    string ext = ".xml";
    // 注意使用上的差异
    ParseStrategy *strategy = NULL;
    if (ext == ".json") {
        strategy = new JsonStrategy();
    } else if (ext == ".xml") {
        strategy = new XmlStrategy();
    }

    strategy->parse("xxx.json");
    return 0;
}

代码果断简洁,实际场景越复杂效果就会越明显。

参考



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论