关注【索引目录】服务号,更多精彩内容等你来探索!
你知道最搞笑的是什么吗?刚从编程训练营毕业的学员写的代码极其简单。六个月后,在接触到设计模式之后,他们写的代码却复杂到需要博士才能理解。开发者的成长历程基本上就是:“等等,我可以用类?” → “所有东西都必须是工厂模式、策略模式、观察者模式和单例模式。”
让我来告诉你我接手一个代码库时的情景,当时有人“设计”了用户全名的显示方式。
目录
- 战争罪行
- 危险信号一: “面向未来”的谬误
- 危险信号二:只有一个实现的接口
- 危险信号之三:没人需要的通用解决方案
- 危险信号 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 });
我确信写这篇文章的人是按行拿稿费的。
症结在于:阅读了太多“企业架构”书籍,并认为文件越多=代码越好。
解决方法:问问自己,“我是在解决一个真正的问题,还是在玩软件工程师角色扮演游戏?”
危险信号之六:过早抽象化
三法则(但人人都忽略它):
-
写下来 -
再写一遍 -
看出规律了吗?现在把它抽象化。
实际情况是:
-
写一次 -
“我可能还会用到这个,让我先提取一下!” -
创建一个框架 -
第二个用例完全不同。 -
与抽象概念抗争六个月 -
重写所有内容
// 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层间接性 -
无处不在的“企业模式”。 -
做出一些简单的改变也需要几周时间。
哲学
新手:复制粘贴一切;
中级:抽象一切;
专家:知道何时不复制粘贴,何时抽象。
目标不是编写简洁的代码或构建可扩展的架构,而是以最小的可行复杂度解决问题。
你的工作不是用你对设计模式的了解来炫耀,而是编写能够做到以下几点的代码:
-
作品 -
容易理解 -
可以轻松更改 -
不会让人想要辞职。
结论
下次当你准备创建一个只有一个实现的接口,或者为两种用例构建一个工厂,或者“以防万一”创建一个基类时,我希望你停下来问问自己:
我是在解决问题还是在制造问题?
大多数抽象概念的产生是因为:
-
我们在一本书里读到过它们。 -
他们看起来“更专业”。 -
我们感到无聊,想要挑战。 -
我们害怕显得不够成熟。
但关键在于:最复杂的代码是不存在的代码。
写枯燥乏味的代码。如果复制粘贴比抽象更简单,那就复制粘贴。等待第三个使用场景。删除过于激进的抽象。
未来的你、你的同事以及任何需要维护你代码的人都会感谢你。
关注【索引目录】服务号,更多精彩内容等你来探索!

