你知道最搞笑的是什麼嗎?剛從程式設計訓練營畢業的學員寫的程式碼極為簡單。六個月後,接觸到設計模式之後,他們寫的程式碼卻複雜到需要博士才能理解。開發者的成長歷程基本上就是:“等等,我可以用類?” → “所有東西都必須是工廠模式、策略模式、觀察者模式和單例模式。”
讓我來告訴你我接手一個程式碼庫時的情景,當時有人「設計」了使用者全名的顯示方式。
// 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
}
枯燥乏味?是的。易讀嗎?也是。易於維護嗎?絕對易於維護。
這是我個人最喜歡的錯誤,因為它完全顛倒了事實。
// 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 });
我確信寫這篇文章的人是按行拿稿費的。
癥結在於:閱讀了太多「企業架構」書籍,並認為文件越多=程式碼越好。
解決方法:問問自己,“我是在解決一個真正的問題,還是在玩軟體工程師角色扮演遊戲?”
三的法則(但每個人都忽略了它):
寫下來
再寫一遍
看規律了嗎?現在把它抽象化。
實際情況是:
寫一次
“我可能還會用到這個,讓我先提取一下!”
建立一個框架
第二個用例完全不同。
與抽象概念抗爭六個月
重寫所有內容
// 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
智慧:重複勞動比錯誤的抽象更划算。以後總有機會避免程式碼重複。過早的抽象就像過早的優化——它是萬惡之源,但拿它開玩笑就沒那麼有趣了。
聽著,我並非反對抽象,我反對的是愚蠢的抽象。以下情況才是真正明智的:
// You're literally switching from Stripe to PayPal next quarter
export interface PaymentProvider {
charge(amount: number): Promise<string>;
}
// This abstraction will save your ass
// 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
}
// Makes mocking way easier
export interface TimeProvider {
now(): Date;
}
// Test with frozen time, run in prod with real time
// 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