web-dev-qa-db-ja.com

else if、else if、else ifなどの多くのステートメントのリファクタリング

PDFから読み取り可能なデータを解析しようとしていますが、次のようなコードを書いてしまいます。

if (IsDob(line))
{
    dob = ParseDob(line);
}
else if (IsGender(line))
{
    gender = ParseGender(line);
}
...
...
...
else if (IsRefNumber(line))
{
    refNumber = ParseRefNumber(line);
}
else
{
    unknownLines.Add(line);
}

次に、このすべてのデータを使用して、関連するオブジェクトを構築します。すべての個人データを使用している顧客。

解析することがたくさんあるとき、これは一種の醜いものになる傾向があります。

それらをTryParsePersonalInfo(line)、TryParseHistory(line)などの関数に分割しました。しかし、どこにでも無限のelse ifがあるので、問題が動いているように感じます。

6
Levi H

これは、あなたが提供した情報を踏まえて、私が最初に行うことです。

次のようなインターフェースを作成します。

interface LineProcessor<E> {
  boolean Process(Record record, Line line); 
}

今のところRecordは適切なバッグだとしましょう。これにこだわらないでください。デモンストレーションのために単純にしています。

class Record {
  public Date dob { get; set; }
  public String gender { get; set; }
  public String refNumber { get; set; }
  // ...
}

構文がC#に対して正しくない場合は、申し訳ありません。私が何をしているのかわからない場合は、明確にします。

次に、LineParserのインスタンスのリストを作成します。次に、ループを記述します(python/pseudocode):

for line in pdf:
  obj = process(record, line)

def process(record, line):
  for handler in processors:
    if handler.process(record, line): return
  else:
    unknown.add(line)

これらのいずれかの実装は次のようになります。

class DobProcessor implements LineProcessor {
  boolean process(Record record, Line line) {
    if (IsDob(line)) {
      record.dob = ParseDob(line);
      return true;
    } else {
      return false;
    }
  }

  Date ParseDob(Line line) {
    //...
  }

  boolean IsDob(Line line) {
    //...
  }
}

これにより、コードが管理しやすくなります。大きなifステートメントの代わりに、それぞれがケースに固有である多くのクラスがあります。これにより、コードが整理されるだけでなく、他のケースの周囲のコードに触れることなく、ケースを追加または削除できるようになります。

もう1つは、プロセスインターフェイスが単一のメソッドになっているため、これを実際には関数ポインター/ラムダのようなものと考えることができるため、必要に応じてインターフェイスを省略できます。

10
JimmyJames

デリゲートのリストを使用する

...拡張可能なオブジェクトモデルに含まれる注入可能なクラスにカプセル化されたidemopotent解析関数を呼び出す

ここでは、いくつかのプログラミングの概念を紹介しますので、長い答えに我慢してください。慣れるまで、これは複雑すぎるように見えるかもしれません。しかし、これらのパターンはすべて非常に一般的であり、商用ソフトウェアでは非常に便利です。

DTOを作成する

まず、結果をまとめて保存する場所が必要です。おそらくDTOクラス。この例のようになります。 ToString()オーバーライドを追加したので、クラス名だけでなく、デバッグペインに内容を表示できます。

_public enum Gender
{
    Male, Female
}

public class DocumentMetadata
{
    public DateTime DateOfBirth { get; set; }
    public Gender Gender { get; set; }
    public string RefNum { get; set; }

    public override string ToString()
    {
        return string.Format("DocumentMetadata: DOB={0:yyyy-MM-dd}, Gender={1}, RefNum={2}", DateOfBirth, Gender, RefNum);
    }
}
_

パターンに従うパーサーメソッドのデリゲートを定義する

これでDTOが作成されたので、行を解析する方法について考えることができます。理想的には、ユニットテストを簡単に実行できる一連のべき等関数が必要です。そして、それらを繰り返し処理するために、それらが何らかの形で類似している場合に役立ちます。したがって、デリゲートを定義します。

_public delegate bool Parser(string line, DocumentMetadata dto);
_

したがって、次のようなパーサーメソッドを記述できます。

_protected bool ParseDateOfBirth(string line, DocumentMetadata dto)
{
    if (!line.StartsWith("DOB:")) return false;
    dto.DateOfBirth = DateTime.Parse(line.Substring(4));
    return true;
}
_

任意の数のパーサーメソッドを記述でき、ブール値を返し、文字列とDTOオブジェクトを引数として受け入れる限り、これらはすべてデリゲートに一致するため、次のようにすべてリストに入れることができます。

_List<Parser>  parsers = new List<Parser>
{
    ParseDateOfBirth,
    ParseGender,
    ParseRefNum
};
_

この機能は、パーサークラスを作成するときに使用します。

基本パーサークラスを作成する

ここで、もう少し心配することがあります。

  1. パーサーメソッドを単一のコードユニットに含める必要があります。クラス。
  2. クラスを注入可能にする必要があります。将来、複数の種類のパーサーが必要になる場合に備えて、単体テストのためにそれをスタブ化することができます。
  3. 反復するパーサーのロジックが共通であることを望みます。

したがって、最初にインターフェースを定義します。

_public interface IDocumentParser
{
    DocumentMetadata Parse(IEnumerable<string> input);
}
_

抽象基本パーサー:

_public abstract class BaseParser : IDocumentParser
{
    protected abstract List<Parser> GetParsers();

    public virtual DocumentMetadata Parse(IEnumerable<string> input)
    {
        var parsers = this.GetParsers();
        var instance = new DocumentMetadata();

        foreach (var line in input)
        {
            foreach (var parser in parsers)
            {
                parser(line, instance);  //This is the line that does it all!!!
            }
        }
        return instance;
    }       
}
_

または、Parse関数をもう少し賢くしたい場合(および正常に解析された行を数える場合):

_    public virtual DocumentMetadata Parse(IEnumerable<string> input)
    {
        var parsers = this.GetParsers();
        var instance = new DocumentMetadata();

        var successCount = input.Sum( line => parsers.Count( parser => parser(line, instance) ));

        Console.WriteLine("{0} lines successfully parsed.", successCount);

        return instance;
    }
_

LINQソリューションはより「巧妙」ですが、ネストされたループは意図をより明確に伝えます。ここで判断の呼び出し。成功した行を数えることができ、その情報を使用してドキュメントを検証できるため、LINQバージョンが好きです。

パーサーを実装する

これで、ドキュメントを解析するための基本的なフレームワークが完成しました。必要なのは、GetParsersを実装して、機能するメソッドのリストを返すことだけです。

_public class DocumentParser : BaseParser
{
    protected override List<Parser> GetParsers()
    {
        return new List<Parser>
        {
            ParseDateOfBirth,
            ParseGender,
            ParseRefNum
        };
    }

    private bool ParseDateOfBirth(string line, DocumentMetadata dto)
    {
       ///Implementation
    }

    private bool ParseGender(string line, DocumentMetadata dto)
    {
       ///Implementation        
    }

    private bool ParseRefNum(string line, DocumentMetadata dto)
    {
       ///Implementation
    }
}
_

最終的な実装では、ドキュメント固有のロジックのみが保持されていることに注意してください。そして、GetParsers()を介してデリゲートを提供するだけです。すべての一般的なロジックは、再利用できる基本クラスにあります。

テスト

これで、数行のコードでドキュメントを解析できます。

_var parser = new DocumentParser();
var doc = parse.Parse(input);
_

しかし、これを挿入したいので、適切に書きましょう:

_public class Application
{
    protected readonly IDocumentParser _parser; // injected

    public Application(IDocumentParser parser)
    {
        _parser = parser;
    }

    public void Run()
    {
        var input = new string[]
        {
            "DOB:1/1/2018",
            "Sex:Male",
            "RefNum:1234"
        };

        var result = _parser.Parse(input);

        Console.WriteLine(result);
    }
}

public class Program
{
    public static void Main()
    {
        var application = new Application(new DocumentParser());
        application.Run();
    }
}
_

出力:

_3 lines successfully parsed.
DocumentMetadata: DOB=2018-01-01, Gender=Male, RefNum=1234
_

これで、次のすべてが揃いました。

  1. すべてのパーサーを反復するための汎用ロジック
  2. 新しいパーサーを導入できる拡張可能なオブジェクトモデル
  3. 注射可能なインターフェース
  4. 複雑な処理を行うべき等能力、単体テスト可能なメソッド
  5. 成功した解析操作をカウントする機能(たとえば、ドキュメントが有効であることを確認するために使用できます)
  6. 結果のデータをカプセル化するクラス

DotNetFiddleの使用例

2
John Wu

あなたの質問への答え

よりC#風の解決策は、デリゲートを利用することです。

delegate bool TryParseHandler(string line);

private readonly TryParseHandler[] _handlers = new[]
{
    TryParseDob,
    TryParseGender,
    TryParseRefNumber,
    //...
}
bool TryParseDob(string line)
{
    if(!IsDob(line)) return false;
    dob = ParseDob(line);
    return true;
}
//etc
void ProcessLine(string line)
{
    foreach(var handler in _handlers)
    {
        if(handler(line)) return;
    }
}

あなたの問題への答え

正解は、「is」ソリューションを完全に捨てることです。 「ライン」はどのようなものですか?定期的ですか?キーワードは含まれていますか?たとえば、dob: 1/1/1990gender: femaleref: 123456のように見えますか?もしそうなら、あなたはそれらのキーワードを引き出して、それをあなたの検索に使いたいです:

private readonly IReadOnlyDictionary<string, Action<string>> _setValueLookup = new Dictionary<string, Action<string>>
{
    ["dob"] = s => dob = DateTime.Parse(s),
    ["gender"] = s => gender = ParseGender(s),
    ["ref"] = s => refString = s,
};
void ProcessLine(string line)
{
    var pair = line.Split(new []{':'}, 2);
    if(pair.Length < 2) 
    {
        unknownLines.Add(line);
        return;
    }

    var key = pair[0];
    var value = pair[1];
    if(!_setValueLookup.TryGetValue(key, out Action<string> callback))
    {
        unknownLines.Add(line);
        return;
    }

    callback(value);
}
1
Kevin Fee