起因:
重构代码的时候看到同事写的屎山代码,大概如下:
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方法实现可能会有所不同,缺乏统一的接口规范,可能会导致使用上的不一致。
叠甲:以上代码完全由豆包将业务代码脱敏加工而成,如有语法错误等,与本人无关