Pattern Repository : Approche Moderne
Repenser le pattern repository pour les applications contemporaines. Quand l'utiliser, comment l'implémenter efficacement, et les alternatives à considérer à l'ère des ORMs et architectures cloud-native.
L'Architecture Hexagonale, aussi connue sous le nom de "Ports and Adapters", trouve une application particulièrement pertinente dans le contexte des microservices. Cette approche permet de maintenir la séparation des préoccupations tout en facilitant les tests et l'évolutivité.
L'architecture hexagonale, conceptualisée par Alistair Cockburn, place la logique métier au centre et isole les préoccupations techniques (base de données, frameworks web, etc.) à la périphérie.
Dans un contexte microservices, chaque service peut adopter cette architecture pour maintenir sa cohérence interne.
user-service/
├── domain/
│ ├── entities/
│ │ └── User.ts
│ ├── services/
│ │ └── UserService.ts
│ └── ports/
│ ├── UserRepository.ts
│ └── NotificationService.ts
├── adapters/
│ ├── inbound/
│ │ ├── rest/
│ │ │ └── UserController.ts
│ │ └── messaging/
│ │ └── UserEventHandler.ts
│ └── outbound/
│ ├── persistence/
│ │ └── MongoUserRepository.ts
│ └── notification/
│ └── EmailNotificationService.ts
└── config/
└── DependencyContainer.ts
// domain/entities/User.ts
export class User {
constructor(
private readonly id: UserId,
private email: Email,
private profile: UserProfile
) {}
updateEmail(newEmail: Email): void {
// Logique métier pour la validation
if (!newEmail.isValid()) {
throw new InvalidEmailError();
}
this.email = newEmail;
}
// Autres méthodes métier...
}
// domain/services/UserService.ts
export class UserService {
constructor(
private userRepository: UserRepository,
private notificationService: NotificationService
) {}
async registerUser(command: RegisterUserCommand): Promise<User> {
// Validation métier
if (await this.userRepository.existsByEmail(command.email)) {
throw new UserAlreadyExistsError();
}
const user = new User(
UserId.generate(),
command.email,
UserProfile.fromCommand(command)
);
await this.userRepository.save(user);
await this.notificationService.sendWelcomeEmail(user);
return user;
}
}
// domain/ports/UserRepository.ts
export interface UserRepository {
save(user: User): Promise<void>;
findById(id: UserId): Promise<User | null>;
existsByEmail(email: Email): Promise<boolean>;
delete(id: UserId): Promise<void>;
}
// domain/ports/NotificationService.ts
export interface NotificationService {
sendWelcomeEmail(user: User): Promise<void>;
sendPasswordResetEmail(user: User, token: string): Promise<void>;
}
// adapters/inbound/rest/UserController.ts
@Controller('/users')
export class UserController {
constructor(private userService: UserService) {}
@Post()
async createUser(@Body() request: CreateUserRequest): Promise<UserResponse> {
const command = new RegisterUserCommand(
new Email(request.email),
request.firstName,
request.lastName
);
const user = await this.userService.registerUser(command);
return UserResponse.fromUser(user);
}
}
// adapters/inbound/messaging/UserEventHandler.ts
@EventHandler()
export class UserEventHandler {
constructor(private userService: UserService) {}
@EventPattern('user.registration.requested')
async handleUserRegistration(event: UserRegistrationRequestedEvent): Promise<void> {
const command = RegisterUserCommand.fromEvent(event);
await this.userService.registerUser(command);
}
}
// adapters/outbound/persistence/MongoUserRepository.ts
@Repository()
export class MongoUserRepository implements UserRepository {
constructor(private mongoClient: MongoClient) {}
async save(user: User): Promise<void> {
const document = {
_id: user.id.value,
email: user.email.value,
profile: user.profile.toDocument(),
createdAt: new Date()
};
await this.mongoClient
.db('users')
.collection('users')
.replaceOne({ _id: document._id }, document, { upsert: true });
}
async findById(id: UserId): Promise<User | null> {
const document = await this.mongoClient
.db('users')
.collection('users')
.findOne({ _id: id.value });
return document ? User.fromDocument(document) : null;
}
}
Pour protéger votre domaine des changements dans les services externes :
// adapters/outbound/external/OrderServiceAdapter.ts
export class OrderServiceAdapter implements OrderService {
constructor(private httpClient: HttpClient) {}
async getUserOrders(userId: UserId): Promise<UserOrder[]> {
try {
const response = await this.httpClient.get(
`/orders/by-user/${userId.value}`
);
// Transformation des données externes vers notre modèle
return response.data.map(this.transformToUserOrder);
} catch (error) {
if (error.status === 404) {
return [];
}
throw new ExternalServiceError('Order service unavailable');
}
}
private transformToUserOrder(externalOrder: any): UserOrder {
// Logique de transformation et validation
return new UserOrder(
new OrderId(externalOrder.id),
new Money(externalOrder.total_amount, externalOrder.currency),
new Date(externalOrder.created_at)
);
}
}
describe('UserService', () => {
let userService: UserService;
let mockUserRepository: jest.Mocked<UserRepository>;
let mockNotificationService: jest.Mocked<NotificationService>;
beforeEach(() => {
mockUserRepository = {
save: jest.fn(),
findById: jest.fn(),
existsByEmail: jest.fn(),
delete: jest.fn(),
};
mockNotificationService = {
sendWelcomeEmail: jest.fn(),
sendPasswordResetEmail: jest.fn(),
};
userService = new UserService(mockUserRepository, mockNotificationService);
});
it('should register a new user successfully', async () => {
// Arrange
const command = new RegisterUserCommand(
new Email('test@example.com'),
'John',
'Doe'
);
mockUserRepository.existsByEmail.mockResolvedValue(false);
// Act
const result = await userService.registerUser(command);
// Assert
expect(mockUserRepository.save).toHaveBeenCalledWith(
expect.any(User)
);
expect(mockNotificationService.sendWelcomeEmail).toHaveBeenCalled();
});
});
describe('UserController Integration', () => {
let app: INestApplication;
let userRepository: UserRepository;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [UserModule],
})
.overrideProvider(UserRepository)
.useClass(InMemoryUserRepository)
.compile();
app = moduleRef.createNestApplication();
userRepository = moduleRef.get(UserRepository);
await app.init();
});
it('should create user via REST API', async () => {
const response = await request(app.getHttpServer())
.post('/users')
.send({
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe'
})
.expect(201);
expect(response.body.email).toBe('test@example.com');
});
});
// config/DependencyContainer.ts
export class DependencyContainer {
static configure(environment: Environment): Container {
const container = new Container();
// Repositories
if (environment === 'production') {
container.bind(UserRepository).to(MongoUserRepository);
} else {
container.bind(UserRepository).to(InMemoryUserRepository);
}
// External services
container.bind(NotificationService).to(EmailNotificationService);
container.bind(OrderService).to(OrderServiceAdapter);
// Domain services
container.bind(UserService).toSelf();
return container;
}
}
// adapters/inbound/rest/HealthController.ts
@Controller('/health')
export class HealthController {
constructor(
private userRepository: UserRepository,
private notificationService: NotificationService
) {}
@Get()
async checkHealth(): Promise<HealthStatus> {
const checks = await Promise.allSettled([
this.userRepository.healthCheck(),
this.notificationService.healthCheck(),
]);
return {
status: checks.every(check => check.status === 'fulfilled')
? 'healthy'
: 'unhealthy',
dependencies: {
database: checks[0].status,
notification: checks[1].status,
}
};
}
}
L'architecture hexagonale dans les microservices offre un excellent équilibre entre flexibilité et maintenabilité. Elle permet de :
L'investissement initial en complexité est largement compensé par les bénéfices à long terme, particulièrement dans des systèmes distribués complexes.
Repenser le pattern repository pour les applications contemporaines. Quand l'utiliser, comment l'implémenter efficacement, et les alternatives à considérer à l'ère des ORMs et architectures cloud-native.
Mettre en œuvre les principes de Clean Architecture dans les applications modernes. Comment créer des systèmes maintenables, testables et indépendants avec une gestion appropriée des dépendances.
Comment identifier, concevoir et implémenter des bounded contexts efficaces dans des domaines complexes. Patterns stratégiques, techniques de context mapping et exemples concrets.