mcpField
S
service-layer-extractor

service-layer-extractor

github.com/patricio0312rev开发63243 次安装41 周前更新
代码仓库

关于 service-layer-extractor

帮助开发者将业务逻辑从控制器迁移到服务层,以改善应用结构和可测试性。内容包括依赖注入、服务测试、目录组织和分阶段迁移方案。

主要功能

  1. 01将逻辑提取到服务类
  2. 02分离 HTTP 与业务职责
  3. 03配置依赖注入
  4. 04独立测试服务
  5. 05规划分阶段迁移

适用场景

  1. 01重构混有业务逻辑的控制器
  2. 02脱离 HTTP 测试应用逻辑
  3. 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