大数跨境

你不是在开发 Netflix:别再像以前那样写代码了

你不是在开发 Netflix:别再像以前那样写代码了 索引目录
2025-12-19
1
导读:关注【索引目录】服务号,更多精彩内容等你来探索!你知道最搞笑的是什么吗?刚从编程训练营毕业的学员写的代码极其简单。

关注【索引目录】服务号,更多精彩内容等你来探索!

你知道最搞笑的是什么吗?刚从编程训练营毕业的学员写的代码极其简单。六个月后,在接触到设计模式之后,他们写的代码却复杂到需要博士才能理解。开发者的成长历程基本上就是:“等等,我可以用类?” → “所有东西都必须是工厂模式、策略模式、观察者模式和单例模式。”

让我来告诉你我接手一个代码库时的情景,当时有人“设计”了用户全名的显示方式。

目录

  • 战争罪行
  • 危险信号一: “面向未来”的谬误
  • 危险信号二:只有一个实现的接口
  • 危险信号之三:没人需要的通用解决方案
  • 危险信号 4:抽象稳定代码,耦合不稳定代码
  • 危险信号之五:“企业”思维模式
  • 危险信号之六:过早抽象化
  • 抽象何时才能真正有意义
    • 1. 将会发生变化的外部 API
    • 2. 多种实际实施方案
    • 3. 测试接缝
    • 4. 插件系统


  • 核对清单:你应该把它抽象化吗?
  • 恢复:删除不良抽象
  • 关于“可扩展”代码的真相
  • 哲学
  • 结论

战争罪行

// user-name-display-strategy.interface.ts
export interface IUserNameDisplayStrategy {
  formatName(context: UserNameContext): string;
  supports(type: DisplayType): boolean;
}

// user-name-context.interface.ts
export interface UserNameContext {
  firstName: string;
  lastName: string;
  locale: string;
  preferences: UserDisplayPreferences;
  culturalNamingConvention: CulturalNamingConvention;
  titlePrefix?: string;
  suffixes?: string[];
}

// user-name-display-strategy.factory.ts
@Injectable()
export class UserNameDisplayStrategyFactory {
  constructor(
    @Inject("DISPLAY_STRATEGIES")
    private readonly strategies: IUserNameDisplayStrategy[]
  ) {}

  create(type: DisplayType): IUserNameDisplayStrategy {
    const strategy = this.strategies.find((s) => s.supports(type));
    if (!strategy) {
      throw new UnsupportedDisplayTypeException(type);
    }
    return strategy;
  }
}

// standard-user-name-display.strategy.ts
@Injectable()
export class StandardUserNameDisplayStrategy
  implements IUserNameDisplayStrategy
{
  supports(type: DisplayType): boolean {
    return type === DisplayType.STANDARD;
  }

  formatName(context: UserNameContext): string {
    return `${context.firstName} ${context.lastName}`;
  }
}

// The module that ties this beautiful architecture together
@Module({
  providers: [
    UserNameDisplayStrategyFactory,
    StandardUserNameDisplayStrategy,
    FormalUserNameDisplayStrategy,
    InformalUserNameDisplayStrategy,
    {
      provide: "DISPLAY_STRATEGIES",
      useFactory: (...strategies) => strategies,
      inject: [
        StandardUserNameDisplayStrategy,
        FormalUserNameDisplayStrategy,
        InformalUserNameDisplayStrategy,
      ],
    },
  ],
  exports: [UserNameDisplayStrategyFactory],
})
export class UserNameDisplayModule {}

// Usage (deep breath):
const context: UserNameContext = {
  firstName: user.firstName,
  lastName: user.lastName,
  locale: "en-US",
  preferences: userPreferences,
  culturalNamingConvention: CulturalNamingConvention.WESTERN,
};

const strategy = this.strategyFactory.create(DisplayType.STANDARD);
const displayName = strategy.formatName(context);

它的实际作用是:

`${user.firstName} ${user.lastName}`;

我可没开玩笑。用两百多行“架构”代码来连接两个字符串并加个空格。写这段代码的开发者估计后腰上都纹着“四人帮”的《设计模式》了。

危险信号一: “面向未来”的谬误

让我告诉你一个秘密:你无法预测未来,而且你在这方面做得非常糟糕。

// "We might need multiple payment providers someday!"
export interface IPaymentGateway {
  processPayment(request: PaymentRequest): Promise<PaymentResult>;
  refund(transactionId: string): Promise<RefundResult>;
  validateCard(card: CardDetails): Promise<boolean>;
}

export interface IPaymentGatewayFactory {
  create(provider: PaymentProvider): IPaymentGateway;
}

@Injectable()
export class StripePaymentGateway implements IPaymentGateway {
  // The only implementation for the past 3 years
  // Will probably be the only one for the next 3 years
  // But hey, we're "ready" for PayPal!
}

@Injectable()
export class PaymentGatewayFactory implements IPaymentGatewayFactory {
  create(provider: PaymentProvider): IPaymentGateway {
    switch (provider) {
      case PaymentProvider.STRIPE:
        return new StripePaymentGateway();
      default:
        throw new Error("Unsupported payment provider");
    }
  }
}

三年后,当你终于添加 PayPal 时:

  • 您的要求已完全改变
  • Stripe 的 API 已经发展演变
  • 这种抽象方法不适用于新的用例。
  • 反正你都要重构所有内容。

你应该这样写:

@Injectable()
export class PaymentService {
  constructor(private stripe: Stripe) {}

  async charge(amount: number, token: string): Promise<string> {
    const charge = await this.stripe.charges.create({
      amount,
      currency: "usd",
      source: token,
    });
    return charge.id;
  }
}

搞定。等 PayPal 上线(如果它真的上线的话),你再根据实际需求重构代码,而不是根据你凌晨两点胡思乱想出来的假设需求。

危险信号二:只有一个实现的接口

这是我最喜欢的。这就像去沙漠“以防万一”带把伞一样。

export interface IUserService {
  findById(id: string): Promise<User>;
  create(dto: CreateUserDto): Promise<User>;
  update(id: string, dto: UpdateUserDto): Promise<User>;
}

@Injectable()
export class UserService implements IUserService {
  // The one and only implementation
  // Will be the one and only implementation until the heat death of the universe

  async findById(id: string): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }
}

恭喜,您已取得以下成就:

  • ✅ 让你的 IDE 跳转到定义页面只需点击两次而不是一次
  • ✅ 像2005年那样,在类名后添加了后缀“Impl”。
  • ✅ 造成了困惑:“等等,为什么会有界面?”
  • ✅ 增加了未来重构的难度(现在需要更新两处内容)
  • ✅ 零实际收益

直接写服务代码就行了:

@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async findById(id: string): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }
}

“但是测试怎么办?” 兄弟,TypeScript 有jest.mock()测试功能。你不需要接口来模拟对象。

接口何时有用:

// YES: Multiple implementations you're ACTUALLY using
export interface NotificationChannel {
  send(notification: Notification): Promise<void>;
}

@Injectable()
export class EmailChannel implements NotificationChannel {
  // Actually used in production
}

@Injectable()
export class SlackChannel implements NotificationChannel {
  // Also actually used in production
}

@Injectable()
export class SmsChannel implements NotificationChannel {
  // You guessed it - actually used!
}

关键在于“实际”。不是“可能”,不是“可以”,也不是“面向未来”。而是“实际”。现在。正在生产中。

危险信号之三:没人需要的通用解决方案

// "This will save SO much time!"
export abstract class BaseService<T, ID = string> {
  constructor(protected repository: Repository<T>) {}

  async findById(id: ID): Promise<T> {
    const entity = await this.repository.findOne({ where: { id } });
    if (!entity) {
      throw new NotFoundException(`${this.getEntityName()} not found`);
    }
    return entity;
  }

  async findAll(query?: QueryParams): Promise<T[]> {
    return this.repository.find(this.buildQuery(query));
  }

  async create(dto: DeepPartial<T>): Promise<T> {
    this.validate(dto);
    return this.repository.save(dto);
  }

  async update(id: ID, dto: DeepPartial<T>): Promise<T> {
    const entity = await this.findById(id);
    this.validate(dto);
    return this.repository.save({ ...entity, ...dto });
  }

  async delete(id: ID): Promise<void> {
    await this.repository.delete(id);
  }

  protected abstract getEntityName(): string;
  protected abstract validate(dto: DeepPartial<T>): void;
  protected buildQuery(query?: QueryParams): any {
    // 50 lines of "reusable" query building logic
  }
}

@Injectable()
export class UserService extends BaseService<User> {
  constructor(userRepository: UserRepository) {
    super(userRepository);
  }

  protected getEntityName(): string {
    return "User";
  }

  protected validate(dto: DeepPartial<User>): void {
    // Wait, users need special validation
    if (!dto.email?.includes("@")) {
      throw new BadRequestException("Invalid email");
    }
    // And password hashing
    // And email verification
    // And... this doesn't fit the pattern anymore
  }

  // Now you need to override half the base methods
  async create(dto: CreateUserDto): Promise<User> {
    // Can't use super.create() because users are special
    // So you rewrite it here
    // Defeating the entire purpose of the base class
  }
}

剧情反转:每个实体最终都变成了“特殊实体”,你不得不重写所有属性。基类变成了一座浪费时间的500行纪念碑。

你本应该这样做:

@Injectable()
export class UserService {
  constructor(
    private userRepository: UserRepository,
    private passwordService: PasswordService
  ) {}

  async create(dto: CreateUserDto): Promise<User> {
    if (await this.emailExists(dto.email)) {
      throw new ConflictException("Email already exists");
    }

    const hashedPassword = await this.passwordService.hash(dto.password);
    return this.userRepository.save({
      ...dto,
      password: hashedPassword,
    });
  }

  // Just the methods users actually need
}

枯燥乏味?是的。易读吗?也是。易于维护吗?绝对易于维护。

危险信号 4:抽象稳定代码,耦合不稳定代码

这是我个人最喜欢的错误,因为它完全颠倒了事实。

// Developer: "Let me abstract this calculation!"
export interface IDiscountCalculator {
  calculate(context: DiscountContext): number;
}

@Injectable()
export class PercentageDiscountCalculator implements IDiscountCalculator {
  calculate(context: DiscountContext): number {
    return context.price * (context.percentage / 100);
  }
}

@Injectable()
export class FixedDiscountCalculator implements IDiscountCalculator {
  calculate(context: DiscountContext): number {
    return context.price - context.fixedAmount;
  }
}

// Factory, strategy pattern, the whole nine yards
// For... basic math that hasn't changed since ancient Babylon

同时,在同一代码库中:

@Injectable()
export class OrderService {
  async processPayment(order: Order): Promise<void> {
    // Hardcoded Stripe API call
    const charge = await fetch("https://api.stripe.com/v1/charges", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.STRIPE_KEY}`,
      },
      body: JSON.stringify({
        amount: order.total,
        currency: "usd",
        source: order.paymentToken,
      }),
    });

    // Parsing Stripe's specific response format
    const result = await charge.json();
    order.stripeChargeId = result.id;
  }
}

让我捋一捋:

  • 基本算术运算(永不改变):高度抽象 ✅
  • 外部 API 调用(不断变化):紧密耦合 ✅
  • 职业选择:值得商榷 ✅

反其道而行之:

// Math is math, keep it simple
export class DiscountCalculator {
  calculatePercentage(price: number, percent: number): number {
    return price * (percent / 100);
  }

  calculateFixed(price: number, amount: number): number {
    return Math.max(0, price - amount);
  }
}

// External dependencies need abstraction
export interface PaymentProcessor {
  charge(amount: number, token: string): Promise<PaymentResult>;
}

@Injectable()
export class StripeProcessor implements PaymentProcessor {
  async charge(amount: number, token: string): Promise<PaymentResult> {
    // Stripe-specific stuff isolated here
  }
}

原则:抽象出变化的事物,不要抽象出稳定的事物。

危险信号之五:“企业”思维模式

我曾经见过一段代码,保存用户的偏好设置竟然需要十一个文件。而且这还不是什么复杂的偏好设置,只是简单的深色模式开关而已。

// preference-persistence-strategy.interface.ts
export interface IPreferencePersistenceStrategy {
  persist(context: PreferencePersistenceContext): Promise<void>;
}

// preference-persistence-context-builder.interface.ts
export interface IPreferencePersistenceContextBuilder {
  build(params: PreferencePersistenceParameters): PreferencePersistenceContext;
}

// preference-persistence-orchestrator.service.ts
@Injectable()
export class PreferencePersistenceOrchestrator {
  constructor(
    private contextBuilder: IPreferencePersistenceContextBuilder,
    private strategyFactory: IPreferencePersistenceStrategyFactory,
    private validator: IPreferencePersistenceValidator
  ) {}

  async orchestrate(params: PreferencePersistenceParameters): Promise<void> {
    const context = await this.contextBuilder.build(params);
    const validationResult = await this.validator.validate(context);

    if (!validationResult.isValid) {
      throw new ValidationException(validationResult.errors);
    }

    const strategy = this.strategyFactory.create(context.persistenceType);
    await strategy.persist(context);
  }
}

它的作用是:

await this.userRepository.update(userId, { darkMode: true });

我确信写这篇文章的人是按行拿稿费的。

症结在于:阅读了太多“企业架构”书籍,并认为文件越多=代码越好。

解决方法:问问自己,“我是在解决一个真正的问题,还是在玩软件工程师角色扮演游戏?”

危险信号之六:过早抽象化

三法则(但人人都忽略它):

  1. 写下来
  2. 再写一遍
  3. 看出规律了吗?现在把它抽象化。

实际情况是:

  1. 写一次
  2. “我可能还会用到这个,让我先提取一下!”
  3. 创建一个框架
  4. 第二个用例完全不同。
  5. 与抽象概念抗争六个月
  6. 重写所有内容
// First API endpoint
@Controller("users")
export class UserController {
  @Get(":id")
  async getUser(@Param("id") id: string) {
    return this.userService.findById(id);
  }
}

// Developer brain: "I should make a base controller for all resources!"

@Controller()
export abstract class BaseResourceController<T, CreateDto, UpdateDto> {
  constructor(protected service: BaseService<T>) {}

  @Get(":id")
  async get(@Param("id") id: string): Promise<T> {
    return this.service.findById(id);
  }

  @Post()
  async create(@Body() dto: CreateDto): Promise<T> {
    return this.service.create(dto);
  }

  @Put(":id")
  async update(@Param("id") id: string, @Body() dto: UpdateDto): Promise<T> {
    return this.service.update(id, dto);
  }

  @Delete(":id")
  async delete(@Param("id") id: string): Promise<void> {
    return this.service.delete(id);
  }
}

// Now every controller that doesn't fit this pattern is a special case
// Users need password reset endpoint
// Products need image upload
// Orders need status transitions
// Everything is fighting the abstraction

明智之举:

// Write the first one
@Controller("users")
export class UserController {
  // Full implementation
}

// Write the second one
@Controller("products")
export class ProductController {
  // Copy-paste, modify as needed
}

// On the third one, IF there's a clear pattern:
// Extract only the truly common parts

智慧:重复劳动比错误的抽象更划算。以后总有机会避免代码重复。过早的抽象就像过早的优化——它是万恶之源,但拿它开玩笑就没那么有趣了。

抽象何时才能真正有意义

听着,我并非反对抽象,我反对的是愚蠢的抽象。以下情况才是真正明智的:

1. 将会发生变化的外部 API

// You're literally switching from Stripe to PayPal next quarter
export interface PaymentProvider {
  charge(amount: number): Promise<string>;
}

// This abstraction will save your ass

2. 多种实际实施方案

// You have all of these in production RIGHT NOW
export interface StorageProvider {
  upload(file: Buffer): Promise<string>;
}

@Injectable()
export class S3Storage implements StorageProvider {
  // Used for production files
}

@Injectable()
export class LocalStorage implements StorageProvider {
  // Used in development
}

@Injectable()
export class CloudinaryStorage implements StorageProvider {
  // Used for images
}

3. 测试接缝

// Makes mocking way easier
export interface TimeProvider {
  now(): Date;
}

// Test with frozen time, run in prod with real time

4. 插件系统

// Designed for third-party extensions
export interface WebhookHandler {
  handle(payload: unknown): Promise<void>;
  supports(event: string): boolean;
}

// Developers can add Slack, Discord, custom handlers

核对清单:你应该把它抽象化吗?

在创建抽象概念之前,先问问自己:

🚨 如果对以下问题回答“否”,请停止:

  • 我现在有2个以上实际的使用案例吗?
  • 这是否能隔离出经常变化的事物?
  • 新来的开发者能理解为什么会有这个功能吗?
  • 这能解决我今天遇到的实际问题吗?

🛑 如果以下情况属实,请务必立即停止:

  • “我们或许有一天会需要它。”
  • “这样更专业。”
  • 我读到过这种模式
  • “它更具可扩展性”
  • 企业应用程序就是这样做的

✅ 绿灯亮起,如果:

  • 目前已有多种实现方式
  • 外部依赖项实际上正在发生变化
  • 大大简化了测试过程
  • 消除大量重复工作

恢复:删除不良抽象

最勇敢的事就是删除代码,尤其是“架构”部分。

前:

// 6 files, 300 lines
export interface IUserValidator {}
export class UserValidationStrategy {}
export class UserValidationFactory {}
export class UserValidationOrchestrator {}
// ...

后:

// 1 file, 20 lines
@Injectable()
export class UserService {
  async create(dto: CreateUserDto): Promise<User> {
    if (!dto.email.includes("@")) {
      throw new BadRequestException("Invalid email");
    }
    return this.userRepository.save(dto);
  }
}

你的团队: “这好多了!”
你的自尊心: “但是……我的架构……”
未来的你: “谢天谢地我把它删掉了。”

关于“可扩展”代码的真相

这里有个秘密:简单的代码比“可扩展”的代码扩展性更好。

Netflix 并没有采用你提到的 BaseAbstractFactoryStrategyManagerProvider 模式。他们使用的是枯燥但直接的代码,解决的是实际问题。

我见过的最具“可扩展性”的代码:

  • 易于阅读
  • 职责明确
  • 谨慎使用抽象概念
  • 新开发人员几分钟内即可理解

最难扩展的代码:

  • 需要博士学位才能理解
  • 有47层间接性
  • 无处不在的“企业模式”。
  • 做出一些简单的改变也需要几周时间。

哲学

新手:复制粘贴一切;
中级:抽象一切;
专家:知道何时不复制粘贴,何时抽象。

目标不是编写简洁的代码或构建可扩展的架构,而是以最小的可行复杂度解决问题。

你的工作不是用你对设计模式的了解来炫耀,而是编写能够做到以下几点的代码:

  • 作品
  • 容易理解
  • 可以轻松更改
  • 不会让人想要辞职。

结论

下次当你准备创建一个只有一个实现的接口,或者为两种用例构建一个工厂,或者“以防万一”创建一个基类时,我希望你停下来问问自己:

我是在解决问题还是在制造问题?

大多数抽象概念的产生是因为:

  • 我们在一本书里读到过它们。
  • 他们看起来“更专业”。
  • 我们感到无聊,想要挑战。
  • 我们害怕显得不够成熟。

但关键在于:最复杂的代码是不存在的代码。

写枯燥乏味的代码。如果复制粘贴比抽象更简单,那就复制粘贴。等待第三个使用场景。删除过于激进的抽象。

未来的你、你的同事以及任何需要维护你代码的人都会感谢你。

关注【索引目录】服务号,更多精彩内容等你来探索!


【声明】内容源于网络
0
0
索引目录
索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
内容 444
粉丝 0
索引目录 索引目录是一家专注于医疗、技术开发、物联网应用等领域的创新型公司。我们致力于为客户提供高质量的服务和解决方案,推动技术与行业发展。
总阅读12
粉丝0
内容444