service-layer-extractor
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseService Layer Extractor
服务层提取器
Extract business logic from controllers into a testable service layer.
将业务逻辑从控制器中提取到可测试的服务层。
Architecture Layers
架构分层
routes/ → Define endpoints, parse requests
controllers/ → Validate input, call services, format responses
services/ → Business logic, orchestration
repositories/ → Database queries
models/ → Data structuresroutes/ → 定义端点,解析请求
controllers/ → 验证输入,调用服务,格式化响应
services/ → 业务逻辑,流程编排
repositories/ → 数据库查询
models/ → 数据结构Before: Fat Controller
重构前:臃肿控制器
typescript
// ❌ Business logic mixed with HTTP concerns
router.post("/users", async (req, res) => {
try {
// Validation
if (!req.body.email) {
return res.status(400).json({ error: "Email required" });
}
// Check duplicate
const existing = await db.query("SELECT * FROM users WHERE email = $1", [
req.body.email,
]);
if (existing.rows.length > 0) {
return res.status(409).json({ error: "Email already exists" });
}
// Hash password
const hashedPassword = await bcrypt.hash(req.body.password, 10);
// Create user
const result = await db.query(
"INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *",
[req.body.email, hashedPassword, req.body.name]
);
// Send welcome email
await sendEmail(req.body.email, "Welcome!", "Thanks for joining");
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});typescript
// ❌ 业务逻辑与HTTP相关代码混合
router.post("/users", async (req, res) => {
try {
// 验证
if (!req.body.email) {
return res.status(400).json({ error: "Email required" });
}
// 检查重复
const existing = await db.query("SELECT * FROM users WHERE email = $1", [
req.body.email,
]);
if (existing.rows.length > 0) {
return res.status(409).json({ error: "Email already exists" });
}
// 密码哈希
const hashedPassword = await bcrypt.hash(req.body.password, 10);
// 创建用户
const result = await db.query(
"INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *",
[req.body.email, hashedPassword, req.body.name]
);
// 发送欢迎邮件
await sendEmail(req.body.email, "Welcome!", "Thanks for joining");
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});After: Service Layer
重构后:服务层
typescript
// ✅ Separated concerns
// services/user.service.ts
export class UserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async createUser(dto: CreateUserDto): Promise<User> {
// Business logic only
const existing = await this.userRepository.findByEmail(dto.email);
if (existing) {
throw new ConflictError("Email already exists");
}
const hashedPassword = await bcrypt.hash(dto.password, 10);
const user = await this.userRepository.create({
...dto,
password: hashedPassword,
});
await this.emailService.sendWelcome(user.email);
return user;
}
}
// controllers/user.controller.ts
export class UserController {
constructor(private userService: UserService) {}
create = asyncHandler(async (req, res) => {
const user = await this.userService.createUser(req.body);
res.status(201).json({ success: true, data: user });
});
}
// repositories/user.repository.ts
export class UserRepository {
async create(data: CreateUserData): Promise<User> {
const result = await db.query(
"INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *",
[data.email, data.password, data.name]
);
return result.rows[0];
}
async findByEmail(email: string): Promise<User | null> {
const result = await db.query("SELECT * FROM users WHERE email = $1", [
email,
]);
return result.rows[0] || null;
}
}typescript
// ✅ 关注点分离
// services/user.service.ts
export class UserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async createUser(dto: CreateUserDto): Promise<User> {
// 仅包含业务逻辑
const existing = await this.userRepository.findByEmail(dto.email);
if (existing) {
throw new ConflictError("Email already exists");
}
const hashedPassword = await bcrypt.hash(dto.password, 10);
const user = await this.userRepository.create({
...dto,
password: hashedPassword,
});
await this.emailService.sendWelcome(user.email);
return user;
}
}
// controllers/user.controller.ts
export class UserController {
constructor(private userService: UserService) {}
create = asyncHandler(async (req, res) => {
const user = await this.userService.createUser(req.body);
res.status(201).json({ success: true, data: user });
});
}
// repositories/user.repository.ts
export class UserRepository {
async create(data: CreateUserData): Promise<User> {
const result = await db.query(
"INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *",
[data.email, data.password, data.name]
);
return result.rows[0];
}
async findByEmail(email: string): Promise<User | null> {
const result = await db.query("SELECT * FROM users WHERE email = $1", [
email,
]);
return result.rows[0] || null;
}
}Dependency Injection
Dependency Injection
typescript
// container.ts (using tsyringe or manual)
import { UserService } from "./services/user.service";
import { UserRepository } from "./repositories/user.repository";
import { EmailService } from "./services/email.service";
export class Container {
private static instances = new Map();
static get<T>(constructor: new (...args: any[]) => T): T {
if (!this.instances.has(constructor)) {
// Create dependencies
const deps = this.resolveDependencies(constructor);
this.instances.set(constructor, new constructor(...deps));
}
return this.instances.get(constructor);
}
private static resolveDependencies(constructor: any): any[] {
// Resolve constructor dependencies
return [];
}
}
// Usage
const userService = Container.get(UserService);typescript
// container.ts (使用tsyringe或手动实现)
import { UserService } from "./services/user.service";
import { UserRepository } from "./repositories/user.repository";
import { EmailService } from "./services/email.service";
export class Container {
private static instances = new Map();
static get<T>(constructor: new (...args: any[]) => T): T {
if (!this.instances.has(constructor)) {
// 创建依赖项
const deps = this.resolveDependencies(constructor);
this.instances.set(constructor, new constructor(...deps));
}
return this.instances.get(constructor);
}
private static resolveDependencies(constructor: any): any[] {
// 解析构造函数依赖
return [];
}
}
// 使用示例
const userService = Container.get(UserService);Testing Services
服务测试
typescript
// user.service.test.ts
describe("UserService", () => {
let service: UserService;
let mockRepository: jest.Mocked<UserRepository>;
let mockEmailService: jest.Mocked<EmailService>;
beforeEach(() => {
mockRepository = {
create: jest.fn(),
findByEmail: jest.fn(),
} as any;
mockEmailService = {
sendWelcome: jest.fn(),
} as any;
service = new UserService(mockRepository, mockEmailService);
});
it("creates user successfully", async () => {
mockRepository.findByEmail.mockResolvedValue(null);
mockRepository.create.mockResolvedValue({
id: "1",
email: "test@example.com",
});
const user = await service.createUser({
email: "test@example.com",
password: "password123",
name: "Test User",
});
expect(user.id).toBe("1");
expect(mockEmailService.sendWelcome).toHaveBeenCalled();
});
it("throws error if email exists", async () => {
mockRepository.findByEmail.mockResolvedValue({ id: "1" } as User);
await expect(
service.createUser({
email: "existing@example.com",
password: "pass",
name: "Test",
})
).rejects.toThrow(ConflictError);
});
});typescript
// user.service.test.ts
describe("UserService", () => {
let service: UserService;
let mockRepository: jest.Mocked<UserRepository>;
let mockEmailService: jest.Mocked<EmailService>;
beforeEach(() => {
mockRepository = {
create: jest.fn(),
findByEmail: jest.fn(),
} as any;
mockEmailService = {
sendWelcome: jest.fn(),
} as any;
service = new UserService(mockRepository, mockEmailService);
});
it("creates user successfully", async () => {
mockRepository.findByEmail.mockResolvedValue(null);
mockRepository.create.mockResolvedValue({
id: "1",
email: "test@example.com",
});
const user = await service.createUser({
email: "test@example.com",
password: "password123",
name: "Test User",
});
expect(user.id).toBe("1");
expect(mockEmailService.sendWelcome).toHaveBeenCalled();
});
it("throws error if email exists", async () => {
mockRepository.findByEmail.mockResolvedValue({ id: "1" } as User);
await expect(
service.createUser({
email: "existing@example.com",
password: "pass",
name: "Test",
})
).rejects.toThrow(ConflictError);
});
});Folder Structure
文件夹结构
src/
├── routes/
│ └── users.routes.ts
├── controllers/
│ └── user.controller.ts
├── services/
│ ├── user.service.ts
│ ├── email.service.ts
│ └── payment.service.ts
├── repositories/
│ └── user.repository.ts
├── models/
│ └── user.model.ts
├── types/
│ └── user.types.ts
└── middleware/
└── validate.tssrc/
├── routes/
│ └── users.routes.ts
├── controllers/
│ └── user.controller.ts
├── services/
│ ├── user.service.ts
│ ├── email.service.ts
│ └── payment.service.ts
├── repositories/
│ └── user.repository.ts
├── models/
│ └── user.model.ts
├── types/
│ └── user.types.ts
└── middleware/
└── validate.tsMigration Strategy
迁移策略
markdown
undefinedmarkdown
undefinedPhase 1: Create Service Layer (Week 1-2)
阶段1:创建服务层(第1-2周)
- Create service classes
- Move business logic to services
- Keep controllers thin
- No breaking changes
- 创建服务类
- 将业务逻辑迁移至服务层
- 简化控制器代码
- 不引入破坏性变更
Phase 2: Add Tests (Week 3-4)
阶段2:添加测试(第3-4周)
- Write service unit tests
- Mock dependencies
- Achieve 80%+ coverage
- 编写服务单元测试
- 模拟依赖项
- 达到80%+的测试覆盖率
Phase 3: Extract Repositories (Week 5-6)
阶段3:提取仓储层(第5-6周)
- Create repository layer
- Move DB queries from services
- Services depend on repositories
- 创建仓储层
- 将数据库查询从服务层迁移至仓储层
- 服务层依赖仓储层
Phase 4: Dependency Injection (Week 7-8)
阶段4:依赖注入(第7-8周)
- Set up DI container
- Remove manual instantiation
- Wire up dependencies
undefined- 搭建DI容器
- 移除手动实例化
- 配置依赖项
undefinedBenefits
优势
- Testability: Services testable without HTTP
- Reusability: Logic reused across endpoints
- Separation: Clear boundaries between layers
- Maintainability: Easier to locate and modify logic
- 可测试性:无需HTTP环境即可测试服务
- 可复用性:逻辑可在多个端点间复用
- 关注点分离:各层边界清晰
- 可维护性:更易定位和修改逻辑
Output Checklist
输出检查清单
- Service classes created
- Business logic extracted from controllers
- Repository layer for data access
- Dependency injection setup
- Unit tests for services
- Folder structure reorganized
- Migration plan documented
- Team trained on new patterns
- 已创建服务类
- 已从控制器中提取业务逻辑
- 已实现数据访问的仓储层
- 已搭建依赖注入环境
- 已编写服务单元测试
- 已重新组织文件夹结构
- 已记录迁移计划
- 已完成团队新模式培训