阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 过度设计(一个按钮点击事件抽象出 5 层接口)

过度设计(一个按钮点击事件抽象出 5 层接口)

作者:陈川 阅读数:26759人阅读 分类: 前端综合

过度设计是前端开发中一种常见的反模式,表面上追求"高扩展性"和"低耦合",实则制造出复杂的抽象层。一个简单的按钮点击事件被拆分成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)
);

维护噩梦的开始

三个月后,新来的开发者需要修改这个点击逻辑:

  1. 首先在事件监听器里找不到业务逻辑
  2. 追踪到业务分发器,发现是动态注册的处理器
  3. 找到处理器实现,发现还要经过DTO转换
  4. 调试时发现数据在转换过程中被意外修改
  5. 错误处理分散在至少三个不同的层级
  6. 想添加一个简单的取消逻辑需要修改所有接口

如何识别过度设计

  1. 接口膨胀:每个简单操作都需要实现多个接口方法
  2. 间接调用:跟踪一个调用栈需要跳转5个以上文件
  3. 僵尸代码:存在大量从未被使用的"扩展点"
  4. 配置疲劳:添加新功能需要修改多处配置文件
  5. 抽象泄漏:下层实现细节不断渗透到上层接口

合理抽象的边界

  1. 当相同模式出现三次以上再考虑抽象
  2. 保持调用栈不超过3层(视图-服务-API)
  3. 避免为"可能"的需求预留扩展点
  4. 确保每个抽象都能独立解释其存在价值
  5. 定期重构而不是预先设计

过度设计的代价

  1. 认知负荷:新成员需要理解整个架构才能修改简单功能
  2. 调试困难:错误可能发生在任何抽象层
  3. 性能损耗:每层抽象都带来额外的函数调用和内存占用
  4. 修改阻力:简单的需求变更需要修改多个文件
  5. 测试复杂度:每个接口都需要单独的单元测试

重构方向

如果已经陷入这种架构,可以考虑:

// 直截了当的实现
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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌