構成された正規表現

2009年7月24日

保守可能なコードを書く上で最も強力なツールの1つは、大きなメソッドを分かりやすい名前の小さなメソッドに分割することです。これは、Kent Beckが「合成メソッドパターン」と呼んでいる手法です。

詳細を理解し、それらの詳細をより高いレベルの構造にまとめることができれば、プログラムをはるかに迅速かつ正確に読むことができます。

-- Kent Beck

メソッドで有効なことは、多くの場合、他のことにも有効です。私が何度か遭遇した、人々がこれを実行できない分野の1つは、正規表現です。

ホテルチェーンの頻繁な宿泊客ポイントの獲得に関するルールが記載されたファイルがあるとします。すべてのルールは次のようになります。

score 400 for 2 nights at Minas Tirith Airport
  

各行からポイント(400)、宿泊日数(2)、ホテル名(Minas Tirith Airport)を抽出する必要があります。

これは正規表現を使うことで明らかに解決できるタスクであり、今まさに「ああ、そうです、これには〜が必要だ」と考えていることでしょう。

const string pattern = 
  @"^score\s+(\d+)\s+for\s+(\d+)\s+nights?\s+at\s+(.*)";

すると、3つの値はグループから簡単に取り出せます。

その正規表現の動作と正しさについて、あなたがどれくらい理解しているかは分かりません。私と同じであれば、このような正規表現を見て、それが何を言っているかを注意深く理解する必要があります。私はしばしば括弧を数えて、グループの位置を把握しようとします(この場合それほど難しくはありませんが、もっと難しいものもたくさん見てきました)。

正規表現をコメントすることを推奨するアドバイスを読んだことがあるかもしれません。(正規表現に変換する際に、しばしばスイッチが必要になります。)このように書くことができます。

    protected override string GetPattern() {
      const string pattern =
        @"^score
        \s+  
        (\d+)          # points
        \s+
        for
        \s+
        (\d+)          # number of nights
        \s+
        night
        s?             #optional plural
        \s+
        at
        \s+
        (.*)           # hotel name
        ";
  
      return pattern;
    }
  }
  

これは理解しやすくなりますが、コメントでは決して満足できません。時々、コメントは良くない、使うべきではないと言うことで非難されたことがあります。これは、両方とも間違っています。コメントが悪いわけではありませんが、より良い選択肢がしばしばあります。私は常に、良い命名と構造によって、コメントを必要としないコードを書こうとしています。(常に成功するわけではありませんが、そうでない場合よりも頻繁に成功できていると感じています)。

正規表現の構造化を試みない人が多いですが、私はそれが有用だと考えています。これがその1つの方法です。

    const string scoreKeyword = @"^score\s+";
    const string numberOfPoints = @"(\d+)";
    const string forKeyword = @"\s+for\s+";
    const string numberOfNights = @"(\d+)";
    const string nightsAtKeyword = @"\s+nights?\s+at\s+";
    const string hotelName = @"(.*)";

    const string pattern =  scoreKeyword + numberOfPoints +
      forKeyword + numberOfNights + nightsAtKeyword + hotelName;
  

パターンを論理的なチャンクに分割し、最後にそれらを再び結合しました。これで、最終的な式を見て式の基本的なチャンクを理解し、各々の正規表現の詳細を確認することができます。

実際の正規表現をトークンのように見せるために、空白を分離しようとする別の方法を示します。

    const string space = @"\s+";
    const string start = "^";
    const string numberOfPoints = @"(\d+)";
    const string numberOfNights = @"(\d+)";
    const string nightsAtKeyword = @"nights?\s+at";
    const string hotelName = @"(.*)";

    const string pattern =  start + "score" + space + numberOfPoints + space +
      "for" + space + numberOfNights + space + nightsAtKeyword + 
       space + hotelName;
  

これにより、個々のトークンが少し明確になりますが、それらの空白変数によって全体的な構造が分かりにくくなります。そのため、前の方法の方が好きです。

しかし、これは疑問を投げかけます。すべての要素はスペースで区切られており、パターンに多くのスペース変数や\s+を入れるのは冗長に感じます。正規表現を部分文字列に分割することの良い点は、特定の目的に合った抽象化をプログラミングロジックを使って考案できることです。部分文字列を取り、それらを空白で結合するメソッドを書くことができます。

    private String composePattern(params String[] arg) {
      return "^" + String.Join(@"\s+", arg);
    }
  

このメソッドを使用すると、次のようになります。

    const string numberOfPoints = @"(\d+)";
    const string numberOfNights = @"(\d+)";
    const string hotelName = @"(.*)";

    const string pattern =  composePattern("score", numberOfPoints, 
      "for", numberOfNights, "nights?", "at", hotelName);
  

これらの代替案を正確に使うことはないかもしれませんが、正規表現をより分かりやすくする方法について考えることを強くお勧めします。コードは理解する必要はなく、読むだけで済むはずです。


更新

この議論では、構成された正規表現の要素をローカル変数にしました。バリエーションとして、一般的に使用される正規表現要素を取り、より広く使用することです。これは、多くの場所で必要な共通の正規表現を使用するのに便利です。同僚のCarlos Villelaは、これらの断片が適切に形成されていない場合(別の断片で閉じられている開き括弧がある場合など)に注意すべき点があるとコメントしています。これはデバッグが難しい場合があります。私はその必要性を感じたことがないので、この問題に遭遇したことはありません。

正規表現のより読みやすい代替案として、Fluentインターフェース(内部DSL)を使用することを提案する人が数人いました。これは別のことだと考えています。正規表現は小さければ問題ありません。実際、同等のFluentインターフェースよりも小さな正規表現の方が好きです。重要なのは合成であり、どちらの手法でも行うことができます。

名前付きキャプチャグループについて言及した人もいました。コメントと同様に、これらは生の正規表現よりも優れていると思いますが、それでも構成された構造の方が読みやすいと思います。構成のポイントは、全体的な正規表現を理解しやすい小さな部分に分割することです。

2014年7月31日に再投稿