如何实现运行时选择方法的策略模式?
背景
我正在尝试在Python 2.7中实现某种变体的策略模式。我希望能够实例化一个名为'my_strategy'的基类,但在运行时可以切换不同的'score'方法实现。我会在'my_strategy'中有很多公共方法,但会有很多不同的'score'实现。主要部分展示了我想要如何使用它。在这里,评分的实现当然是个简单的示例。
我尝试过的(也就是我到目前为止的代码)
strategy.py:
from algo_one import *
#from algo_two import *
class my_strategy ( object ):
def __init__(self, candidate = ""):
self.candidate = candidate
self.method = 'default'
self.no = 10
self._algo = algo_one
def set_strategy(self, strategy='default'):
self.strategy = strategy
if self.strategy == 'algo_one':
self._algo = algo_one
elif self.strategy == 'algo_two':
# self._algo = algo_two
pass
else:
self._algo = None
def score(self, *args):
if len(args) > 0:
self.candidate = args[0]
self._algo.score(self.candidate)
if __name__ == "__main__":
s = my_strategy()
s.strategy = 'algo_one'
s.candidate = "hello world"
print s.score()
print s.score("hi")
# s.set_method('algo_two')
# print s.score("hi")
我想把选定的策略保存在某种私有指针中,以指向子类的方法。
algo_one.py:
from strategy import my_strategy
class algo_one ( my_strategy ):
def score(self, candidate):
return len(candidate)*self.no
我可以有一个没有类的方法,但之后我需要访问基类的公共变量。
algo_two.py:
from strategy import my_strategy
class algo_two ( my_strategy ):
def score(self, candidate):
return len(candidate)*3
我还有一个空的init.py。
错误信息
1. 在score中 self._algo.score(self.candidate)
类型错误:未绑定的方法score()必须用algo_one实例作为第一个参数(而得到了str实例)
2. 如果我取消注释第二个策略的导入:
from algo_two import *
我会得到以下错误。
导入错误:无法导入名称my_strategy
我猜这是因为我遇到了某种循环依赖。
3.
from algo_one import *
这显然不太好(无法检测未定义的名称),但如果我
from algo_one import algo_one
我会得到
导入错误:无法导入名称algo_one
问题
我觉得这些错误是相互关联的,而且我的整体方法可能有问题。如果不只是解决错误,我希望能得到改进设计的建议。或者任何评论也可以。我也欢迎关于这个问题标题的建议。谢谢!
3 个回答
你没有在
__init__
方法里创建你的算法对象。记住,要创建一个类的对象,你需要调用它:self._algo = algo_one()
是的,这就是循环依赖。不过,我不明白为什么 algo_one 和 algo_two 需要从 my_strategy 继承。你可以把它们做成普通对象,或者从其他地方的一个基类继承。或者,把它们都放在同一个文件里——在 Python 中,没必要把类放在不同的文件里。
这是和第二个问题一样的情况。
你现在面临的一个主要问题是,你的算法试图从基础类进行子类化,这其实是个很大的设计缺陷(你已经意识到了这一点)。不如使用简单的方法绑定,这样可以处理所有必要的事情:
def algo_one(candidate):
# do stuff
return "A fluffy unicorn"
def algo_two(candidate):
# do some other stuff
return "Awesome rabbits"
# not really necessary, just to make it easier to add new algorithms
STRATEGIES = { "one": algo_one, "two": algo_two }
class Strategy(object):
def __init__(self):
...
def set_strategy(self, which):
if which not in STRATEGIES:
raise ValueError("'%s' is an unknown strategy" % which)
# compatibility checks about the entries in STRATEGIES omitted here
self._algo = STRATEGIES[which]
def score(self, *args):
# ...
return self._algo(...)
如果你需要一种更复杂的方法(这取决于你的具体需求),让所有的算法彼此了解,可以把算法和选择策略的部分分成不同的类,相互引用(下面是简化版):
class ScoreAlgo(object):
def __init__(self, parent):
self._strategy = parent # if you need a back-reference, just be aware of circular dependencies in the garbage collection
def __del__(self):
self._strategy = None # resolve circular dependency for the GC
def score(self, candidate):
return None
class Strategy(object):
def __init__(self):
...
def set_strategy(self, ...):
...
self._algo = ScoreAlgo(self)
def score(self, ...):
return self._algo.score(...)
如果你需要很多不同的算法,建议把ScoreAlgo做成一个抽象基类,这样子类就必须实现score()这个方法。
你还可以使用混入模式(这比方法绑定稍微正式一些)或者其他几种方式。这些都取决于你的整体需求。
编辑:我刚刚在两个def score():
的代码块中添加了return
,这样可以避免大家对为什么这些方法可能不返回任何东西产生困惑。
你把事情想得太复杂了。Python中的函数是第一类对象,所以在Python中实现策略模式的最简单方法就是把一个“策略”函数传递给你的“上下文”对象(也就是使用这个策略的对象)。有趣的是,任何可调用的对象(也就是说,任何实现了__call__
方法的对象)都可以使用。
def default_score_strategy(scorer):
return len(scorer.candidate) * 3
def universal_answer_score_strategy(scorer):
return 42 # definitly the universal answer <g>
class ComplicatedStrategy(object):
def __init__(self, factor):
self.factor = factor
def __call__(self, scorer):
return len(scorer.candidate) * self.factor
class Scorer(object):
def __init__(self, candidate="", strategy=default_score_strategy):
self.candidate = candidate
self.strategy = strategy
def score(self):
return self.strategy(self)
s1 = Scorer("foo")
s2 = Scorer("bar", strategy=universal_answer_score_strategy)
s3 = Scorer("baaz", strategy=ComplicatedStrategy(365))
注意,你的策略不一定要和Scorer
类在同一个模块里(当然,默认的策略除外),而且包含Scorer
类的模块也不需要导入策略模块——也不需要知道这些策略是在哪里定义的:
# main.py
from mylib.scores import Scorer
from myapp.strategies import my_custom_strategy
s = Scorer("yadda", my_custom_strategy)