From 123fe1b5413f8603f33f3ee7c6dcc9f5dc586a0f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 01:06:37 +0000 Subject: [PATCH] refactor: reserve plain factory names for Outcome-returning variants Apply ADR-0005 across the Usage samples and the documentation: the Outcome-returning factory carries the plain name and the throwing variant takes the OrThrow suffix, so no method returning an Outcome wears the Try prefix whose bool+out shape it never honours. - Temperature: TryFromKelvin/TryFromCelsius become FromKelvin/FromCelsius (Outcome), the throwing FromKelvin/FromCelsius become FromKelvinOrThrow/FromCelsiusOrThrow, and the AbsoluteZero call site and the "Attempts to create" XML-doc summaries are updated to match. - Amount: the throwing Add/Subtract become AddOrThrow/SubtractOrThrow. - Docs (EN + FR): reword the GettingStarted, UsagePatterns and OutcomeGuide examples (TryAdd/TryParseAmount/TryCreateAmount, and the non-factory TryAlternativeProviderAsync), and align the Amount.Add prose references in WritingErrorMessages and WritingErrorsGuide. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013eNZiEbSu9gdvUSLBoPNd4 --- FirstClassErrors.Usage/Model/Amount.cs | 4 ++-- FirstClassErrors.Usage/Model/Temperature.cs | 18 +++++++++--------- doc/GettingStarted.en.md | 4 ++-- doc/GettingStarted.fr.md | 4 ++-- doc/OutcomeGuide.en.md | 14 +++++++------- doc/OutcomeGuide.fr.md | 14 +++++++------- doc/UsagePatterns.en.md | 20 ++++++++++---------- doc/UsagePatterns.fr.md | 20 ++++++++++---------- doc/WritingErrorMessages.en.md | 2 +- doc/WritingErrorMessages.fr.md | 2 +- doc/WritingErrorsGuide.en.md | 2 +- doc/WritingErrorsGuide.fr.md | 2 +- 12 files changed, 53 insertions(+), 53 deletions(-) diff --git a/FirstClassErrors.Usage/Model/Amount.cs b/FirstClassErrors.Usage/Model/Amount.cs index cc87b4b..91312bd 100644 --- a/FirstClassErrors.Usage/Model/Amount.cs +++ b/FirstClassErrors.Usage/Model/Amount.cs @@ -39,7 +39,7 @@ public Amount(decimal value, Currency currency) { public decimal Value { get; } public Currency Currency { get; } - public Amount Add(Amount other) { + public Amount AddOrThrow(Amount other) { ArgumentNullException.ThrowIfNull(other); EnsureSameCurrency(other); @@ -47,7 +47,7 @@ public Amount Add(Amount other) { return new Amount(Value + other.Value, Currency); } - public Amount Subtract(Amount other) { + public Amount SubtractOrThrow(Amount other) { ArgumentNullException.ThrowIfNull(other); EnsureSameCurrency(other); diff --git a/FirstClassErrors.Usage/Model/Temperature.cs b/FirstClassErrors.Usage/Model/Temperature.cs index 978e13a..757b405 100644 --- a/FirstClassErrors.Usage/Model/Temperature.cs +++ b/FirstClassErrors.Usage/Model/Temperature.cs @@ -25,7 +25,7 @@ public sealed class Temperature : IEquatable, IComparable /// Represents the absolute zero temperature, the lowest possible temperature where particles have minimal energy. /// - public static readonly Temperature AbsoluteZero = FromKelvin(AbsoluteZeroInKelvin); + public static readonly Temperature AbsoluteZero = FromKelvinOrThrow(AbsoluteZeroInKelvin); /// /// Creates a from a kelvin value. @@ -35,12 +35,12 @@ public sealed class Temperature : IEquatable, IComparable /// Thrown when is lower than absolute zero. /// - public static Temperature FromKelvin(decimal kelvin) { - return TryFromKelvin(kelvin).GetResultOrThrow(); + public static Temperature FromKelvinOrThrow(decimal kelvin) { + return FromKelvin(kelvin).GetResultOrThrow(); } /// - /// Attempts to create a from a kelvin value. + /// Creates a from a kelvin value. /// /// Temperature in kelvin. /// @@ -48,7 +48,7 @@ public static Temperature FromKelvin(decimal kelvin) { /// is not below absolute zero; otherwise a failure containing the corresponding /// . /// - public static Outcome TryFromKelvin(decimal kelvin) { + public static Outcome FromKelvin(decimal kelvin) { if (IsLowerThanAbsoluteZero(kelvin)) { return Outcome.Failure(InvalidTemperatureError.BelowAbsoluteZero(kelvin, TemperatureUnit.Kelvin)); } return Outcome.Success(new Temperature(kelvin)); @@ -62,12 +62,12 @@ public static Outcome TryFromKelvin(decimal kelvin) { /// /// Thrown when the converted kelvin value is lower than absolute zero. /// - public static Temperature FromCelsius(decimal celsius) { - return TryFromCelsius(celsius).GetResultOrThrow(); + public static Temperature FromCelsiusOrThrow(decimal celsius) { + return FromCelsius(celsius).GetResultOrThrow(); } /// - /// Attempts to create a from a Celsius value. + /// Creates a from a Celsius value. /// /// Temperature in degrees Celsius. /// @@ -75,7 +75,7 @@ public static Temperature FromCelsius(decimal celsius) { /// is not below absolute zero; otherwise a failure containing the corresponding /// . /// - public static Outcome TryFromCelsius(decimal celsius) { + public static Outcome FromCelsius(decimal celsius) { decimal kelvin = celsius + CelsiusToKelvinOffset; if (IsLowerThanAbsoluteZero(kelvin)) { return Outcome.Failure(InvalidTemperatureError.BelowAbsoluteZero(celsius, TemperatureUnit.Celsius)); } diff --git a/doc/GettingStarted.en.md b/doc/GettingStarted.en.md index 10cf3d4..d065def 100644 --- a/doc/GettingStarted.en.md +++ b/doc/GettingStarted.en.md @@ -107,7 +107,7 @@ Each diagnostic is a hypothesis: a plausible cause, an origin (whether the suspe When the failure is exceptional, turn the error into its paired exception: ```csharp -public Amount Add(Amount other) { +public Amount AddOrThrow(Amount other) { if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } @@ -121,7 +121,7 @@ The business code names the situation without repeating codes or messages. When failure is an expected part of the flow, carry the same `Error` without throwing: ```csharp -public static Outcome TryAdd(Amount left, Amount right) { +public static Outcome Add(Amount left, Amount right) { if (left.Currency != right.Currency) { return Outcome.Failure( InvalidAmountOperationError.CurrencyMismatch(left, right)); diff --git a/doc/GettingStarted.fr.md b/doc/GettingStarted.fr.md index 71ee218..2af2c6c 100644 --- a/doc/GettingStarted.fr.md +++ b/doc/GettingStarted.fr.md @@ -107,7 +107,7 @@ Chaque diagnostic est une hypothèse : une cause plausible, une origine (le soup Lorsque l’échec est exceptionnel, transformez l’erreur en son exception associée : ```csharp -public Amount Add(Amount other) { +public Amount AddOrThrow(Amount other) { if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } @@ -121,7 +121,7 @@ Le code métier nomme la situation sans répéter les codes ni les messages. Lorsque l’échec fait normalement partie du flux, transportez la même `Error` sans la lever : ```csharp -public static Outcome TryAdd(Amount left, Amount right) { +public static Outcome Add(Amount left, Amount right) { if (left.Currency != right.Currency) { return Outcome.Failure( InvalidAmountOperationError.CurrencyMismatch(left, right)); diff --git a/doc/OutcomeGuide.en.md b/doc/OutcomeGuide.en.md index 7f60399..881a1d6 100644 --- a/doc/OutcomeGuide.en.md +++ b/doc/OutcomeGuide.en.md @@ -60,7 +60,7 @@ return Outcome.Failure( Use `IsSuccess` and `IsFailure` when explicit branching is clearest. ```csharp -Outcome result = TryCreateAmount(value, currencyCode); +Outcome result = CreateAmount(value, currencyCode); if (result.IsFailure) { Log(result.Error!); @@ -83,7 +83,7 @@ A function that **returns a value** transforms the carried value (a step that ca ```csharp Outcome total = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Then(amount => amount.WithVat()); ``` @@ -91,7 +91,7 @@ A function that **returns an `Outcome`** runs another step that may itself fail: ```csharp Outcome result = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Then(CheckLimits) .Then(Charge); ``` @@ -116,7 +116,7 @@ A value fallback can be returned as a successful outcome: ```csharp Outcome amount = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Recover(error => Outcome.Success(Amount.Zero)); ``` @@ -160,7 +160,7 @@ It does nothing on success and throws `error.ToException()` on failure. Use it with `Outcome` when the boundary requires a value or an exception. ```csharp -Amount amount = TryCreateAmount(value, currencyCode).GetResultOrThrow(); +Amount amount = CreateAmount(value, currencyCode).GetResultOrThrow(); ``` The exception is created and thrown at this point. The stack trace therefore begins at the escalation point, not where the `Outcome` failure was created. @@ -212,13 +212,13 @@ public async Task> CheckoutAsync( string currencyCode, CancellationToken cancellationToken) { - return await TryCreateAmount(value, currencyCode) + return await CreateAmount(value, currencyCode) .Then(CheckLimits) .Then( (amount, ct) => ChargeAsync(amount, ct), cancellationToken) .Recover( - (error, ct) => TryAlternativeProviderAsync(error, ct), + (error, ct) => AlternativeProviderAsync(error, ct), cancellationToken); } ``` diff --git a/doc/OutcomeGuide.fr.md b/doc/OutcomeGuide.fr.md index 6d82452..fc8f857 100644 --- a/doc/OutcomeGuide.fr.md +++ b/doc/OutcomeGuide.fr.md @@ -60,7 +60,7 @@ return Outcome.Failure( Utilisez `IsSuccess` et `IsFailure` lorsqu’un branchement explicite est le plus lisible. ```csharp -Outcome result = TryCreateAmount(value, currencyCode); +Outcome result = CreateAmount(value, currencyCode); if (result.IsFailure) { Log(result.Error!); @@ -83,7 +83,7 @@ Une fonction qui **renvoie une valeur** transforme la valeur portée (une étape ```csharp Outcome total = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Then(amount => amount.WithVat()); ``` @@ -91,7 +91,7 @@ Une fonction qui **renvoie un `Outcome`** exécute une autre étape susceptible ```csharp Outcome result = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Then(CheckLimits) .Then(Charge); ``` @@ -116,7 +116,7 @@ Une valeur de repli peut être retournée sous forme de succès : ```csharp Outcome amount = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Recover(error => Outcome.Success(Amount.Zero)); ``` @@ -160,7 +160,7 @@ Elle ne fait rien en cas de succès et lève `error.ToException()` en cas d’é Utilisez-la avec `Outcome` lorsqu’une frontière exige une valeur ou une exception. ```csharp -Amount amount = TryCreateAmount(value, currencyCode).GetResultOrThrow(); +Amount amount = CreateAmount(value, currencyCode).GetResultOrThrow(); ``` L’exception est créée et levée à cet endroit. La stack trace commence donc au point d’escalade, pas là où l’échec `Outcome` a été créé. @@ -212,13 +212,13 @@ public async Task> CheckoutAsync( string currencyCode, CancellationToken cancellationToken) { - return await TryCreateAmount(value, currencyCode) + return await CreateAmount(value, currencyCode) .Then(CheckLimits) .Then( (amount, ct) => ChargeAsync(amount, ct), cancellationToken) .Recover( - (error, ct) => TryAlternativeProviderAsync(error, ct), + (error, ct) => AlternativeProviderAsync(error, ct), cancellationToken); } ``` diff --git a/doc/UsagePatterns.en.md b/doc/UsagePatterns.en.md index d46ff80..097efac 100644 --- a/doc/UsagePatterns.en.md +++ b/doc/UsagePatterns.en.md @@ -26,29 +26,29 @@ A value object must not enter an invalid state. Whether failure belongs to the c ```csharp // Contract: returns a valid Temperature or fails — failure has no place in this method's return type. -public static Temperature FromKelvin(decimal kelvin) { - return TryFromKelvin(kelvin).GetResultOrThrow(); +public static Temperature FromKelvinOrThrow(decimal kelvin) { + return FromKelvin(kelvin).GetResultOrThrow(); } ``` ```csharp // Contract: an out-of-range value is part of the normal contract — the caller must handle it explicitly. -public static Outcome TryFromKelvin(decimal kelvin) { +public static Outcome FromKelvin(decimal kelvin) { if (kelvin < 0) { return Outcome.Failure(InvalidTemperatureError.BelowAbsoluteZero(kelvin, TemperatureUnit.Kelvin)); } return Outcome.Success(new Temperature(kelvin)); } ``` -Both report the exact same documented error, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvin` does not repeat the check; it escalates `TryFromKelvin`'s failure with `GetResultOrThrow()`. When both contracts are genuinely useful, centralize validation in the `Outcome`-returning version and derive the throwing one from it — not the reverse. +Both report the exact same documented error, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvinOrThrow` does not repeat the check; it escalates `FromKelvin`'s failure with `GetResultOrThrow()`. When both contracts are genuinely useful, centralize validation in the `Outcome`-returning version and derive the throwing one from it — not the reverse. ## 🧮 Domain operations -Operations between valid domain objects may still violate a rule. The choice here is a matter of API contract, not of who is theoretically allowed to react — a caller of `Add` could still catch and handle a thrown mismatch, just as a caller of `TryAdd` could still ignore a returned one. What differs is what each method promises to its caller: +Operations between valid domain objects may still violate a rule. The choice here is a matter of API contract, not of who is theoretically allowed to react — a caller of `AddOrThrow` could still catch and handle a thrown mismatch, just as a caller of `Add` could still ignore a returned one. What differs is what each method promises to its caller: ```csharp // Contract: the amounts passed in must already share the same currency. -public Amount Add(Amount other) { +public Amount AddOrThrow(Amount other) { if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } return new Amount(Value + other.Value, Currency); @@ -57,7 +57,7 @@ public Amount Add(Amount other) { ```csharp // Contract: a currency mismatch is an expected outcome the caller must handle. -public Outcome TryAdd(Amount other) { +public Outcome Add(Amount other) { if (Currency != other.Currency) { return Outcome.Failure(InvalidAmountOperationError.CurrencyMismatch(this, other)); } return Outcome.Success(new Amount(Value + other.Value, Currency)); @@ -73,7 +73,7 @@ Each item in a batch can fail independently — but whether that failure is part ```csharp // Contract: a per-line failure is expected and handled locally — log it and move on. foreach (string line in file) { - TryParseAmount(line).Finally( + ParseAmount(line).Finally( onSuccess: Process, onFailure: Log); } @@ -82,7 +82,7 @@ foreach (string line in file) { ```csharp // Contract: any invalid line invalidates the whole file — stop immediately. foreach (string line in file) { - Amount amount = TryParseAmount(line).GetResultOrThrow(); + Amount amount = ParseAmount(line).GetResultOrThrow(); Process(amount); } ``` @@ -148,7 +148,7 @@ When several expected-failure operations must run in sequence, compose their out ```csharp Outcome result = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Then(CheckLimits) .Then(Charge); ``` diff --git a/doc/UsagePatterns.fr.md b/doc/UsagePatterns.fr.md index 2f94706..7fe2133 100644 --- a/doc/UsagePatterns.fr.md +++ b/doc/UsagePatterns.fr.md @@ -26,29 +26,29 @@ Un value object ne doit jamais entrer dans un état invalide. Que l’échec app ```csharp // Contrat : renvoie une Temperature valide ou échoue — l'échec n'a pas sa place dans le type de retour de cette méthode. -public static Temperature FromKelvin(decimal kelvin) { - return TryFromKelvin(kelvin).GetResultOrThrow(); +public static Temperature FromKelvinOrThrow(decimal kelvin) { + return FromKelvin(kelvin).GetResultOrThrow(); } ``` ```csharp // Contrat : une valeur hors limite fait partie du contrat normal — l'appelant doit la traiter explicitement. -public static Outcome TryFromKelvin(decimal kelvin) { +public static Outcome FromKelvin(decimal kelvin) { if (kelvin < 0) { return Outcome.Failure(InvalidTemperatureError.BelowAbsoluteZero(kelvin, TemperatureUnit.Kelvin)); } return Outcome.Success(new Temperature(kelvin)); } ``` -Les deux rapportent exactement la même erreur documentée, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvin` ne répète pas la vérification : elle escalade l’échec de `TryFromKelvin` avec `GetResultOrThrow()`. Lorsque les deux contrats sont réellement utiles, centralisez la validation dans la version retournant un `Outcome`, puis dérivez-en la version levante — pas l’inverse. +Les deux rapportent exactement la même erreur documentée, `InvalidTemperatureError.BelowAbsoluteZero`. `FromKelvinOrThrow` ne répète pas la vérification : elle escalade l’échec de `FromKelvin` avec `GetResultOrThrow()`. Lorsque les deux contrats sont réellement utiles, centralisez la validation dans la version retournant un `Outcome`, puis dérivez-en la version levante — pas l’inverse. ## 🧮 Opérations métier -Des objets métier valides peuvent néanmoins participer à une opération qui viole une règle. Le choix ici est une question de contrat d’API, pas de qui serait théoriquement en mesure de réagir — un appelant d’`Add` pourrait tout à fait capturer et traiter un écart levé, tout comme un appelant de `TryAdd` pourrait tout à fait ignorer un échec retourné. Ce qui diffère, c’est ce que chaque méthode promet à son appelant : +Des objets métier valides peuvent néanmoins participer à une opération qui viole une règle. Le choix ici est une question de contrat d’API, pas de qui serait théoriquement en mesure de réagir — un appelant d’`AddOrThrow` pourrait tout à fait capturer et traiter un écart levé, tout comme un appelant de `Add` pourrait tout à fait ignorer un échec retourné. Ce qui diffère, c’est ce que chaque méthode promet à son appelant : ```csharp // Contrat : les montants fournis doivent déjà être exprimés dans la même devise. -public Amount Add(Amount other) { +public Amount AddOrThrow(Amount other) { if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } return new Amount(Value + other.Value, Currency); @@ -57,7 +57,7 @@ public Amount Add(Amount other) { ```csharp // Contrat : une incompatibilité de devises est une issue attendue que l'appelant doit traiter. -public Outcome TryAdd(Amount other) { +public Outcome Add(Amount other) { if (Currency != other.Currency) { return Outcome.Failure(InvalidAmountOperationError.CurrencyMismatch(this, other)); } return Outcome.Success(new Amount(Value + other.Value, Currency)); @@ -73,7 +73,7 @@ Chaque élément d’un lot peut échouer indépendamment — mais le fait que c ```csharp // Contrat : un échec par ligne est attendu et traité localement — le loguer et continuer. foreach (string line in file) { - TryParseAmount(line).Finally( + ParseAmount(line).Finally( onSuccess: Process, onFailure: Log); } @@ -82,7 +82,7 @@ foreach (string line in file) { ```csharp // Contrat : une ligne invalide invalide tout le fichier — arrêter immédiatement. foreach (string line in file) { - Amount amount = TryParseAmount(line).GetResultOrThrow(); + Amount amount = ParseAmount(line).GetResultOrThrow(); Process(amount); } ``` @@ -148,7 +148,7 @@ Lorsque plusieurs opérations susceptibles d’échouer doivent s’enchaîner, ```csharp Outcome result = - TryCreateAmount(value, currencyCode) + CreateAmount(value, currencyCode) .Then(CheckLimits) .Then(Charge); ``` diff --git a/doc/WritingErrorMessages.en.md b/doc/WritingErrorMessages.en.md index d085160..bafbb68 100644 --- a/doc/WritingErrorMessages.en.md +++ b/doc/WritingErrorMessages.en.md @@ -48,7 +48,7 @@ Good: Avoid: -> “Currency validation failed in `Amount.Add` for order 42.” +> “Currency validation failed in `Amount.AddOrThrow` for order 42.” The second example exposes implementation detail and an occurrence identifier that does not belong in a reusable public summary. diff --git a/doc/WritingErrorMessages.fr.md b/doc/WritingErrorMessages.fr.md index 4fb0e4e..063b75a 100644 --- a/doc/WritingErrorMessages.fr.md +++ b/doc/WritingErrorMessages.fr.md @@ -48,7 +48,7 @@ Bon : À éviter : -> « La validation des devises a échoué dans `Amount.Add` pour la commande 42. » +> « La validation des devises a échoué dans `Amount.AddOrThrow` pour la commande 42. » Le second exemple expose un détail d’implémentation et un identifiant d’occurrence qui n’ont pas leur place dans un résumé public réutilisable. diff --git a/doc/WritingErrorsGuide.en.md b/doc/WritingErrorsGuide.en.md index ac00aaa..58f2bf9 100644 --- a/doc/WritingErrorsGuide.en.md +++ b/doc/WritingErrorsGuide.en.md @@ -101,7 +101,7 @@ Good: Avoid: -> “This exception is thrown by `Amount.Add` when the currency fields are different.” +> “This exception is thrown by `Amount.AddOrThrow` when the currency fields are different.” ## 5. State the violated rule diff --git a/doc/WritingErrorsGuide.fr.md b/doc/WritingErrorsGuide.fr.md index bd4edd2..5821dbe 100644 --- a/doc/WritingErrorsGuide.fr.md +++ b/doc/WritingErrorsGuide.fr.md @@ -101,7 +101,7 @@ Bon : À éviter : -> « Cette exception est levée par `Amount.Add` lorsque les champs de devise sont différents. » +> « Cette exception est levée par `Amount.AddOrThrow` lorsque les champs de devise sont différents. » ## 5. Énoncer la règle violée