一刀切——直到不行

一刀切——直到不行

_

万能设计——直到它不再万能

追求可复用软件是一项崇高的事业。它承诺效率、可维护性,以及跨复杂系统的统一愿景。但正如任何花费时间完善抽象概念的人所知,从"一个合理的观察"到真正通用的设计之路往往充满挑战。初始的简单性很快会转变为僵化的结构,扼杀创新并迫使开发人员做出笨拙的妥协。这种二元性正是"万能设计"悖论的核心。

抽象的魅力

通常,旅程始于一个简单的真理:我们在重复自己。在一个典型的电商应用中,我们可能有独立的产品、客户、分类和订单服务。虽然每个服务在其领域内独特,但它们通常执行相似的操作。考虑以下TypeScript中假设的ProductService代码片段:

interface ProductService {
  getAll(): Product[];
  getById(id: string): Product | null;
  create(product: Product): Product;
  update(id: string, updates: Partial<Product>): Product;
  delete(id: string): void;
}

这种模式在许多领域都很熟悉。CustomerService会镜像这些操作,可能带有细微变化,如处理地址或联系信息。初始抽象看似显而易见:创建一个封装这些通用模式的通用CRUDService。这感觉像是个胜利——代码更少,组织更好,维护更方便。

过度泛化的陷阱

问题在于当我们试图将所有用例都塞进这个单一抽象时出现。CRUDService最初是一个干净的起点:

abstract class CRUDService<T> {
  abstract getAll(): T[];
  abstract getById(id: string): T | null;
  abstract create(data: Partial<T>): T;
  abstract update(id: string, updates: Partial<T>): T;
  abstract delete(id: string): void;
}

一开始,这似乎很完美。现在我们可以将ProductService定义为具体实现:

class ProductService extends CRUDService<Product> {
  constructor(private repo: ProductRepository) {
    super();
  }

  getAll(): Product[] {
    return this.repo.findAll();
  }

  getById(id: string): Product | null {
    return this.repo.findById(id);
  }

  create(product: Product): Product {
    return this.repo.save(product);
  }

  update(id: string, updates: Partial<Product>): Product {
    return this.repo.update(id, updates);
  }

  delete(id: string): void {
    this.repo.delete(id);
  }
}

这可行!但随着我们引入更多复杂性,抽象开始出现裂痕。当我们的某个服务需要其他服务没有的功能时会发生什么?也许OrderService需要实时验证,而ProductService不需要。或者,CustomerService需要处理客户创建后发送邮件的异步后台任务。

试图将这些不同的需求塞进同一个CRUDService抽象很快会变成噩梦。我们引入可选参数、回调和一大堆if/else语句,使代码变得臃肿且难以理解。曾经简洁明了的抽象,现在感觉像紧身衣。

粒度化设计的路径

关键洞察在于认识到并非所有功能都同等重要。有些模式确实是通用的,而有些则是领域特定的。解决方案不是完全抛弃抽象,而是要更谨慎地选择我们想要泛化的内容。

1. 确定核心模式

首先,确定真正通用的模式,这些模式很可能在许多服务中共享。在大多数CRUD操作中,getById、getAll以及基本的create、update、delete可能普遍适用。这些可以构成核心抽象的基础。

2. 允许扩展

对于更专业的功能,使用继承和组合来允许服务在不需要额外功能的情况下扩展核心。TypeScript的abstract class非常适合此目的:

abstract class BaseCRUDService<T> {
  abstract getAll(): T[];
  abstract getById(id: string): T | null;
  abstract create(data: Partial<T>): T;
  abstract update(id: string, updates: Partial<T>): T;
  abstract delete(id: string): void;
}

class ProductService extends BaseCRUDService<Product> {
  // 核心CRUD实现
}

class OrderService extends BaseCRUDService<Order> {
  async validateOrder(order: Order): Promise<boolean> {
    // 订单特定验证逻辑
    return true;
  }
}

3. 利用混入实现可选功能

对于可选但可能在多个服务中有用的功能,考虑使用混入或实用函数。这使核心抽象保持精简,同时允许服务按需选择所需功能。

function withValidation<T extends BaseCRUDService<any>>(service: T): T & {
  validate(data: T['T']): boolean;
} {
  return {
    ...service,
    validate(data: T['T']) {
      // 验证逻辑
      return true;
    },
  } as any;
}

class AdvancedProductService extends withValidation(ProductService) {
  // 继承CRUD和验证
}

4. 保持抽象聚焦

抽象的黄金法则是YAGNI(你不会需要它)。如果一个功能没有明确被多个服务需要,就不要将其添加到抽象中。过度泛化是创建僵化结构并使其成为维护负担的最快方式。

真实案例:电商平台

让我们重新审视我们的电商平台。最初,我们有重复CRUD逻辑的独立服务。通过重构,我们实现了更清晰的架构:

// 核心抽象
abstract class BaseCRUDService<T> {
  // ... 标准CRUD方法
}

// 领域特定服务
class ProductService extends BaseCRUDService<Product> {
  // 产品特定逻辑
}

class OrderService extends BaseCRUDService<Order> {
  // 订单特定逻辑
  validateOrder(order: Order): boolean {
    // 实时验证
    return true;
  }
}

// 使用混入实现可选功能
function withEmailNotifications<T extends BaseCRUDService<any>>(service: T): T & {
  sendCreationEmail(data: T['T']): void;
} {
  return {
    ...service,
    sendCreationEmail(data: T['T']) {
      // 发送邮件逻辑
    },
  } as any;
}

class NotifiedCustomerService extends withEmailNotifications(CustomerService) {
  // 继承CRUD和邮件通知
}

这种结构使核心CRUDService保持专注,同时允许领域特定服务按需扩展。withEmailNotifications混入提供了可选功能,可应用于需要它的任何服务。

粒度的代价

当然,这种方法并非没有权衡。粒度化设计可能导致代码重复增加,因为每个服务都需要定义核心模式的实现。如果管理不当,还可能存在不一致性。但这些成本通常会被灵活性和可维护性的好处所抵消。

总结

"万能设计"方法是一个诱人的起点,但很少是最终目的地。最成功的软件系统在泛化和专业化之间取得了平衡。通过识别核心模式、允许扩展并牢记YAGNI,我们可以创建既可复用又可适应的抽象。关键不是过度简化,而是以细致入微的方式设计,认识到并非所有功能都同等重要——而这完全可以接受。

# AI Roto:Blender中手动逐帧动画的终结 2026-09-06
什么是DESIGN.md?通过AI进行UI设计的共享指南 2026-09-06

评论区