
本文介绍如何通过策略模式与组合(composition)重构过度依赖抽象类继承的绘图系统,避免爆炸式增长的抽象基类,提升代码可维护性、复用性与可测试性。
本文介绍如何通过策略模式与组合(composition)重构过度依赖抽象类继承的绘图系统,避免爆炸式增长的抽象基类,提升代码可维护性、复用性与可测试性。
在面向对象设计中,当面对“为每种数据维度+图表类型组合都定义一个抽象类”(如 AbstractMonthlyContourPlot、AbstractSeasonalLinearPlot)时,往往已落入继承滥用的陷阱。你直觉上感到“越来越深的继承链让代码更复杂”,这非常准确——它违背了开闭原则(难以新增图表类型而不修改基类),也违反了单一职责原则(一个类被迫承载“时间结构逻辑”和“渲染逻辑”双重责任)。
上述示例中,原始设计将绘图行为(contourf vs plot)与布局结构(按月排列子图)耦合在同一个继承链中,导致类爆炸风险。而重构后的方案采用组合优于继承(Composition over Inheritance)这一核心 Pythonic 原则,将关注点清晰分离:
-
Plotter策略接口:封装如何绘制单个子图,每个具体实现(ContourPlotter、LinearPlotter)只专注一种可视化逻辑; -
MonthlyPlot容器类:封装如何组织子图布局(如 12 个月、共享坐标轴等),不关心具体绘图细节,仅依赖Plotter抽象协议; -
运行时组合:通过构造函数注入具体
Plotter实例,实现行为的动态装配。
这种设计带来显著优势:
调用 Cutout.Pro 视觉处理 API 进行背景移除、人像抠图和照片增强,支持文件上传与图片 URL 输入。
✅ 高内聚低耦合:ContourPlotter 不再需要知道“月份”概念;MonthlyPlot 也不需了解 contourf 的 API;
✅ 无限可扩展:新增 HeatmapPlotter 或 BarPlotter 只需实现 Plotter 接口,无需修改任何现有类;
✅ 易于测试:可单独单元测试 ContourPlotter.plot(),也可用 mock Plotter 测试 MonthlyPlot.plot() 的布局逻辑;
✅ 符合 Python 哲学:“显式优于隐式”——行为由传入对象决定,而非继承关系推导。
以下是关键代码片段(含类型提示与健壮性增强):
立即学习“Python免费学习笔记(深入)”;
from abc import ABC, abstractmethod
from numpy import ndarray
from typing import List, Tuple, Protocol
import matplotlib.pyplot as plt
# 1. 定义绘图策略协议(Protocol 更灵活,非强制继承)
class Plotter(Protocol):
def plot(self, ax: plt.Axes, data: Tuple[ndarray, ...]) -> None: ...
# 2. 具体策略实现(无继承,纯函数式职责)
class ContourPlotter:
def plot(self, ax: plt.Axes, data: Tuple[ndarray, ...]) -> None:
ax.contourf(*data)
class LinearPlotter:
def plot(self, ax: plt.Axes, data: Tuple[ndarray, ...]) -> None:
ax.plot(*data)
# 3. 组合容器:专注结构逻辑,依赖协议而非具体类
class MonthlyPlot:
def __init__(self, plotter: Plotter, n_months: int = 12):
self.plotter = plotter
self.n_months = n_months # 支持自定义周期(如季度=4)
def plot(self, month_to_data: List[Tuple[ndarray, ...]]) -> plt.Figure:
if len(month_to_data) != self.n_months:
raise ValueError(f"Expected {self.n_months} months of data, got {len(month_to_data)}")
fig, axs = plt.subplots(self.n_months, 1, figsize=(8, 2 * self.n_months))
# 处理单子图时 axs 不是数组的情况
if self.n_months == 1:
axs = [axs]
for month, data in enumerate(month_to_data):
self.plotter.plot(ax=axs[month], data=data)
return fig
# 使用示例:灵活组合,语义清晰
monthly_contour = MonthlyPlot(ContourPlotter(), n_months=12)
fig = monthly_contour.plot(month_to_data)
# 同一布局逻辑,切换渲染策略仅需换一个对象
monthly_bar = MonthlyPlot(BarPlotter(), n_months=4) # 季度柱状图注意事项与进阶建议:
⚠️ 避免过度设计:若仅需 2–3 种图表且无复用需求,直接函数式编程(如 def plot_monthly_contour(...))可能更 Pythonic;
⚠️ 考虑使用 functools.singledispatch 或 @dataclass + __call__ 实现轻量策略;
? 进一步解耦:可将 month_to_data 结构抽象为 DataSource 协议,使 MonthlyPlot 完全脱离数据格式假设;
? 生产级增强:添加 plt.style.context 支持、savefig 封装、异常统一处理等。
总结而言,当你发现抽象类名称开始包含多个形容词(AbstractSeasonalMultiPanelContour),就是该按下暂停键、审视是否该用组合来“搭积木”,而非用继承去“建高楼”——这才是 Python 社区推崇的清晰、灵活、可持续的面向对象实践。

















