关于 service-layer-extractor
帮助开发者将业务逻辑从控制器迁移到服务层,以改善应用结构和可测试性。内容包括依赖注入、服务测试、目录组织和分阶段迁移方案。
主要功能
- 01将逻辑提取到服务类
- 02分离 HTTP 与业务职责
- 03配置依赖注入
- 04独立测试服务
- 05规划分阶段迁移
适用场景
- 01重构混有业务逻辑的控制器
- 02脱离 HTTP 测试应用逻辑
- 03在多个端点复用服务
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ patricio0312rev/ skills --skill service-layer-extractor安装后在 Claude Code 中输入/service-layer-extractor确认已加载。
详细介绍
服务层提取器
将业务逻辑从控制器中提取出来,移至可测试的服务层。
架构分层
routes/ → Define endpoints, parse requests
controllers/ → Validate input, call services, format responses
services/ → Business logic, orchestration
repositories/ → Database queries
models/ → Data structures
改造前:臃肿的控制器
// ❌ 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 });
}
});
改造后:服务层
// ✅ 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;
}
}
依赖注入
// 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);
测试服务
// 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: "[email protected]",
});
const user = await service.createUser({
email: "[email protected]",
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: "[email protected]",
password: "pass",
name: "Test",
})
).rejects.toThrow(ConflictError);
});
});
文件夹结构
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.ts
迁移策略
## Phase 1: Create Service Layer (Week 1-2)
- [ ] Create service classes
- [ ] Move business logic to services
- [ ] Keep controllers thin
- [ ] No breaking changes
## Phase 2: Add Tests (Week 3-4)
- [ ] Write service unit tests
- [ ] Mock dependencies
- [ ] Achieve 80%+ coverage
## Phase 3: Extract Repositories (Week 5-6)
- [ ] Create repository layer
- [ ] Move DB queries from services
- [ ] Services depend on repositories
## Phase 4: Dependency Injection (Week 7-8)
- [ ] Set up DI container
- [ ] Remove manual instantiation
- [ ] Wire up dependencies
优势
- 可测试性:无需 HTTP 即可测试服务
- 可复用性:可在多个端点复用逻辑
- 分离性:各层之间界限清晰
- 可维护性:更容易查找和修改逻辑
输出检查清单
- 已创建服务类
- 已从控制器中提取业务逻辑
- 已为数据访问创建存储库层
- 已设置依赖注入
- 已为服务编写单元测试
- 已重新组织文件夹结构
- 已记录迁移计划
- 已对团队进行新模式培训
评论
支持 Markdown