跨项目设计模式(二):策略模式——从 ImageKnife 的加载器到 HMRouter 的生命周期

13 阅读8分钟

一、前言

这是跨项目设计模式系列的第二篇,聚焦策略模式(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>

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 字段类型和前缀选择具体策略:

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 个枚举值来描述不同的降采样策略:

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 根据原图和目标尺寸计算缩放因子:

export interface BaseDownsampling {
  getName(): string;
  getScaleFactor(
    sourceWidth: number, sourceHeight: number,
    requestWidth: number, requestHeight: number,
    downsampType?: DownsampleStrategy
  ): number;
}

四个具体实现类分别是 FitCenterAtLeastAtMostCenterInsideDefaultDownSampling。它们的算法逻辑差异较大:AtMost 会先计算最大整数缩放因子再取 2 的次幂,FitCenter 根据 MEMORY 或 QUALITY 模式决定取 Math.max 还是 Math.minDefaultDownSampling 只在分辨率超过 7680x4320 时才介入。

Downsampler 类充当上下文角色,通过 getDownsampler 方法将枚举映射到具体策略实例:

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_MEMORYFIT_CENTER_QUALITY 共用 FitCenter,区分行为靠传入的 downsampType 参数。策略对象内部再用 downsampType 做一次分支。这种做法把粒度划分放在了策略类内部,减少了类的数量,代价是策略类不再"纯粹"——它需要感知自己服务于哪个枚举值。

calculateScaling 在拿到缩放因子后,还会根据图片格式(PNG、WebP、其他)决定是 floorround 还是直接除。这属于上下文逻辑,不归策略管。

四、HMRouter 的生命周期 Handler Map:15 个 Strategy 消除 switch-case

HMRouter 的页面生命周期有 15 个状态:onPrepareonAppearonDisAppearonShownonHiddenonWillAppearonWillDisappearonWillShowonWillHideonReadyonBackPressedonResultonActiveonInactiveonNewParam。如果在 dispatch 方法中用 switch-case 对 15 个状态逐一调用对应的生命周期方法,代码会冗长且高度重复。

HMRouter 的做法是定义一个 IExecuteLifecycleHandler 接口,每个生命周期状态对应一个实现类:

interface IExecuteLifecycleHandler {
  executeLifecycle(
    lifecycle: IHMLifecycle,
    ctx: HMLifecycleContext,
    callback?: ESObject
  ): void | boolean;
}

15 个实现类的结构几乎一致,每个只做一件事——调用 IHMLifecycle 上对应的可选方法。例如 OnShown 调用 lifecycle.onShown?.(ctx)OnBackPressed 调用 lifecycle.onBackPressed?.(ctx) 并返回布尔结果。

全部策略实例注册到一个 Map<AllLifecycleState, IExecuteLifecycleHandler> 中:

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 方法的核心调用因此被压缩成一行:

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

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;
};

四个子基类 MemoryCacheInterceptorFileCacheInterceptorLoadInterceptorDecodeInterceptor 分别对应缓存读写、资源加载和解码阶段。Process 的默认实现是:先调 Resolve,成功则终止链条,失败则交给 next_ 处理。

默认 Loader 的组装在 ImageKnifeLoader::CreateDefaultImageLoader 中完成:

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);
}

DownloadInterceptorDefaultResolve 中判断 URL 是否为网络地址,如果是本地路径(/data/storagefile: 开头)则返回 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 查表还是责任链,都能获得策略模式的核心收益——新增变体时不需要修改或尽量少修改已有的业务代码。