仅需函数式行为时,使用面向对象风格接口与类是否恰当?
当我们只需要函数式行为(无需跟踪或修改状态,实例仅用于调用唯一暴露的方法一次)时,用面向对象风格的接口和类来实现是否合理?
我经常写出这样的Python代码:
from abc import ABC, abstractmethod class IFoo(ABC): @abstractmethod def bar(self) -> str: ... class ThisFoo(IFoo): def __init__(self, x: str): self._x = x def bar(self) -> str: return f'Doing this with {self._x}' class ThatFoo(IFoo): def __init__(self, x: str): self._x = x def bar(self) -> str: return f'Doing that with {self._x}'
这些Foo类的唯一用途就是让外部调用其bar方法,而且客户端代码只会调用一次。我通常会写一个便捷函数来封装这个逻辑:
def bar(type_: str, x: str) -> str: dispatcher = { 'this': ThisFoo, 'that': ThatFoo } Foo = dispatcher[type_] foo = Foo(x) return foo.bar()
客户端代码可以这样调用:
print(bar('this', 'baz')) print(bar('that', 'baz'))
当然,类实现不是必须的,下面是等效的纯函数式实现:
def func_bar(type_: str, x: str) -> str: dispatcher = { 'this': this_bar, 'that': that_bar } func = dispatcher[type_] return func(x) def this_bar(x: str) -> str: return f'Doing this with {x}' def that_bar(x: str) -> str: return f'Doing that with {x}' print(func_bar('this', 'baz')) print(func_bar('that', 'baz'))
有人觉得这里用类不合适,认为类应该只在需要持久化对象、跟踪可变状态时使用,但这个场景并不满足。实际上我的Foo实现里完全可以删掉__init__,直接在bar方法里把x: str作为参数,不用让bar引用实例属性self._x。
不过我认为基于类的实现有几个明显优势:
契约强制执行
如果后续要添加OtherFoo,只要继承IFoo接口,mypy这类静态类型检查工具就会确保我实现符合规范的bar方法。相比之下,函数式实现里如果不小心把函数命名成other_abr,还把这个拼写错误的函数加入调度器,静态分析工具没法发现这个问题。
当然,只要把other_bar加入func_bar的调度器,类型检查工具能检测到签名错误,但如果没把这个函数加进去,静态分析就发现不了签名不匹配的问题。
辅助方法/函数的组织
假设bar需要特定于实现的辅助方法,类实现里可以把辅助方法作为私有方法放在对应类中:
from abc import ABC, abstractmethod class IFoo(ABC): @abstractmethod def bar(self) -> str: ... class ThisFoo(IFoo): def __init__(self, x: str): self._x = x def bar(self) -> str: great_text = self._helper() return f'Doing this with {self._x}, {great_text}' def _helper(self) -> str: return f'{self._x} is great' class ThatFoo(IFoo): def __init__(self, x: str): self._x = x def bar(self) -> str: great_text = self._helper() return f'Doing that with {self._x}, {great_text}' def _helper(self) -> str: return f'{self._x} is the greatest'
辅助方法各自归属对应的具体类,能清晰区分哪个辅助方法对应哪种实现。而函数式实现里最自然的方式是把辅助函数定义成独立的私有函数:
def this_bar(x: str) -> str: great_text = _this_bar_helper(x) return f'Doing this with {x}' def _this_bar_helper(x: str) -> str: return f'{x} is great' def that_bar(x: str) -> str: great_text = _that_bar_helper(x) return f'Doing that with {x}' def _that_bar_helper(x: str) -> str: return f'{x} is the greatest'
但这样很难明确哪个辅助函数属于哪个公共函数(命名规范有帮助,但不如类方法可靠),尤其是存在多个辅助方法,其中一部分是特定实现的,一部分可能被多个实现共享时。
当然也可以把特定实现的辅助函数定义成对应公共函数的嵌套函数,但这会在公共函数里混合不同抽象层级,而且辅助函数的定义会打断代码,降低可读性。
避免嵌套参数传递的便利性
上面的代码也能体现这一点:类实现里,公共方法和私有方法都能访问self._x;而函数式实现里,辅助函数需要显式传递参数,这意味着函数签名会有更多样板代码,函数体也更长,因为要把参数传给辅助函数。当参数带默认值时,情况会更尴尬——需要在多个嵌套辅助函数的签名里重复输入相同的默认值。
有人可能觉得这是好事,因为能强制辅助函数明确所需参数,但如果要修改公共函数的签名,就得更新函数体(把新参数传给辅助函数)以及所有嵌套辅助函数的签名和实现,这非常繁琐。而类实现里,只要修改__init__方法的签名就可以了。
内容的提问来源于stack exchange,提问作者Yakkanomica

