🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

你知道最搞笑的是什麼嗎?剛從程式設計訓練營畢業的學員寫的程式碼極為簡單。六個月後,接觸到設計模式之後,他們寫的程式碼卻複雜到需要博士才能理解。開發者的成長歷程基本上就是:“等等,我可以用類?” → “所有東西都必須是工廠模式、策略模式、觀察者模式和單例模式。”

讓我來告訴你我接手一個程式碼庫時的情景,當時有人「設計」了使用者全名的顯示方式。

目錄

戰爭罪行

// 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層間接性

  • 無所不在的「企業模式」。

  • 做出一些簡單的改變也需要幾週。

哲學

新手:全部複製貼上

中間體:抽像一切

專家:要知道何時既不做也不做

目標不是編寫簡潔的程式碼或建立可擴展的架構,而是以最小的可行複雜度解決問題。

你的工作不是用你對設計模式的了解來炫耀,而是寫出能夠做到以下幾點的程式碼:

  • 作品

  • 容易理解

  • 可以輕鬆更改

  • 不會讓人想要辭職。

結論

下次當你準備建立一個只有一個實現的接口,或者為兩個用例建置一個工廠,或者“以防萬一”建立一個基類時,我希望你停下來問問自己:

我是在解決問題還是在製造問題?

大多數抽象概念的產生是因為:

  • 我們在一本書裡讀到它們。

  • 他們看起來「更專業」。

  • 我們感到無聊,想要挑戰。

  • 我們害怕顯得不夠成熟。

但關鍵在於:最複雜的程式碼是不存在的程式碼。

寫出枯燥乏味的程式碼。如果複製貼上比抽象更簡單,那就複製貼上。等待第三個使用場景。刪除過於激進的抽象。

未來的你、你的同事以及任何需要維護你程式碼的人都會感謝你。

現在去刪除一些介面。


PS:如果你是寫使用者名稱顯示策略工廠的人,我很抱歉。但同時,也請尋求協助。

建築設計就是債務,要明智地使用。大多數系統並不需要抵押貸款。


原文出處:https://dev.to/adamthedeveloper/youre-not-building-netflix-stop-coding-like-you-are-1707


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝15   💬10   ❤️5
409
🥈
我愛JS
📝2   💬8   ❤️4
94
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付