之前一段时间,easytrader
上用户报了很多 issues,基本都是关于客户端软件无法获取持仓的错误。所以最近就抽了段时间解决下,里面用到了策略模式,就想顺便复习下设计模式。
这里介绍下相关的上下文,easytrader
是一个模拟证券客户端操作的Python
类库,基本就是一个定制版的按键精灵,支持银河、华泰等公司以及通用版客户端(支持多家券商),能进行的操作有买卖、获取持仓等。
issue
里面的错误是关于获取持仓 get_position
这个接口的,画成类图的话如下所示(省略了一些上下文无关的信息):
这边有问题的是获取持仓这一步。原来客户端的持仓保存在Grid
中,类似网页端的 table
,然后 easytrader
通过Ctrl + C
的方式复制内容到剪切板,再解析剪切板的内容获取持仓:
示例代码:
class ClientTrader:
def get_position(self):
# ...
grid_data = self._get_grid_data()
# ...
return position
这个逻辑是所有客户端通用的,因此 YHTrader
和 HTTrader
都是继承的父类 ClientTrader
中的默认方法。
Break
但是前段时间银河和通用客户端的一部分券商通过更新封杀了通过剪切板获取 Grid
数据的方式,导致原有的 get_position
失效。这时在 issue
里有开发者提出可以通过在 Grid
上右键将对应内容保存为 xls
文件再解析保存的文件获取持仓,并给出了示例代码。
Question
这时候的问题就是怎么将通过 xls
获取持仓的策略和原有的通过剪切板获取持仓的策略整合到代码里,修复银河客户端和一部分通用客户端无法获取持仓的 Bug
。
解决方式
在 ClientTrader
类中实现 get_position_by_copy
和 get_position_by_xls
的方法,并在 get_position
进行 hardcode
,然后 YHTrader
通过继承的方式默认使用 xls
方式,示例代码如下:
class ClientTrader:
def get_position(self, strategy='copy'):
# ...
if strategy == 'copy':
grid_data = self._get_grid_data_by_copy()
elif strategy == 'xls':
grid_data = self._get_grid_data_by_xls()
else:
raise NotImplementedError(f'Stratege {strategy} not implenmented')
# ...
return position
def _get_grid_data_by_copy(self):
# ...
def _get_grid_data_by_xls(self):
# ...
class YHTrader(ClientTrader):
# 修改银河的方法默认为 xls 策略
def get_position(self, strategy='xls')
# ...
优点
api
的改动较小缺点
如果获取 position
的方式继续增多,get_position
中会有很多面条式的代码,不好维护。
修改持仓策略的话需要修改ClientTrader
类,而这个类作为父类,修改它影响范围较广,违背了对扩展开放,对修改关闭
的开闭原则。
污染ClientTrader
和 get_position
的代码结构,导致阅读代码时有很多无关的代码细节干扰思路
如果我们基于继承实现的话会怎么样,因为通用客户端从原有的仅支持一种策略变为支持两种,此时就需要基于 copy
策略的父类实现使用 Xls
策略的子类,而银河客户端从 Xls
策略子类继承,这里同时需要修改获取 Trader
的工厂方法,提供获取Xls
类的方法。示例代码如下:
class ClientTrader:
def get_position(self):
# ...
grid_data = self._get_grid_data()
# ...
return position
def _get_grid_data(self):
# 默认实现基于 copy 的 grid_data 获取逻辑
class ClientTraderWithXls(ClientTrader):
def _get_grid_data(self):
# 实现基于 xls 的 grid_data 获取逻辑
# 银河改为从 ClientTraderWithXls 继承
class YHTrader(ClientTraderWithXls):
pass
class HTTrader(ClientTrader):
pass
### api.py
def get_trader(broker):
if broker == 'client_trader':
return ClienTrader()
if broker == 'client_trader_with_xls': # 新增获取 xls 策略子类的方法
return ClientTraderWithXls()
# ...
优点
xls
的策略逻辑抽取到了子类中,比较独立client_trader
类缺点
那有没有更好的方法呢?自然是有的,那就是设计模式中的策略模式。
根据《Design Pattern》,策略模式的目的是:
定义一组封装内部细节并可互换的算法,使得客户端跟它们使用的算法解耦。
而为什么不选择将对应的算法直接嵌入类中或通过继承实现呢?除了我们上面所讲的几点外,书中还列出了直接 hardcode
的带来的一些其他问题:
将算法细节包含在类内实现,会使得类本身复杂度上升和代码膨胀,导致维护困难,尤其需要支持多个算法的时候。
HardCode
的实现会导致类基本丧失了可维护性。可能需要在不同的时间使用不同的算法,而且有时不需要同时支持多种算法。
HardCode
本身允许通过参数调用不同的方法,但是也同时将所有算法的代码包含在内。
类继承
的方式虽然只包含对应算法的代码,但是却不允许用户切换算法实现。
当类本身包含了大量算法实现的时候,修改或者变更已存在的算法都会非常困难。
而为了避免这些问题,我们可以定义一些将算法细节封装起来的类,它们具有统一的接口。而基于这样的选择封装的类,我们称它为一个Strategy
。
这时就让我们按照策略模式的方式修改我们的代码如下 :
### grid_stragies.py
import abc
class IGridStrategy(abc.ABC):
"""获取 grid 策略接口定义"""
@abc.abstractmethod
def get(self):
"""返回 grid 数据"""
pass
class Copy(IGridStrategy):
def get(self):
"""实现基于 Copy 的逻辑"""
pass
class Xls(IGridStrategy):
def get(self):
"""实现基于 Xls 的逻辑"""
pass
### clienttraders.py
import grid_stragies
class ClientTrader:
# The strategy to use for getting grid data
grid_strategy = grid_stragies.Copy
def get_position(self):
# ...
grid_data = self.grid_strategy(self).get()
# ...
return position
# 银河改为使用 Xls strategy
class YHTrader(ClientTrader):
grid_strategy = grid_stragies.Xls
class HTTrader(ClientTrader):
pass
# 如果希望 ClientTrader 使用 Xls 方法,只需要
import grid_strategies
client_trader = ClientTrader()
client_trader.grid_strategy = grid_strategies.Xls
优点
client_trader
相关类grid_strategy
属性来动态切换策略,避免多种方法需要多种策略时组合爆炸的问题。缺点
允许动态切换类导致的性能轻微下降。如果不需要动态切换的话可以通过 mixin
的形式实现。
适用场景
策略模式适合于以下场景:
HardCode
的实现)。此时可以通过将相关条件分支移动到对应的策略类中消除条件语句。实现细节
strategy 如何访问 context
class Context:
strategy = ConcreteStrategy1
def do_something(self):
self.strategy.do_something(self)
class Context:
strategy = ConcreteStrategy1
def do_something(self):
self.strategy.do_something(self.arg1, self.arg2)
class Context:
def do_something(self, strategy):
strategy.do_something(self)
# usage
Context().do_something(ConcreteStrategy1)
第一种方法更加灵活,但是暴露了 context 本身过多的信息给 strategy,使得策略和 context 紧耦合。
第二种方法如果需要新的参数,则需要变更 strategy 类的接口,这时候需要 strategy 类本身设计的较为合理。
第三种方法提供了 client 在运行时变更 strategy 的能力。
缺点
虽然策略模式有很多优点,但是不可避免的也会有一些缺陷,正如”没有银弹“所说的,掌握设计模式的关键便是理解它们对应的 tradeoff,如此才可以在正确的时间选择正确的模式。
如果 client 需要切换 strategy 的话,它必须理解对应的 strategy 的区别。
context 和 strategy 之间的交互带来的额外成本。如果 strategy 设计不合理的话,context 传递的一些参数可能在某些 strategy 中永远不会被用到。
类以及对象数量的膨胀。这在设计模式中是很难避免的,毕竟设计模式的第二原则就是 Favor object composition over class inheritance.
实际案例
django-rest-framework
rest_framework.views.APIView
就大量使用了策略模式以使得用户在编写对应的 APIView
时可以获得最大的灵活性
class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS