246
220

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

あなたのエラーコードは何ですか?

Last updated at Posted at 2018-10-05

システムにおいて必ずといっていいほど存在するエラーコード。
このエラーコードってプロジェクトによってさまざまな形式だったりします。
そんなエラーコードとそのハンドリングで比較的上手くいっているパターンについてご紹介します。

具体的には列挙体を使ったエラーハンドリングです。

エラーコードの型は何ですか?

ソフトウェアやハードウェア、およそコンピュータに関わっていれば必ずといっていいほど目にするエラーコード。
皆さんが作っているシステム、日々携わっているシステム、それらのエラーコードってどのようなものでしょうか。
より具体的に言えばどんな型のエラーコードを取り扱っていますか?

例えば数字です。
80011
こんなエラーコードがあるでしょう。

例えば文字列です。
'E1000'
こういったエラーコードもあるでしょう。

色々なエラーコードが存在する世の中で今挙げたエラーコードを取り扱っている方もいれば、全く違うエラーコードの場合もあるでしょう。

そんなエラーコードですが私はプログラム中では必ず列挙体を使うようにしています。
というわけで今回はエラーコードを列挙体にするというお話です。

違いを比べよう

エラーコードを列挙体で表現するメリットは何でしょうか。

物事のメリットを考える時、比較をすることでそれのメリットが浮き彫りになることはあると思います。
前述の文字列や数字のエラーコードと比較をしていきましょう。

メッセージ性

まずは次のエラーコードをご覧ください。

404

このエラーコードは何を意味するのでしょうか。
もしかしたら「見つからなかった」という意図を受け取ることができた人がいるかもしれません。
人によってその解釈はあいまいになると思います。

では同じエラーを列挙体で表すとどうでしょうか。

UpdateUserError.UserNotFound

印象がだいぶ変わって見えますね。

数字や文字列をエラーコードにした場合、ある種の暗号を扱うことと同義です。
これらをエラーコードにしてしまうとそれだけで解読するのは難しくなってしまいます。

プログラムでマジックナンバーを避けるのと同様、可能であれば避けたいところです。

列挙体を利用すると、エラーコードそのものに意味がわかるように名前をつけることができるのでこの問題が解決されます。

エラーコードを見るだけでそれが何のエラーかわかる。
とても素敵なことですね。

一意性

一意性とは意味や値が一つに確定しているという性質です。
これも列挙体のメリットです。

例えばユースケースは異なるけれど、とても似通ったエラーは存在します。

  • ユーザ更新時に対象ユーザが見つからなかった
  • ユーザ削除時に対象ユーザが見つからなかった

数字や文字列でエラーコードを表現した場合、意図せずして同じエラーコードになってしまうことがあります。
特に先ほどの 404 などは典型で、ユースケース毎に担当者が異なった場合に、今回の「見つからなかった」に着目すると、結果として次のようなコード対応表になってしまうことがあります。

コード 内容
404 ユーザ更新時に対象ユーザが見つからなかった
404 ユーザ削除時に対象ユーザが見つからなかった

数字や文字列等のある種暗号めいたものをエラーコードにした場合、エラーコードが重複することがあります。
そのエラーコードが何を表しているのかはコンテキスト(文脈)を確認しないと判別できません。

  • "更新処理の" 404 だから「更新対象のユーザが見つからない」
  • "削除処理の" 404 だから「削除対象のユーザが見つからない」

"更新処理の"、"削除処理の" それぞれがコンテキストです。

列挙体はこのコンテキストを型の名前や実際の値の名前で表すことができます。

enum UserUpdateError{
  UserNotFound
}

enum UserDeleteError{
  UserNotFound
}

「ユーザが見つからない」というエラーですが、それぞれユースケースが異なるのがわかります。

また 404 という数字を検索して、そのエラーコードを発生させた箇所を探すのはうんざりするような検索処理です。
検索結果に出てくるのがエラーコードだけであればよいですが、たまたま全く同じ数値がコード上に存在していたりすると、付近のコードを見て判断するしかなかったりします。

int UpdateUserName(string userId, string userName){
  var user = userRepository.Find(userId);
  if(user == null) {
    return 1; // 見つけたいのはこのエラーコード 1
  }

  user.ChangeUserName(userName);

  return 0;
}

long Factorial(int n) {
  if (n == 0) {
    return 1L; // ここが検索に引っかかります
  }

  return n * Factorial(n - 1); // この行も引っかかります
}

列挙体であれば参照されているところを検索すれば OK です。

UserUpdateError? UpdateUserName(string userId, string userName){
  var user = userRepository.Find(userId);
  if(user == null) {
    return UserUpdateError.UserNotFound;
  }

  user.ChangeUserName(userName);

  return null; // あまり null を判断基準にするのはお勧めできませんが本題ではないので適当に
}

コードを見るだけでそれがどこで起きたエラーかわかる。
とても素敵なことですね。

管理手法

コード 意味
5000 作成しようとしたユーザ名が重複する
5001 更新対象ユーザが存在しない
5002 削除対象ユーザが存在しない

エラーコードを数字や文字列で管理する場合にこんな表で管理することがあったりします。
エラーコード単体では意味がわからないので、エラーが発生するたびにコードとこの対応表とを見比べることになります。

さて「ユーザを更新しようとしたがユーザ名重複により失敗した」というエラーを追加しようしてみましょう。
現在のコードに文末に追加する形になると思います。

コード 意味
5000 作成しようとしたユーザ名が重複する
5001 更新対象ユーザが存在しない
5002 削除対象ユーザが存在しない
5003 更新用のユーザ名が重複している

こうなると、作成処理、更新処理、削除処理とエラーコードが入り乱れることになります。
なるべくなら処理ごとに分けて俯瞰できるように管理しておきたいものですね。

管理できるようにするのであれば、例えば次のようなコード体系にすれば追加削除も容易でしょうか。

コード 意味
5000-0000 作成しようとしたユーザ名が重複する
5001-0000 更新対象ユーザが存在しない
5002-0000 削除対象ユーザが存在しない

このコード群に「更新用のユーザ名が重複している」エラーを追加するとこんな感じです。

コード 意味
5000-0000 作成しようとしたユーザ名が重複する
5001-0000 更新対象ユーザが存在しない
5001-0001 更新用のユーザ名が重複している
5002-0000 削除対象ユーザが存在しない

作成処理は 5000 番台、更新処理は 5001 番台、削除処理は 5002 番台と整理されています。
エラーが発生する際にエラーコードとその意味を突き合わせをする必要は発生しますが、上手く管理されているエラーコードではないでしょうか。

    :
    :
    :

さて、列挙体の場合は次のようになります。

enum CreateUserError{
  DuplicatedUserName,
}

enum UpdateUserError{
  TargetNotFound,
}

enum DeleteUserError{
  TargetNotFound,
}

もはや対応表は不要でしょう。
Excel を使う理由が一つ減りました。

先ほど例に挙げた更新処理に重複エラーを追加すると次のようになります。

enum CreateUserError{
  DuplicatedUserName,
}

enum UpdateUserError{
  TargetNotFound,
  DuplicatedUserName,
}

enum DeleteUserError{
  TargetNotFound,
}

エラー内容の追加に備えたコード体系を考える必要もなくなります。

コードがあれば対応表が要らない。
とても素敵なことですね。

メリットまとめ

列挙体をエラーコードにすると以下のメリットがあります。

  • コードを見るだけでそれが何のエラーかわかる。
  • コードを見るだけでそれがどこで起きたエラーかわかる。
  • コードがあれば対応表が要らない。

いかがでしょうか。
少し興味が沸いたでしょうか。
興味が沸いたならここから先のエラーコードを列挙体にしたときの実装をご覧になるとよいでしょう。

実装

いよいよ実装をしていきます。
ここまでで決まった基本方針はエラーコードを極力列挙体にすることだけです。

勿論エラーというものにはエラーコード以外にもいくつも考えなくてはいけない事柄があります。
まずはエラーコードを列挙体にしてみて、その過程でエラーコード以外の要素に触れながら組み立てていきましょう。

エラーコードを発行する

エラーコードの発行自体はほとんどいままでと変わりません。
文字列や数字で表現していたエラーコードを列挙体で表すようにするだけです。

あまり面白みはないので、列挙体にしたことで大きく変更する余地のあるエラーハンドリングを見てみましょう。

ハンドリング

エラーは種々様々なものが存在しますが、その取扱い方は比較的単純なパターンに収まることが多いです。
具体例を挙げるなら「エラーコードに対応したメッセージを表示する」というパターンは代表的なハンドリングです。

パターン化できるということは「なんとかして」共通化をすれば毎回似たような処理を書いたり、あるいはコピペをするといったことをしなくて済みそうです。
今回はユーザ更新というユースケースをモデルにして処理の共通化を図っていきます。

まずはユーザ更新のレスポンスとそのエラーコードを定義してみます。

public class UserUpdateResponse {
  public UserUpdateError[] Errors { get; set; }
}

public enum UserUpdateError{
  UserNotFound,
  Duplicated,
  InvalidCharactor,
}

このクラスを使ってエラーからメッセージを表示するスクリプトは次のようになります。

var logic = new UserUpdateInteractor();
var request = new UserUpdateRequest("taro", "jiro");
var response = logic.Handle(request);
if(response.Errors.Any()){
  for(var errorCode in response.Errors){
    switch(errorCode) {
      case UserUpdateError.UserNotFound:
        Console.WriteLine("ユーザが存在しません");
        break;
      case UserUpdateError.Duplicated:
        Console.WriteLine("すでに存在しているユーザ名です");
        break;
      case UserUpdateError.InvalidUserName:
        Console.WriteLine("利用できない文字が利用されています");
        break;
      default:
        throw new Exception("What's the code: " + errorCode):
    }
  }
  var errorCode = response.Error.Value;
}else{
  Console.WriteLine("成功しました");
}

ユーザ更新についてはこの程度よいでしょう。
次は「ユーザ削除」のハンドリングをご覧ください。

var logic = new UserDeleteInteractor();
var request = new UserDeleteRequest("taro");
var response = logic.Handle(request);
if(response.Errors.Any()){
  for(var errorCode in response.Errors){
    switch(errorCode) {
      case UserDeleteError.UserNotFound:
        Console.WriteLine("ユーザが存在しません");
        break;
      default:
        throw new Exception("What's the code: " + errorCode):
    }
  }
}else{
  Console.WriteLine("削除処理が成功しました");
}

非常に似通ったコードになります。

この重複はどのユースケースでも必ず起きる重複です。
つまり必然的な重複です。共通化すべきものです。
この部分の共通化を目指します。

共通化をする場合、目標を定めるのはとてもよいプラクティスかと思います。
今回はこんなコードを目標とします。

var logic = new UserUpdateInteractor();
var request = new UserUpdateRequest("taro", "jiro");
var response = logic.Handle(request);
if(response.HasError()){
  Console.WriteLine(response.ToErrorMessage());
}else{
  Console.WriteLine("成功しました");
}
var logic = new UserDeleteInteractor();
var request = new UserDeleteRequest("taro");
var response = logic.Handle(request);
if(response.HasError()){
  Console.WriteLine(response.ToErrorMessage());
else{
  Console.WriteLine("削除処理が成功しました");
}

なかなかシンプルで良い目標に思えます。

この共通化のための第一歩はレスポンスにスーパークラスを用意することでしょう。

public abstract class Response<TErrorCode> {
  protected Response(TErrorCode error)
      : this(new []{ error }){
  }

  protected Response(IEnumerable<TErrorCode> errors)
      : this(errors.Select(x => new ErrorContainer<TErrorCode>(x))){
  }

  protected Response(IEnumerable<ErrorContainer<TErrorCode>> errors){
      Errors = errors.ToArray();
  }

  // エラーがないときに利用するコンストラクタ
  protected Response(){
    Errors = new ErrorContainer<TErrorCode>[]{};
  }

  public ErrorContainer<TErrorCode>[] Errors { get; }

  public bool HasError(){
    return Errors.Any();
  }
}

// エラー情報としてコード以外の情報を含めることもできるようにしておく
public class ErrorContainer<TErrorCode>{
  public ErrorContainer(TErrorCode code, object item = null){
    ErrorCode = code;
    Item = item;
  }

  public TErrorCode ErrorCode { get; }
  public object Item { get; }
}

ErrorContainer というコンテナクラスにエラーコードを含めるようにしているのは、エラーによってはそのエラーが起きた原因をメッセージに表示する必要があるケースに対処するためです(後述)。
このResponseをすべてのレスポンスオブジェクトが継承すれば、少なくともresponse.HasError()までは動きそうです。

var logic = new UserUpdateInteractor();
var request = new UserUpdateRequest("taro", "jiro");
var response = logic.Handle(request);
if(response.HasError()){ // ← ここまでは動きそう
  Console.WriteLine(response.ToErrorMessage()); // ← ここはどうする?
}else{
  Console.WriteLine("成功しました");
}

そうなると次はresponse.ToErrorMessage()の部分への実装方法が気になりますね。
どうやって実現すべきでしょうか。

まずはResponseのメソッドとして用意してみましょう。

public abstract class Response<TErrorCode> {
  // エラーがあるときに利用するコンストラクタ
  protected Response(IEnumerable<ErrorContainer<TErrorCode>> errors){
    Errors = errors.ToArray();
  }

  // エラーがないときに利用するコンストラクタ
  protected Response(){
    Errors = [];
  }

  public ErrorContainer<TErrorCode>[] Errors { get; }

  public bool HasError(){
    return errors.Any();
  }

  public string ToErrorMessage() {
    if (!HasError()) {
      return "";
    }

    foreach (var code in Errors.Select(x => x.ErrorCode)) {
      if (code.GetType() == typeof(UserUpdateError)) {
        var updateErrorCode = (UserUpdateError) (object) code;
        switch (updateErrorCode) {
          case UserUpdateError.UserNotFound: return "ユーザが存在しません";
          case UserUpdateError.Duplicated: return "すでに存在しているユーザ名です";
          case UserUpdateError.InvalidUserName: return "利用できない文字が利用されています";
          default: throw new Exception("What's the code: " + code);
        }
      }
    }

    throw new Exception("Unregistered error code: ");
  }
}

これは大きな問題を抱えたコードです。
スーパークラスがサブクラスの知識を持ってしまうというのがそれですね。
サブクラスの知識とはTErrorCodeUserUpdateErrorという型が指定されるという「知識」です。

例えばエラーコードを追加したとしましょう。
そのたびにResponseにはエラーコードに対応したメッセージを追加する変更を行う必要があります。

例えばUserGetResponseクラスを追加したとしましょう。
そのたびにResponseは新たなクラスが利用するエラーコード用のメッセージを追加する変更を行う必要があります。

これは非常に手間ですし、ともすれば定義をし忘れます。
またコードに変更がある以上Responseクラスがデグレする危険に晒されます。

また細かい点ですが(UserUpdateError) (object) codeというキャストでオートボクシングとアンボクシングが発生するのも少し気になるところです。

一体どのようにすれば納得感のあるToErrorMessageを実現できるのでしょうか。

エラーメッセージの定義箇所

そもそもエラーメッセージはどこに定義するべきでしょうか。

エラーメッセージはエラーコードに密接な関係性を持っているので、なるべくならエラーコードと一緒に定義しておきたいものです。
特にプロジェクトが一つでエラーコードが同一バイナリであれば尚更です。

enum にはフィールドを定義できませんがアトリビュートやアノテーションを利用するという手段であればこれを実現できます。

[AttributeUsage(AttributeTargets.Field)]
public class ErrorMessageAttribute : Attribute {
  public ErrorMessageAttribute(string message){
    Message = message;
  }

  public string Message { get; }
}
public enum UserUpdateError{
  [ErrorMessage("ユーザが存在しません")]
  UserNotFound,

  [ErrorMessage("すでに存在しているユーザ名です")]
  Duplicated,

  [ErrorMessage("利用できない文字が利用されています")]
  InvalidCharactor,
}

これは非常に管理がしやすいです。
メッセージを変えたいといった要望にも気軽に答えることができます。
(多言語化をする場合はメッセージ ID を定義することになります)

アトリビュートを利用した場合のResponseは次のような実装になります。

public abstract class Response<TErrorCode> {
  private static readonly ConcurrentDictionary<Type, Dictionary<object, ErrorMessageAttribute>> cache = new ConcurrentDictionary<Type, Dictionary<object, ErrorMessageAttribute>>();

  // エラーがあるときに利用するコンストラクタ
  protected Response(TErrorCode error)
    : this(new[] {error}) {
  }

  protected Response(IEnumerable<TErrorCode> errors)
    : this(errors.Select(x => new ErrorContainer<TErrorCode>(x))) {
  }

  protected Response(IEnumerable<ErrorContainer<TErrorCode>> errors) {
    Errors = errors.ToArray();
  }

  // エラーがないときに利用するコンストラクタ
  protected Response() {
    Errors = new ErrorContainer<TErrorCode>[] { };
  }

  public ErrorContainer<TErrorCode>[] Errors { get; }

  public bool HasError() {
    return Errors.Any();
  }

  public string ToErrorMessage() {
    if (!HasError()) {
      return "";
    }

    var messages = Errors
      .Select(x => x.ErrorCode)
      .Select(getMessage);

    var message = string.Join(Environment.NewLine, messages);

    return message;
  }

  private string getMessage(TErrorCode code) {
    var errorMessageAttributes = cache.GetOrAdd(code.GetType(), type => {
      var lookup = type.GetFields()
        .Where(x => x.FieldType == type)
        .SelectMany(x => x.GetCustomAttributes(false), (x, attribute) => new {code = x.GetValue(null), attribute})
        .ToLookup(x => x.attribute.GetType());

      var dictionary = lookup[typeof(ErrorMessageAttribute)].ToDictionary(x => x.code, x => (ErrorMessageAttribute) x.attribute);
      return dictionary;
    });

    if (errorMessageAttributes.TryGetValue(code, out var errorMessageAttribute)) {
      return errorMessageAttribute.Message;
    } else {
      return "";
    }
  }
}

少し込み入った処理が記述されていますが、型からアトリビュートを取り出してそこに設定されたメッセージを表示しているだけです。
この段階で以下のスクリプトが実現できています。

var logic = new UserUpdateInteractor();
var request = new UserUpdateRequest("taro", "jiro");
var response = logic.Handle(request);
if(response.HasError()){
  Console.WriteLine(response.ToErrorMessage());
}else{
  Console.WriteLine("成功しました");
}

プレゼンテーションのロジックがだいぶすっきりしてきました。

拡張メソッド

ところでResponseはそもそもレイヤーを超えてデータを渡すための DTO のようなものでした。
その目的は「データを渡すため」です。

データを渡すためのオブジェクトに、プレゼンテーションのためにメッセージを準備するメソッドを定義すべきでしょうか。

メッセージはプレゼンテーションのためのものです。
プレゼンテーションのためのものはプレゼンテーション層のモジュールとして用意すべきでしょう。

プレゼンテーション層のモジュールとして用意する場合は次のようなヘルパークラスを用意します。

public static class ResponseHelper {
  private static readonly ConcurrentDictionary<Type, Dictionary<object, ErrorMessageAttribute>> cache = new ConcurrentDictionary<Type, Dictionary<object, ErrorMessageAttribute>>();

  public static string ToErrorMessage<TErrorCode>(Response<TErrorCode> response) {
    if (!response.HasError()) {
      return "";
    }

    var messageBuilder = new StringBuilder();

    var messages = response.Errors
      .Select(x => x.ErrorCode)
      .Select(getMessage);

    return messages.ToString();
  }

  private static string getMessage<TErrorCode>(TErrorCode code) {
    var errorMessageAttributes = cache.GetOrAdd(code.GetType(), type => {
      var lookup = type.GetFields()
        .Where(x => x.FieldType == type)
        .SelectMany(x => x.GetCustomAttributes(false), (x, attribute) => new {code = x.GetValue(null), attribute})
        .ToLookup(x => x.attribute.GetType());

      var dictionary = lookup[typeof(ErrorMessageAttribute)].ToDictionary(x => x.code, x => (ErrorMessageAttribute) x.attribute);
      return dictionary;
    });

    if (errorMessageAttributes.TryGetValue(code, out var errorMessageAttribute)) {
      return errorMessageAttribute.Message;
    } else {
      return "";
    }
  }
}

これを利用すればすべてのレスポンスのエラーメッセージ簡単なコードで取得することができます。

var message = ResponseHelper.GetErrorMessage(response);

しかし目標としていたresponse.ToErrorMessage()という記述の表現力は魅力的です。

表現力以外にも、インテリセンスを利かせるだけでエラーメッセージを取得するメソッドを呼び出せるというのも魅力です。
Helperの名前が何であったかを思い出す必要がなくなります。

こういった場合には拡張メソッドを利用するのがよいでしょう。
修正は先ほどのヘルパークラスの名前を変えて少し変更を加えるだけです。

public static class ResponseExtensions {
  private static readonly ConcurrentDictionary<Type, Dictionary<object, ErrorMessageAttribute>> cache = new ConcurrentDictionary<Type, Dictionary<object, ErrorMessageAttribute>>();

  public static string ToErrorMessage<TErrorCode>(this Response<TErrorCode> response) { // ← this が追加されました
    if (!response.HasError()) {
      return "";
    }

    var messages = response.Errors
      .Select(x => x.ErrorCode)
      .Select(getMessage);

    return messages.ToString();
  }

  private static string getMessage<TErrorCode>(TErrorCode code) {
    var errorMessageAttributes = cache.GetOrAdd(code.GetType(), type => {
      var lookup = type.GetFields()
        .Where(x => x.FieldType == type)
        .SelectMany(x => x.GetCustomAttributes(false), (x, attribute) => new {code = x.GetValue(null), attribute})
        .ToLookup(x => x.attribute.GetType());

      var dictionary = lookup[typeof(ErrorMessageAttribute)].ToDictionary(x => x.code, x => (ErrorMessageAttribute) x.attribute);
      return dictionary;
    });

    if (errorMessageAttributes.TryGetValue(code, out var errorMessageAttribute)) {
      return errorMessageAttribute.Message;
    } else {
      return "";
    }
  }
}

メッセージの編集

ところで今のエラーメッセージのモジュールは複数のエラーが出現したときに常に改行コードでエラーメッセージ同士を結合してしまいます。
Console に表示するときはとても使いやすいですが、Web 用のメッセージの場合改行は改行コードではなく '<br>' で結合されていた方が都合がよい場合もあるでしょう。
もし今のまま '<br>' で結合したい場合は改行コードを置換する必要があります。

var message = response.ToErrorMessage();
message = message.Replace(Environment.NewLine, "<br>");

一度出来上がったメッセージを再度編集するのは処理的に少し無駄があります。
可能であれば編集も同時にこなしてメッセージを作りたいところです。

これを実現するにはまず拡張メソッドを次のようなコードに改変します。

public static class ResponseExtensions {
  private static ConcurrentDictionary<Type, IErrorMessageProvider> providers = new ConcurrentDictionary<Type, IErrorMessageProvider>();

  public static string ToErrorMessage<TErrorCode>(this Response<TErrorCode> response, IMessageFormatter aFormatter = null) {
    if (!response.HasError()) {
      return "";
    }

    var formatter = aFormatter ?? new NewLineMessageFormatter();
    var provider = getProvider<TErrorCode>();
    var message = formatter.Format(response.Errors, provider);

    return message;
  }

  private static ErrorMessageProvider<TErrorCode> getProvider<TErrorCode>() {
    var type = typeof(TErrorCode);
    var provider = providers.GetOrAdd(type, _ => new ErrorMessageProvider<TErrorCode>());
    return (ErrorMessageProvider<TErrorCode>)provider;
  }
}

MessageProvider 関連のコードは次の通りです。

public interface IErrorMessageProvider {
}

public class ErrorMessageProvider<TErrorCode> : IErrorMessageProvider {
  private readonly Dictionary<TErrorCode, ErrorMessageAttribute> cache;

  public ErrorMessageProvider() {
    var type = typeof(TErrorCode);
    var lookup = type.GetFields()
      .Where(x => x.FieldType == type)
      .SelectMany(x => x.GetCustomAttributes(false), (x, attribute) => new {code = x.GetValue(null), attribute})
      .ToLookup(x => x.attribute.GetType());

    cache = lookup[typeof(ErrorMessageAttribute)].ToDictionary(x => (TErrorCode) x.code, x => (ErrorMessageAttribute) x.attribute);
  }

  public string ErrorMessage(TErrorCode key) {
    if (cache.TryGetValue(key, out var errorMessageAttribute)) {
      return errorMessageAttribute.Message;
    } else {
      return "";
    }
  }
}

最後は MessageFormatter です。

public interface IMessageFormatter {
  string Format<TErrorCode>(IEnumerable<ErrorContainer<TErrorCode>> errors, ErrorMessageProvider<TErrorCode> provider);
}

public class NewLineMessageFormatter : IMessageFormatter {
  public string Format<TErrorCode>(IEnumerable<ErrorContainer<TErrorCode>> errors, ErrorMessageProvider<TErrorCode> provider)
  {
    return string.Join(
      Environment.NewLine,
      errors.Select(e => {
        var errorMessage = provider.ErrorMessage(e.ErrorCode);
        errorMessage = string.Format(errorMessage, e.Item);
        return errorMessage;
      })
    );
  }
}

もし '<br>' で結合したメッセージが欲しい場合は次のような Formetter を用意して引数に渡しましょう。 

public class JoinByWebBrTagMessageFormatter : IMessageFormatter {
  public string Format<TErrorCode>(IEnumerable<ErrorContainer<TErrorCode>> errors, ErrorMessageProvider<TErrorCode> provider) {
    return string.Join(
      "<br>",
      errors.Select(e => {
        var errorMessage = provider.ErrorMessage(e.ErrorCode);
        errorMessage = string.Format(errorMessage, e.Item);
        return errorMessage;
      })
    );
  }
}

ところで ErrorContainer には Item というプロパティがあります。
これは入力値をエラーメッセージに組み込ませたい場合等に使います。
例)「'taro@'というユーザ名には利用できない文字が利用されています」というエラーメッセージ

今のコードであれば次のようにすれば置換することができます。

public enum UserUpdateError {
  [ErrorMessage("ユーザが存在しません")]
  UserNotFound,

  [ErrorMessage("すでに存在しているユーザ名です")]
  Duplicated,

  [ErrorMessage("'{0}'というユーザ名には利用できない文字が利用されています")]
  InvalidUserName,
}

より親切なメッセージになりますね。

より細かなハンドリング

場合によっては特定のエラーコードだけ処理を変えたいこともあると思います。
そういった場合にはErrorsフィールドを見ることで処理を分岐できます。

var logic = new UserUpdateInteractor();
var request = new UserUpdateRequest("taro", "jiro");
var response = logic.Handle(request);
if(response.HasError()){
  if(response.Errors.Any(x => x.ErrorCode == UserUpdateError.Duplicated)){
    // 特別処理
  }else{
    Console.WriteLine(response.ToErrorMessage());
  }
}else{
  Console.WriteLine("成功しました");
}

クリティカルな部分で、きめ細かくユーザさんをフォローしたいときにはこういった処理を書いてあげるとよいでしょう。

まとめ

今回のように列挙体をエラーコードにするとユースケース毎で「何のエラーが起こりうるのか」といった仕様がコード上に見えてきます。
これはビジネスロジックの単体テストを行う上での重要な指針となります(少なくともすべての列挙体のエラーが出るようにする)。

またエラーコードからそれが発生している具体的なコードの箇所も、列挙体が利用されているところを検索すればよいので容易かつ高速です。
対応表とにらめっこする必要がないので目に優しいですし、暗号のようなコードを覚える必要もないので脳にも優しいです。

もし API を叩いて戻ってくるエラーコードが数字や文字列だったとしても、それを受け取った直後に列挙体に変換するとよいでしょう。
それをするだけの価値はあると思います。

エラー設計はシステムのクオリティです。

必要に応じて細かくハンドリングが行えるエラー設計は、ユーザーに優しいシステムです。
エラーコードからそのエラー発生個所がわかるエラー設計は、保守に優しいシステムです。
可能な限りパターン化してすべてのハンドリングが不要なエラー設計は、開発に優しいシステムです。

エラー設計を最大の課題として、優しいシステムを作っていきましょう。

ソース

記事にでてきたコードです。
ロジックについては手抜きですが、Sample プロジェクトを実行して動きを確かめることはできると思います。
https://github.com/nrslib/CompetentResponse

246
220
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
246
220

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?