过度设计(一个按钮点击事件抽象出 5 层接口)
过度设计是前端开发中一种常见的反模式,表面上追求"高扩展性"和"低耦合",实则制造出复杂的抽象层。一个简单的按钮点击事件被拆分成5层接口,每个接口只做微不足道的转发,最终连原作者都难以理解数据流向。
第一层:事件监听器包装器
从最基础的点击事件开始,我们首先创建"事件监听器管理层":
// 第一层抽象:事件监听器包装器
class EventListenerWrapper {
private handlers: Map<string, (event: Event) => void> = new Map();
register(element: HTMLElement, eventType: string, handler: (event: Event) => void) {
const wrappedHandler = (e: Event) => {
console.log(`[${new Date().toISOString()}] Event ${eventType} triggered`);
handler(e);
};
this.handlers.set(`${element.id}-${eventType}`, wrappedHandler);
element.addEventListener(eventType, wrappedHandler);
}
// 还应该实现unregister方法,虽然从来没人用过
}
这个包装器不仅记录日志,还把所有处理器存在Map里。实际上我们只是要给按钮加个点击事件,但现在已经需要管理事件处理器的生命周期了。
第二层:业务逻辑分发器
点击事件不能直接处理业务逻辑,我们需要"业务逻辑分发中心":
// 第二层抽象:业务逻辑路由器
interface IActionHandler {
handleAction(payload: unknown): Promise<void>;
}
class BusinessLogicDispatcher {
private static instance: BusinessLogicDispatcher;
private handlers: Record<string, IActionHandler> = {};
private constructor() {} // 单例模式,必须的
static getInstance(): BusinessLogicDispatcher {
if (!BusinessLogicDispatcher.instance) {
BusinessLogicDispatcher.instance = new BusinessLogicDispatcher();
}
return BusinessLogicDispatcher.instance;
}
registerHandler(actionType: string, handler: IActionHandler) {
this.handlers[actionType] = handler;
}
async dispatch(actionType: string, payload: unknown) {
if (!this.handlers[actionType]) {
throw new Error(`No handler registered for ${actionType}`);
}
return this.handlers[actionType].handleAction(payload);
}
}
现在点击事件需要先经过分发器,再由分发器找到真正的处理器。我们引入了单例模式和接口,代码复杂度开始指数级增长。
第三层:领域服务抽象
真正的业务逻辑应该放在"领域服务"中,所以我们创建:
// 第三层抽象:领域服务
interface IUserService {
updateUserProfile(data: unknown): Promise<void>;
}
class UserService implements IUserService {
private apiClient: ApiClient;
private logger: Logger;
private validator: Validator;
constructor() {
this.apiClient = new ApiClient('/api');
this.logger = Logger.getInstance();
this.validator = new Validator();
}
async updateUserProfile(data: unknown) {
try {
this.validator.validate(data);
const response = await this.apiClient.post('/profile', data);
this.logger.log('Profile updated', response);
} catch (error) {
this.logger.error('Update failed', error);
throw new DomainException('Failed to update profile');
}
}
}
注意这里已经出现了4个新的依赖项,每个依赖项都有自己的初始化逻辑和配置。我们距离那个简单的点击事件越来越远。
第四层:DTO转换层
数据不能直接从事件传递到服务层,需要经过DTO转换:
// 第四层抽象:DTO转换器
interface IDtoTransformer<TFrom, TTo> {
transform(from: TFrom): TTo;
}
class ClickEventToProfileDtoTransformer
implements IDtoTransformer<MouseEvent, UserProfileDto> {
transform(event: MouseEvent): UserProfileDto {
const target = event.target as HTMLElement;
return {
userId: target.dataset['userId'],
lastClickPosition: {
x: event.clientX,
y: event.clientY
},
timestamp: new Date().toISOString()
};
}
}
这个转换器把鼠标点击位置也作为用户资料的一部分,虽然业务上根本不需要这些信息,但架构上很"完整"。
第五层:响应处理器
最后,我们需要处理服务层返回的结果:
// 第五层抽象:响应处理器
interface IResponseHandler<T> {
handleSuccess(response: T): void;
handleError(error: Error): void;
}
class ProfileUpdateResponseHandler implements IResponseHandler<void> {
private notificationService: NotificationService;
constructor() {
this.notificationService = new NotificationService();
}
handleSuccess() {
this.notificationService.show({
type: 'success',
message: 'Profile updated successfully',
duration: 5000
});
}
handleError(error: Error) {
this.notificationService.show({
type: 'error',
message: `Update failed: ${error.message}`,
duration: 10000
});
Sentry.captureException(error);
}
}
最终组装
现在把这些层全部组装起来:
// 初始化所有组件
const wrapper = new EventListenerWrapper();
const dispatcher = BusinessLogicDispatcher.getInstance();
const userService = new UserService();
const transformer = new ClickEventToProfileDtoTransformer();
const responseHandler = new ProfileUpdateResponseHandler();
// 注册业务处理器
dispatcher.registerHandler('UPDATE_PROFILE', {
async handleAction(payload: MouseEvent) {
const dto = transformer.transform(payload);
try {
await userService.updateUserProfile(dto);
responseHandler.handleSuccess();
} catch (error) {
responseHandler.handleError(error as Error);
}
}
});
// 绑定按钮点击事件
wrapper.register(
document.getElementById('profile-button')!,
'click',
(event) => dispatcher.dispatch('UPDATE_PROFILE', event)
);
维护噩梦的开始
三个月后,新来的开发者需要修改这个点击逻辑:
- 首先在事件监听器里找不到业务逻辑
- 追踪到业务分发器,发现是动态注册的处理器
- 找到处理器实现,发现还要经过DTO转换
- 调试时发现数据在转换过程中被意外修改
- 错误处理分散在至少三个不同的层级
- 想添加一个简单的取消逻辑需要修改所有接口
如何识别过度设计
- 接口膨胀:每个简单操作都需要实现多个接口方法
- 间接调用:跟踪一个调用栈需要跳转5个以上文件
- 僵尸代码:存在大量从未被使用的"扩展点"
- 配置疲劳:添加新功能需要修改多处配置文件
- 抽象泄漏:下层实现细节不断渗透到上层接口
合理抽象的边界
- 当相同模式出现三次以上再考虑抽象
- 保持调用栈不超过3层(视图-服务-API)
- 避免为"可能"的需求预留扩展点
- 确保每个抽象都能独立解释其存在价值
- 定期重构而不是预先设计
过度设计的代价
- 认知负荷:新成员需要理解整个架构才能修改简单功能
- 调试困难:错误可能发生在任何抽象层
- 性能损耗:每层抽象都带来额外的函数调用和内存占用
- 修改阻力:简单的需求变更需要修改多个文件
- 测试复杂度:每个接口都需要单独的单元测试
重构方向
如果已经陷入这种架构,可以考虑:
// 直截了当的实现
document.getElementById('profile-button')?.addEventListener('click', async () => {
try {
const response = await fetch('/api/profile', {
method: 'POST',
body: JSON.stringify({ userId: '123' })
});
showNotification('Profile updated');
} catch (error) {
showNotification('Update failed');
console.error(error);
}
});
这个版本可能不够"企业级",但它:
- 可读性强
- 易于修改
- 调试路径清晰
- 没有隐藏的依赖
- 新人能立即理解
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn