Aller au contenu
Filtrer les règles par style d’implémentation
Style
Les deux styles
Sévérité
mineure
S’applique à
  • Use cases (application)
  • Adapters driving

UC-03 · Un use case reçoit une command qui porte le nom de l’opération ​

Style
Les deux styles
Sévérité
mineure
S’applique à
  • Use cases (application)
  • Adapters driving

Signal ​

  • Un use case avec une longue liste de paramètres primitifs. Neuf, c’est déjà beaucoup trop.
  • Un DTO générique nommé d’après une entity (WalletDto, HolderInput), et partagé par plusieurs opérations.

Pourquoi ​

Il existe trois façons de recevoir l’entrée :

  • des types du domaine dans la signature : un typage fort et les gardes des value objects, mais l’interface dépend du modèle ;
  • des types primitifs : aucun couplage, mais ni vérification de type, ni gardes ;
  • des objets command : souvent la meilleure option.

Une command est un appel de méthode sérialisé. Nommée d’après l’opération, elle est plus explicite qu’un DTO. Elle peut aussi passer par une queue jusqu’à un command handler : l’équivalent d’un use case, découplé dans le temps.

Exceptions ​

Il n’y a pas de bonne ou de mauvaise réponse : cela dépend des goûts et des objectifs, et les trois styles cohabitent. Quelques paramètres simples conviennent.

Correction ​

Une command par opération, avec des champs simples. Le use case en tire les value objects.

Adaptation ​

Notre choix : la requête est un schéma Zod dans driving/http (open-wallet.request.ts), validé à la périphérie. Le controller construit la command, déclarée avec son handler dans application (OpenWalletCommand), et ne la partage jamais entre opérations. Le use case la convertit en value objects, ce qui rétablit les gardes du domaine.

Voir aussi ​

UC-01 (une méthode par use case), UC-02 (pas de types du transport dans l’application), VO-01.

Exemple ​

ts
// Avoid: one DTO shared by every operation on a wallet, with optional primitive fields
export interface WalletDto {
	holderId?: string;
	walletId?: string;
	currency?: string;
	reference?: string;
}

export abstract class WalletService {
	abstract open(dto: WalletDto): Promise<WalletDto>;
	abstract block(dto: WalletDto): Promise<WalletDto>;
}
ts
// Preferred: one command per operation, named after it, converted into value objects by its handler
export interface OpenWalletCommand {
	readonly holderId: string;
	readonly currency: string;
}

export class OpenWalletHandler extends CommandHandler<OpenWalletCommand, WalletId, OpenWalletError> {
	async handle(command: OpenWalletCommand): Promise<Result<WalletId, OpenWalletError>> {
		const holderId = HolderId.of(command.holderId);

		if (!holderId.ok) {
			return holderId;
		}

		// the currency the same way, then the holder opens the wallet, as in the factories overview
	}
}
ts
// driving/http/wallet.controller.ts: the request, checked at the edge, becomes the command
const opened = await this.#openWallet.handle({ holderId, currency: body.currency });

Avec WalletDto, rien ne dit quels champs open lit, ni lesquels block exige : il faut lire le code. OpenWalletCommand le dit dans son nom et dans ses champs, et BlockWalletCommand a les siens. Le handler reçoit des chaînes, et les convertit en value objects : HolderId.of rétablit la garde du domaine.