一、前言
这是跨项目设计模式系列的第二篇,聚焦策略模式(Strategy Pattern)。策略模式的核心思路是把一组可互换的算法封装在独立的类里,让调用方通过统一接口选择具体实现,而不必在业务代码中写大量 if-else 或 switch-case。
ImageKnife、ImageKnifePro 和 HMRouter 三个仓库在不同场景下都运用了这一模式,但落地方式各有差异。ImageKnife 用它来分发图片加载策略和下采样算法,HMRouter 用它消除生命周期分发中的条件分支,ImageKnifePro 则用拦截器链替代了显式的策略选择。下面逐一展开。
二、ImageKnife 的 ImageLoaderFactory:按 URL 类型分发
ImageKnife 需要处理多种来源的图片:HTTP/HTTPS 网络地址、datashare:// 和 file:// 开头的文件系统路径、应用 Resource 资源 ID、本地沙箱文件,以及用户自定义的加载方式。如果把这些逻辑全部塞进同一个函数,代码会臃肿且难以维护。
策略接口 IImageLoaderStrategy 只定义了一个方法 loadImage,接收请求对象、回调数据等参数,返回 Promise<void>:
typescript
export interface IImageLoaderStrategy {
loadImage(
request: RequestJobRequest,
requestList: List<ImageKnifeRequestWithSource> | undefined,
fileKey: string,
callBackData: ImageKnifeData,
callBackTimeInfo: TimeInfo
): Promise<void>;
}
五个具体策略类分别实现这一接口:HttpLoaderStrategy 负责网络下载与文件缓存回退,FileSystemLoaderStrategy 处理 datashare:// 和 file:// 协议,ResourceLoaderStrategy 读取应用内 Resource 资源,FileLocalLoadStrategy 加载本地沙箱文件,CustomLoaderStrategy 则将加载逻辑交给业务方自定义回调。
工厂类 ImageLoaderFactory.getLoaderStrategy 根据请求的 src 字段类型和前缀选择具体策略:
typescript
export class ImageLoaderFactory {
static getLoaderStrategy(request: RequestJobRequest): IImageLoaderStrategy | null {
if (request.customGetImage !== undefined &&
request.requestSource === ImageKnifeRequestSource.SRC) {
return new CustomLoaderStrategy();
}
if (typeof request.src === 'string') {
if (request.src.startsWith('datashare://') || request.src.startsWith('file://')) {
return new FileSystemLoaderStrategy();
} else if (ImageKnifeLoader.isLocalLoadSrc(request.context, request.src)) {
return new FileLocalLoadStrategy();
} else if (request.src.startsWith('http://') || request.src.startsWith('https://')) {
return new HttpLoaderStrategy();
} else {
return null;
}
} else if (typeof request.src === 'number') {
return new ResourceLoaderStrategy();
}
return null;
}
}
这段代码有一个值得留意的细节:CustomLoaderStrategy 在自定义加载失败后,会清除 customGetImage 回调、重新调用 ImageLoaderFactory.getLoaderStrategy 获取默认策略进行回退。这意味着策略之间存在降级关系,而非简单的互斥选择。工厂内部仍然用 if-else 做路由,但每个分支的实际加载逻辑已经被隔离到独立的类中,新增一种来源只需添加一个策略类和一条分支判断。
三、ImageKnife 的 DownsampleStrategy:7 种下采样算法
图片加载到内存前通常需要降采样以节省内存。ImageKnife 定义了 8 个枚举值来描述不同的降采样策略:
typescript
export enum DownsampleStrategy {
AT_MOST, // 请求尺寸大于实际尺寸时不放大
FIT_CENTER_MEMORY, // 两边自适应,内存优先
FIT_CENTER_QUALITY, // 两边自适应,质量优先
CENTER_INSIDE_MEMORY, // 按宽高比最大比适配,内存优先
CENTER_INSIDE_QUALITY,// 按宽高比最大比适配,质量优先
AT_LEAST, // 等比缩放取最小比例
NONE, // 不降采样
DEFAULT // 超过 8K 分辨率时等比缩放
}
策略接口 BaseDownsampling 要求实现两个方法------getName 返回策略名称,getScaleFactor 根据原图和目标尺寸计算缩放因子:
typescript
export interface BaseDownsampling {
getName(): string;
getScaleFactor(
sourceWidth: number, sourceHeight: number,
requestWidth: number, requestHeight: number,
downsampType?: DownsampleStrategy
): number;
}
四个具体实现类分别是 FitCenter、AtLeast、AtMost、CenterInside 和 DefaultDownSampling。它们的算法逻辑差异较大:AtMost 会先计算最大整数缩放因子再取 2 的次幂,FitCenter 根据 MEMORY 或 QUALITY 模式决定取 Math.max 还是 Math.min,DefaultDownSampling 只在分辨率超过 7680x4320 时才介入。
Downsampler 类充当上下文角色,通过 getDownsampler 方法将枚举映射到具体策略实例:
typescript
getDownsampler(downsampType: DownsampleStrategy) {
switch (downsampType) {
case DownsampleStrategy.FIT_CENTER_MEMORY:
case DownsampleStrategy.FIT_CENTER_QUALITY:
return new FitCenter();
case DownsampleStrategy.AT_MOST:
return new AtMost();
case DownsampleStrategy.CENTER_INSIDE_MEMORY:
case DownsampleStrategy.CENTER_INSIDE_QUALITY:
return new CenterInside();
case DownsampleStrategy.AT_LEAST:
return new AtLeast();
case DownsampleStrategy.DEFAULT:
return new DefaultDownSampling();
default:
throw new Error('Unsupported downsampling strategy');
}
}
这里有一点和加载器策略不同:8 个枚举值映射到 5 个实现类,FIT_CENTER_MEMORY 和 FIT_CENTER_QUALITY 共用 FitCenter,区分行为靠传入的 downsampType 参数。策略对象内部再用 downsampType 做一次分支。这种做法把粒度划分放在了策略类内部,减少了类的数量,代价是策略类不再"纯粹"------它需要感知自己服务于哪个枚举值。
calculateScaling 在拿到缩放因子后,还会根据图片格式(PNG、WebP、其他)决定是 floor、round 还是直接除。这属于上下文逻辑,不归策略管。
四、HMRouter 的生命周期 Handler Map:15 个 Strategy 消除 switch-case
HMRouter 的页面生命周期有 15 个状态:onPrepare、onAppear、onDisAppear、onShown、onHidden、onWillAppear、onWillDisappear、onWillShow、onWillHide、onReady、onBackPressed、onResult、onActive、onInactive、onNewParam。如果在 dispatch 方法中用 switch-case 对 15 个状态逐一调用对应的生命周期方法,代码会冗长且高度重复。
HMRouter 的做法是定义一个 IExecuteLifecycleHandler 接口,每个生命周期状态对应一个实现类:
typescript
interface IExecuteLifecycleHandler {
executeLifecycle(
lifecycle: IHMLifecycle,
ctx: HMLifecycleContext,
callback?: ESObject
): void | boolean;
}
15 个实现类的结构几乎一致,每个只做一件事------调用 IHMLifecycle 上对应的可选方法。例如 OnShown 调用 lifecycle.onShown?.(ctx),OnBackPressed 调用 lifecycle.onBackPressed?.(ctx) 并返回布尔结果。
全部策略实例注册到一个 Map<AllLifecycleState, IExecuteLifecycleHandler> 中:
typescript
const executeLifecycleHandlerMap: Map<AllLifecycleState, IExecuteLifecycleHandler> = new Map();
executeLifecycleHandlerMap.set(InnerLifecycleState.onPrepare, new OnPrepare());
executeLifecycleHandlerMap.set(InnerLifecycleState.onAppear, new OnAppear());
executeLifecycleHandlerMap.set(HMLifecycleState.onDisAppear, new OnDisAppear());
// ... 共 15 条
executeLifecycleHandlerMap.set(HMLifecycleState.onNewParam, new OnNewParam());
dispatch 方法的核心调用因此被压缩成一行:
typescript
private executeLifecycle(state: AllLifecycleState, lifecycle: IHMLifecycle,
ctx: HMLifecycleContext, callback?: ESObject): void | boolean {
return executeLifecycleHandlerMap.get(state)?.executeLifecycle(lifecycle, ctx, callback);
}
和 ImageKnife 的两个策略场景比较,这里的策略选择不依赖工厂或 switch-case,而是用 Map 直接查表。Map 的 key 是生命周期状态枚举,value 是策略实例。这种方式在状态数量多且后续可能继续增长的场景下更有优势------新增一个生命周期只需要写一个 Handler 类和一行 Map.set,不会触碰现有的分发逻辑。
dispatch 方法还承担了优先级排序和全局生命周期合并的职责。它将全局生命周期、Observer 回调和页面绑定的生命周期合并成一个数组,按 priority 降序排列后逐一执行。onBackPressed 状态有特殊处理:任一 Handler 返回 true 就终止后续执行。这些都是上下文层面的编排,策略类本身不感知。
五、ImageKnifePro 的 Interceptor 替代 Strategy
ImageKnifePro 是 ImageKnife 的 C++ 重写版本,它没有沿用 IImageLoaderStrategy + ImageLoaderFactory 的策略模式,而是采用了拦截器-责任链的设计。
基类 Interceptor 定义了纯虚函数 Resolve 和链式调用的 Process:
cpp
class Interceptor {
public:
std::string name;
virtual bool Resolve(std::shared_ptr<ImageKnifeTask> task) = 0;
virtual void Cancel(std::shared_ptr<ImageKnifeTask> task);
virtual bool Process(std::shared_ptr<ImageKnifeTask> task,
std::function<bool(std::shared_ptr<ImageKnifeTask>)> resolveCallback = nullptr);
protected:
std::shared_ptr<Interceptor> next_ = nullptr;
};
四个子基类 MemoryCacheInterceptor、FileCacheInterceptor、LoadInterceptor、DecodeInterceptor 分别对应缓存读写、资源加载和解码阶段。Process 的默认实现是:先调 Resolve,成功则终止链条,失败则交给 next_ 处理。
默认 Loader 的组装在 ImageKnifeLoader::CreateDefaultImageLoader 中完成:
cpp
auto loader = std::make_shared<ImageKnifeLoaderInternal>();
loader->AddMemoryCacheInterceptor(memoryInterceptor);
loader->AddLoadInterceptor(downloadInterceptor);
loader->AddLoadInterceptor(resourceInterceptor);
loader->AddDecodeInterceptor(decodeInterceptor);
loader->AddFileCacheInterceptor(FileCacheInterceptorDefault::GetInstance());
if (ImageKnifeDecoderAvif::IsAvifEnable()) {
loader->AddDecodeInterceptor(decodeInterceptorAvif, Position::END);
}
DownloadInterceptorDefault 在 Resolve 中判断 URL 是否为网络地址,如果是本地路径(/data/storage、file: 开头)则返回 false 让链条传递给下一个拦截器 ResourceInterceptorDefault。这与 ImageKnife 工厂模式中由 ImageLoaderFactory 集中路由不同------在责任链中,每个拦截器自行判断能否处理当前请求,不能则交给下家。
这种演进有实际原因。ImageKnifePro 的 LoadInterceptor 需要支持异步分离(Detach/OnComplete)、CRC32 校验和多域名自动重试(RetryFallbackUrls)。这些横切逻辑如果用纯策略模式实现,要么每个策略类都要重复处理,要么需要在策略外层再包一层。拦截器链天然支持在 Process 中嵌入这些通用逻辑,各 Resolve 实现只需关注自己的核心职责。
从扩展性看,业务方可以通过 AddLoadInterceptor 将自定义下载器插入链条的指定位置,无需修改已有的拦截器代码,也无需改动工厂类的 if-else 分支。
六、策略模式 vs 工厂模式的边界
读完三个仓库的代码,一个直观的疑问是:ImageLoaderFactory 到底是策略模式还是工厂模式?
从定义上看,工厂模式关心对象的创建,策略模式关心算法的选择。ImageLoaderFactory.getLoaderStrategy 确实在"创建"策略对象,但调用方拿到策略对象后只做一件事------调用 loadImage。创建不是目的,选择合适的加载算法才是目的。所以它是用工厂手法实现的策略模式,两者在这里重叠了。
三个仓库的实现可以按选择机制分为三类。第一类是工厂集中路由:ImageLoaderFactory 根据输入条件在一个函数内选择策略,新增策略需要修改工厂。第二类是 Map 查表:HMRouter 的 executeLifecycleHandlerMap 把状态枚举映射到 Handler 实例,新增状态只需新增一个 Handler 和一行注册代码。第三类是链式自决:ImageKnifePro 的拦截器各自判断能否处理请求,新增拦截器无需修改已有代码。
三类方式各有适用范围。工厂路由适合选项少且稳定的场景,Map 查表适合选项多但行为单一的场景(15 个 Handler 的实现几乎一样),链式自决适合各策略之间存在回退、重试等协作关系的场景。
在实际开发中,区分"这是策略模式还是工厂模式"意义不大。关键问题是:算法的选择逻辑放在哪里,选择之后的执行逻辑是否被隔离。只要做到这两点,无论用 if-else 工厂、Map 查表还是责任链,都能获得策略模式的核心收益------新增变体时不需要修改或尽量少修改已有的业务代码。