
本文介绍如何用接口抽象与组合替代强制继承,消除无实际逻辑的抽象方法(如 _remove_exclusions),提升代码可维护性与可扩展性。
本文介绍如何用接口抽象与组合替代强制继承,消除无实际逻辑的抽象方法(如 `_remove_exclusions`),提升代码可维护性与可扩展性。
在构建分层测量计算系统(如医疗质量指标 ASC)时,常需定义具备共性行为的抽象基类(如 Denominator、Numerator)。但当某些子类(如 Submeasure 的变体 sub2)天然无需实现某抽象方法(如 _remove_exclusions)时,强行保留该方法不仅违反单一职责原则,还会导致代码臃肿、语义模糊,甚至引发“空实现陷阱”(即 pass 或无意义覆盖)。
根本症结在于:将行为契约(interface)与实现绑定在继承链中,忽视了“是否需要该能力”是实例级决策,而非类型级约束。解决方案不是增加更多抽象基类(如 Exclusions),而是转向面向接口的组合设计——将排除逻辑抽象为可插拔组件,由具体实例按需装配。
✅ 正确做法:用 Excludable 接口 + 组合替代抽象方法
首先定义轻量、专注的接口:
from abc import ABC, abstractmethod
import pandas as pd
class Excludable(ABC):
"""定义排除操作的契约:输入 DataFrame,返回过滤后 DataFrame"""
@abstractmethod
def remove_exclusions(self, df: pd.DataFrame) -> pd.DataFrame:
"""执行排除逻辑,如移除不符合入组条件的记录"""
...接着,提供默认实现(可按需扩展):
class DefaultExclusion(Excludable):
"""标准排除策略示例:基于 'excluded' 列过滤"""
def remove_exclusions(self, df: pd.DataFrame) -> pd.DataFrame:
if 'excluded' in df.columns:
return df[~df['excluded']].copy()
return df # 无排除列则原样返回然后,重构 Submeasure,移除抽象方法依赖,改为组合式持有:
class Submeasure(Denominator, Numerator, Stratification):
"""
子测量组件基类,支持按需启用排除逻辑
"""
def __init__(
self,
name: str,
data: list[pd.DataFrame],
exclusion_handler: Excludable | None = None
):
self.name = name
self.data = data
self.exclusion_handler = exclusion_handler # 可选依赖,非强制
def apply_exclusions(self) -> None:
"""仅当配置了排除处理器时才执行"""
if self.exclusion_handler:
self.data = [
self.exclusion_handler.remove_exclusions(df)
for df in self.data
]
# 其他原有方法(_set_populace, _remove_exclusions 等)全部移除!此时,Submeasure 不再承担“必须实现排除”的义务,而是将责任委托给 Excludable 实例——这正是开闭原则(对扩展开放,对修改关闭)的体现。
? 在具体场景中灵活装配
以 ASC 测量类为例,清晰区分 sub1 与 sub2 的职责:
class ASC(Measurement):
def __init__(
self,
sub1_data: list[pd.DataFrame],
sub2_data: list[pd.DataFrame] | None = None
):
super().__init__("ASC")
# sub1:完整生命周期,需独立排除
self.__sub1__ = _Sub_1(
name=self.get_name() + "_sub_1",
data=sub1_data,
exclusion_handler=DefaultExclusion() # 显式启用
)
# sub2:复用 sub1 结果,跳过排除阶段
self.__sub2__ = _Sub_2(
name=self.get_name() + "_sub_2",
data=sub2_data
# exclusion_handler 未传入 → 默认为 None → apply_exclusions 无动作
)
def __get_sub2_subset(self, sub1_results: dict[str, pd.DataFrame]) -> tuple[pd.DataFrame, pd.DataFrame]:
# ...(原有逻辑不变:从 sub1 numerator 中提取子集)
populace = sub1_results.get(self.__sub1__.get_name())
populace = populace[populace['numerator']].drop('numerator', axis=1)
stratification = sub1_results.get(self.__sub1__.get_name() + '_stratification')
stratification = stratification[
stratification['patient_id'].isin(populace['patient_id'])
].copy()
return populace, stratification调用时,sub1 可正常执行 apply_exclusions(),而 sub2 即使调用该方法也静默跳过——无需 @override、无需 pass、无歧义、零冗余。
⚠️ 注意事项与最佳实践
-
避免“伪抽象”:若某抽象方法在多数子类中为空实现或抛
NotImplementedError,说明它不属于该抽象层级,应被重构为可选组件。 -
命名清晰化:接口名(如
Excludable)应体现能力而非动作;方法名(如remove_exclusions)应明确输入输出,避免下划线前缀(_)暗示内部使用。 -
空处理安全:组合对象(如
exclusion_handler)应允许为None,并在调用处做防御性检查(如if handler:),而非强求非空。 -
未来扩展友好:新增排除策略(如
AgeBasedExclusion、GeographicExclusion)只需实现Excludable,无需修改任何已有类或继承关系。
这种设计让架构更贴近真实业务语义:sub1 是独立计算单元,sub2 是派生视图——它们共享结构,但不共享所有行为。抽象不再是为了“看起来统一”,而是为了“按需赋能”。

















