策略模式-淘宝订单列表的「按钮」实战

起因:

重构代码的时候看到同事写的屎山代码,大概如下:

dart 复制代码
// 该方法用于获取更多操作选项列表,并根据不同条件动态添加操作项
// 最后更新状态以反映这些操作选项和按钮文本
void getMoreOperations() {
    // 初始化更多操作列表,默认包含一个 "Edit" 操作
    final List<String> moreOperations = ["Edit"];
    // 声明一个可空的字符串变量,用于存储按钮的第二文本
    String? _btnText;

    // 如果客户端信息标志 1 为真,添加操作 1 到更多操作列表中
    if (clientInfoFlag1 == true) {
      moreOperations.add(OperationEnum.Operation1.label);
    }
    // 如果客户端信息标志 2 为真,添加操作 2 到更多操作列表中
    if (clientInfoFlag2 == true) {
      moreOperations.add(OperationEnum.Operation2.label);
    }

    // 如果客户端信息标志 3 为真,添加操作 3 到更多操作列表中
    if (clientInfoFlag3 == true) {
      moreOperations.add(OperationEnum.Operation3.label);
    }

    // 通用角色判断条件:满足角色标志 1 或角色标志 2 或角色标志 3
    // 若满足条件,则添加 "Reassign" 操作到更多操作列表中
    if (roleFlag1 || roleFlag2 || roleFlag3) {
      moreOperations.add(OperationEnum.Reassign.label);
    }

    // 如果允许呼叫
    if (callEnabled) {
      // 若可以创建试驾
      if (createTestDrive) {
        // 同时添加 "ReserveTestDrive" 和 "ImmediateTestDrive" 操作到更多操作列表中
        moreOperations.addAll([OperationEnum.ReserveTestDrive.label, OperationEnum.ImmediateTestDrive.label]);
      }
      // 设置按钮第二文本为 "ContactCustomer"
      _btnText = OperationEnum.ContactCustomer.label;
    } else {
      // 若不允许呼叫,但可以创建试驾
      if (createTestDrive) {
        // 添加 "ImmediateTestDrive" 操作到更多操作列表中
        moreOperations.add(OperationEnum.ImmediateTestDrive.label);
        // 设置按钮第二文本为 "ReserveTestDrive"
        _btnText = OperationEnum.ReserveTestDrive.label;
      }
    }

    // 发出状态更新,将更多操作列表和按钮第二文本更新到状态中
    emit(state.copyWith(moreOperations: moreOperations, btnSecondText: _btnText));
  }

// 该方法根据用户选择的操作执行相应的处理逻辑
void moreOperationAction(String operation, BuildContext context) {
    // 通过传入的操作字符串获取对应的操作枚举
    final operationEnum = OperationEnum.getButtonModelEnum(operation);
    // 根据操作枚举进行不同的处理
    switch (operationEnum) {
      // 若选择的操作是 "Edit"
      case OperationEnum.Edit:
        // 调用编辑处理方法
        edit();
        break;
      // 若选择的操作是 "Reassign"
      case OperationEnum.Reassign:
        // 调用通用处理程序的可分配处理方法,并传入回调函数
        commonHandler.assignableHandler(context, callback: () {
          // 触发通用事件跟踪
          trackEvent("GenericEvent1", "SubEvent1", {});
          // 若角色标志 2 为真
          if (roleFlag2) {
            // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 4
            refreshPage(isRefresh: true, trackId: 4);
          } 
          // 若角色标志 3 为真
          else if (roleFlag3) {
            // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 5
            refreshPage(isRefresh: true, trackId: 5);
          } 
          // 其他情况
          else {
            // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 7
            refreshPage(isRefresh: true, trackId: 7);
          }
        });
        break;
      // 若选择的操作是 "ReserveTestDrive"
      case OperationEnum.ReserveTestDrive:
        // 调用预约试驾处理方法
        reserveDrive();
        break;
      // 若选择的操作是 "ImmediateTestDrive"
      case OperationEnum.ImmediateTestDrive:
        // 进行通用导航,传入页面路径和参数
        navigator?.pushNamed("GenericPagePath",
            arguments: {"customerName": clientInfo?.name, "phone": clientInfo?.mobile});
        break;
      // 若选择的操作是操作 2
      case OperationEnum.Operation2:
        // 调用通用处理程序的操作 2 处理方法,并传入回调函数
        commonHandler.operation2Handler(context, callback: () {
          // 触发通用事件跟踪
          trackEvent("GenericEvent1", "SubEvent2", {});
          // 调用跟进回调方法
          followCallBack();
          // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 1
          refreshPage(isRefresh: true, trackId: 1);
        });
        break;
      // 若选择的操作是操作 1
      case OperationEnum.Operation1:
        // 显示确认对话框,传入上下文、确认处理方法和确认消息
        showDialogConfirm(
            context: context, confirm: operation1Handler, child: "Some confirmation message");
        break;
      // 若选择的操作是操作 3
      case OperationEnum.Operation3:
        // 调用通用处理程序的操作 3 处理方法,并传入回调函数
        commonHandler.operation3Handler(context, callback: () {
          // 调用跟进回调方法
          followCallBack();
          // 刷新页面,设置刷新标志为 true,并指定跟踪 ID 为 1
          refreshPage(isRefresh: true, trackId: 1);
        });
        break;
      // 若选择的操作不在上述枚举范围内
      default:
        // 不做任何处理
        break;
    }
  }

就是最简单的ifElse实现,也不能说他错。但是,实在是,又臭又长。接盘的时候,直接重构了一遍。

这种场景是非常普遍的,可以参考一下淘宝的订单列表按钮实现,和上面业务代码的内容,结构基本一致

接下来用策略模式重构(不懂没关系,看完下面的内容就秒懂🤌🤌🤌🤌🤌🤌)

先说下大致想法:

  • 定义一个ActionButtonData类,用来存放按钮的全部内容,包括名称、code、点击响应等。
  • 观察淘宝的订单列表,按钮结构可以大致分为「更多」,以及三个外露的按钮。我们可以给这些按钮赋一个priority字段,优先级高的优先外露。最后在补充一些业务字段。
  • 最重要的一个方法是,shouldDisplay,每个字类都继承并重写这个方法,将上述字段根据自己业务需要赋值。

代码实现:

dart 复制代码
// 该类用于存储操作按钮的数据信息
class ActionButtonData {
  // 按钮的名称,用于显示
  final String name;
  // 模板代码,用于标识按钮的类型或用途
  final String templateCode;

  /// 用于展示不可操作的原因,弹窗使用
  List<String>? markContent;

  // 流程 ID,可用于跟踪相关流程
  String? flowId;

  /// 流程的状态,例如「不可操作」、「操作中」、「可操作」等,
  /// 用于展示在主副按钮的右上角,以及更多操作的右边
  String? actionDesc;
  // 操作状态码,用于表示不同的操作状态
  // 1 - 可操作、2 - 处理中、3 - 不可操作、21 - 已拒绝(仅特定场景使用)
  int? actionStatusCode;
  // 是否显示按钮
  bool? display;
  // 按钮是否可点击
  bool? isClick;
  // 按钮点击时的处理函数
  Function(ActionButtonData)? onTapHandler;
  // 按钮的优先级,用于排序显示
  final int priority;

  // 判断按钮是否应该显示的方法
  // 子类可重写此方法,在返回之前重置 markContent、actionDesc、actionStatusCode、display、isClick 等属性
  bool shouldDisplay(Map<String, dynamic> dataMap) {
    return true;
  }

  // 构造函数,初始化按钮数据
  ActionButtonData({
    required this.name,
    required this.templateCode,
    this.markContent = const [],
    this.priority = 0,
    this.onTapHandler,
  });

  // 从 JSON 数据创建 ActionButtonData 实例的构造函数
  ActionButtonData.fromJson(Map<String, dynamic> json)
      : name = json["name"],
        markContent = json["markContent"] ?? [],
        priority = json["priority"] ?? 0,
        onTapHandler = json["onTapHandler"],
        templateCode = json["templateCode"];
}

实现淘宝订单的「再买一单」、「挑选服务」、「申请开票」按钮实战:

挑选服务

dart 复制代码
// 挑选服务按钮类,继承自 ActionButtonData
class ServiceSelection extends ActionButtonData {
  ServiceSelection() : super(name: "挑选服务", templateCode: "service_selection", priority: 5);

  @override
  bool shouldDisplay(Map<String, dynamic> dataMap) {
    bool result = false;
    String orderId = "";

    // 检查订单类型是否为手机类型,并且订单状态为已完成
    if (dataMap["orderType"] == "phone" && dataMap["orderStatus"] == "completed") {
      result = true;
    }

    if (dataMap["orderId"] != null) {
      orderId = dataMap["orderId"];
    }

    this.onTapHandler = (ActionButtonData _) {
      // 假设这是通用的事件跟踪方法
      trackEvent("service_selection_button_tap", {"orderId": orderId});
      trackEvent("order_detail_buttons_click", {"templateCode": this.templateCode});

      Function refreshFunction = dataMap["refreshFunction"];

      // 假设这是通用的页面导航方法
      navigateToPage("service_selection_page", {
        "orderId": orderId,
        "onSave": () {
          // 挑选服务完成后刷新页面
          refreshFunction(context: null);
        }
      });
    };

    this.isClick = true;
    return result;
  }
}

申请开票

dart 复制代码
// 申请开票按钮类,继承自 ActionButtonData
class InvoiceApplication extends ActionButtonData {
  InvoiceApplication() : super(name: "申请开票", templateCode: "invoice_application", priority: 6);

  @override
  bool shouldDisplay(Map<String, dynamic> dataMap) {
    bool result = false;
    String orderId = "";

    // 检查订单状态是否为已完成
    if (dataMap["orderStatus"] == "completed") {
      // 获取订单完成时间
      DateTime? completionTime = dataMap["completionTime"] as DateTime?;
      if (completionTime != null) {
        // 计算当前时间
        DateTime now = DateTime.now();
        // 计算完成时间距今的天数
        int daysPassed = now.difference(completionTime).inDays;
        // 检查是否在 180 天内
        if (daysPassed <= 180) {
          result = true;
        }
      }
    }

    if (dataMap["orderId"] != null) {
      orderId = dataMap["orderId"];
    }

    this.onTapHandler = (ActionButtonData _) {
      // 假设这是通用的事件跟踪方法
      trackEvent("invoice_application_button_tap", {"orderId": orderId});
      trackEvent("order_detail_buttons_click", {"templateCode": this.templateCode});

      Function refreshFunction = dataMap["refreshFunction"];

      // 假设这是通用的页面导航方法
      navigateToPage("invoice_application_page", {
        "orderId": orderId,
        "onSave": () {
          // 申请开票完成后刷新页面
          refreshFunction(context: null);
        }
      });
    };

    this.isClick = true;
    return result;
  }
}

再买一单:

dart 复制代码
// 再买一单按钮类,继承自 ActionButtonData
class BuyAgain extends ActionButtonData {
  BuyAgain() : super(name: "再买一单", templateCode: "buy_again", priority: 999);

  @override
  bool shouldDisplay(Map<String, dynamic> dataMap) {
    bool result = false;
    String orderId = "";

    // 检查订单状态是否为已完成
    if (dataMap["orderStatus"] == "completed") {
      result = true;
    }

    if (dataMap["orderId"] != null) {
      orderId = dataMap["orderId"];
    }

    this.onTapHandler = (ActionButtonData _) {
      // 假设这是通用的事件跟踪方法
      trackEvent("buy_again_button_tap", {"orderId": orderId});
      trackEvent("order_detail_buttons_click", {"templateCode": this.templateCode});

      Function refreshFunction = dataMap["refreshFunction"];

      // 假设这是通用的页面导航方法
      navigateToPage("buy_again_page", {
        "orderId": orderId,
        "onSave": () {
          // 再买一单操作完成后刷新页面
          refreshFunction(context: null);
        }
      });
    };

    this.isClick = true;
    return result;
  }
}

最后是按钮列表的构建:

dart 复制代码
List<ActionButtonData> buttonList = [];
if (context != null) {
  buttonList = [
    ViewLogistics(),
    AddEvaluation(),
    SelectService(),
    ApplyForInvoice(),
    DeleteOrder(),
    SellAndReplace(),
    AddToCart(),
    Reorder(),
  ];
  // shouldDisplay方法入参构建省略
  buttonList = buttonList.where((element) => element.shouldDisplay(dataMap) == true).toList();
  buttonList.sort((a, b) => b.priority.compareTo(a.priority));
}

总结:将一大坨的ifElse统一判断按钮,响应按钮,改成让各自业务按钮类自行决定。这样可以大大解耦。(看完赶紧去重构同事的屎山代码!!!!!!!!!!)

(用copilot自我审视了一下,你说得对,下次重构我再改😭)

优点

  • 灵活性高:通过shouldDisplay方法,可以根据不同的条件动态决定按钮是否显示。
  • 可扩展性强:可以通过继承ActionButtonData类,重写shouldDisplay方法和onTapHandler方法,来实现不同的按钮行为。
  • 数据封装良好:将按钮的各种属性(如名称、模板代码、优先级等)封装在一个类中,便于管理和使用。
  • 事件处理方便:通过onTapHandler属性,可以方便地为按钮添加点击事件处理逻辑。
  • 优先级排序:通过priority属性,可以对按钮进行优先级排序,决定按钮的显示顺序。

缺点

  • 代码复杂度高:由于需要重写多个方法来实现不同的按钮行为,代码可能会变得复杂且难以维护。
  • 依赖外部数据:shouldDisplay方法依赖于传入的dataMap数据,如果数据不完整或格式不正确,可能会导致按钮显示逻辑出错。
  • 耦合度高:按钮的显示逻辑和点击事件处理逻辑都在同一个类中,可能会导致类的职责过多,增加耦合度。
  • 缺乏统一的接口:不同按钮的shouldDisplay和onTapHandler方法实现可能会有所不同,缺乏统一的接口规范,可能会导致使用上的不一致。

叠甲:以上代码完全由豆包将业务代码脱敏加工而成,如有语法错误等,与本人无关

相关推荐
十五年专注C++开发33 分钟前
设计模式之适配器模式(二):STL适配器
c++·设计模式·stl·适配器模式·包装器
小墙程序员1 小时前
Flutter 教程(四)包管理
flutter
渊渟岳1 小时前
掌握设计模式--中介者模式
设计模式
云徒川1 小时前
【设计模式】单例模式
设计模式
sunly_3 小时前
Flutter:切换账号功能记录
android·java·flutter
getapi3 小时前
Flutter和React Native在开发app中,哪个对java开发工程师更适合
java·flutter·react native
恋猫de小郭4 小时前
Android 确定废弃「屏幕方向锁定」等 API ,如何让 App 适配大屏和 PC/XR 等场景
android·前端·flutter
木子庆五7 小时前
Android设计模式之模板方法模式
android·设计模式·模板方法模式
Antonio91510 小时前
【设计模式】状态模式
设计模式
木子庆五12 小时前
Android设计模式之工厂方法模式
android·设计模式·工厂方法模式