zukucode
主にWEB関連の情報を技術メモとして発信しています。

C# 設計ルールをレビューだけに頼らず自動チェックする

「この値は直接書き換えない」「この項目はデータベースへ保存する処理からだけ使う」といったルールを、開発時に決めることがあります。

ルールをドキュメントへ書くことは大切ですが、ファイルが増えると、すべての変更で守られているかを人だけで確認する負担も大きくなります。

今回は、C#のプロジェクトで実際に使われている自動チェックを題材に、機械で判定できるルールを、レビューだけに頼らない形にする方法を紹介します。

初心者向けに、まずC#標準の機能でできることから説明します。独自の解析ツールを一から作る手順ではなく、何を自動化し、何を人が確認するかを考える記事です。

ルールを知っていることと、守れることは別

たとえば、0以上の金額だけを扱うクラスを作ったとします。

書き換え可能な例
using System;

public sealed class OrderAmount
{
    public decimal Value { get; set; }

    public OrderAmount(decimal value)
    {
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(value));
        }

        Value = value;
    }
}

作成時には負の値を拒否しますが、作成後は次のように書き換えられます。

作成時の検証を通らない変更
var amount = new OrderAmount(1000m);
amount.Value = -1m;

「必ず作成時のチェックを通す」と決めていても、公開されたsetがあれば別の経路で変更できます。ルールの文章と、コードで許される操作が一致していません。

まずは言語の機能で書けない形にする

この例では、setをなくすだけで外からの代入を防げます。

OrderAmount.cs
using System;

public sealed class OrderAmount
{
    public decimal Value { get; }

    public OrderAmount(decimal value)
    {
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(value));
        }

        Value = value;
    }
}

Valueを取得することはできますが、外からamount.Value = -1mと代入するとコンパイルエラーになります。別の金額が必要なら、新しいOrderAmountを作り、同じチェックを通します。

このように、単純なルールなら独自ツールを作らずに表現できます。

守りたいこと最初に検討する方法
外から値を代入させないsetterを公開しない
クラスの内部だけで使いたいprivateにする
別アセンブリから使わせたくないinternalにする
不正な値で作らせたくない作成時に検証する

アセンブリは、C#のプロジェクトをビルドしてできるDLLなどの単位です。internalは同じフォルダだけに制限する機能ではない点に注意します。

ただし、この方法でも、後から誰かがsetを追加する変更そのものは禁止できません。「この種類のクラスにはsetterを追加しない」というルールまで守りたい場合は、別のチェックが必要です。

複数のクラスに共通するルールをチェックする

題材の実装には、金額や名前などの値を表す特定のクラス群について、公開setterなどを検出する独自のアナライザーがあります。

値そのものを表し、作成後に書き換えない設計のオブジェクトを、ここでは値オブジェクトと呼びます。

アナライザーは、ソースコードを調べ、ルールに合わない箇所を報告する仕組みです。C#ではRoslynというコンパイラー基盤のAPIを使って作れます。警告やエラーとして報告できることは、Microsoft Learnのコード分析の説明でも紹介されています。

題材の実装では、値オブジェクト用の特定のinterfaceを実装した型を対象にしています。プロジェクト内のすべてのクラスへ同じ禁止事項を当てはめているわけではありません。

検出対象には公開setterに加えてinitも含まれています。initは作成時だけ代入できる機能ですが、コンストラクターなどに集約した検証とは別の経路で値を設定できるため、この設計では許可しない扱いです。

これは、このプロジェクトの値オブジェクトに対するルールです。DTOなどのデータ受け渡し用クラスまで、setやinitを禁止する必要があるとは限りません。

上のOrderAmountは、getterだけにする効果を示す簡略例です。独自アナライザーの対象を示すinterfaceや、アナライザー本体は含めていません。

このサンプルへ公開setterを追加しても、C#標準のコンパイラーが設計違反として検出するわけではありません。

保存処理だけで使う項目も対象にする

題材の実装には、データベースへの保存用の項目を、許可された場所の外から参照していないか確認するアナライザーもあります。

たとえば、テーブル同士の関係を保存するためだけに必要な値を、画面の業務判断で使い始めると、テーブル構造を変えたときに画面の処理まで影響を受ける可能性があります。

この実装では、保存専用であることを示す属性や型の情報を調べ、参照を許可する属性も確認しています。属性は、クラスやメンバーへ付ける追加情報です。

ここで参考になるのは、「利用禁止」とコメントするだけでなく、どの項目を、どこから使えるかをコードで判定できる形にしていることです。

ただし、公開範囲の変更だけで制限できるなら、そちらの方が簡単です。同じアセンブリ内でも役割ごとに参照を制限したいなど、言語標準の公開範囲だけでは表しにくい場合に、追加のチェックを検討します。

文字列検索ではなく、型やメンバーを調べる理由

「ファイルにpublic setがあればエラー」という単純な文字列検索では、コメントに書いただけでも検出したり、書式の違いで見逃したりする可能性があります。

題材のアナライザーは、プロパティ、setterの公開範囲、実装しているinterfaceなど、コンパイラーが把握した情報を使って判定しています。

そのため、「どんな文字列があるか」よりも、「どの種類のクラスに、どんな操作を公開しているか」をルールにできます。

一方で、型の判定方法にも確認が必要です。名前だけで対象を判定するのか、名前空間まで区別するのかによって、同名の別の型を誤って対象にする可能性が変わります。独自チェックも、実装して終わりではありません。

ローカルとビルドで同じルールを使う

開発者のエディターだけに設定すると、環境によって検出できたり、できなかったりします。

題材の実装では、共通のビルド設定から独自アナライザーのプロジェクトを参照しています。また、紹介したルールの既定の重大度はエラーに設定されています。

このようにリポジトリ側で設定すると、各自が手動で導入する作業を減らせます。ただし、CIでも実際に対象プロジェクトをビルドし、解析が無効化されていないことを確認する必要があります。

既存のアナライザーの報告レベルを調整する場合は、.editorconfigを使えます。次のAPP0001は、独自ルールがすでに実装・導入されていると仮定した例です。

.editorconfigの設定例
[*.cs]
dotnet_diagnostic.APP0001.severity = warning

この設定だけで新しいルールを作れるわけではありません。warningは警告、errorはエラーとして扱う指定です。設定の詳細は、Microsoft Learnのアナライザールールのカスタマイズを参照してください。

既存コードへ導入する場合は、まず警告で対象を確認し、誤検出や必要な例外を整理してからエラーへ変更する方法もあります。これは導入時の進め方の例で、題材のプロジェクトがこの順序で導入されたという意味ではありません。

自動チェック自体の動作も確認する

間違ったコードを検出できても、正しいコードを大量に止めるルールでは使い続けにくくなります。

導入時には、違反する例と、違反しない例を対にして確認します。

確認例期待する結果
対象の値オブジェクトへ公開setterを追加する検出する
対象の値オブジェクトをgetterだけにするこのルールでは検出しない
対象外のDTOへsetterを書く値オブジェクト用ルールでは検出しない
保存専用の項目を許可された場所から参照する検出しない
同じ項目を許可されていない場所から参照する検出する
コメントに違反例を書くコードとして検出しない

これらは導入時の確認項目で、題材のすべてのアナライザーを検証済みとするものではありません。

例外を認める場合は、理由がわかる狭い範囲に限定します。対処が難しいたびにプロジェクト全体でルールを無効にすると、以降の違反も見えなくなります。

自動化しない方がよい判断もある

「このクラスは読みやすいか」「この分割は業務に合っているか」は、単純な禁止ルールにはしにくい判断です。

自動チェックに向いていること人のレビューで確認すること
対象の型に公開setterがあるかその型を値オブジェクトにするべきか
禁止したメンバーを参照しているかどこまで保存処理へ閉じ込めるか
必須の属性が付いているか属性に書いた内容が実際の動作に合っているか

独自アナライザーには作成と保守の費用がかかります。まずは言語の機能や既存の解析ルールで表現できないか確認し、繰り返し指摘している、判定基準の明確なルールから対象にします。

自動チェックが減らせるのは、決めたルールを毎回探して確認する作業です。レビューでは、そのルールが今の設計に適しているかや、業務の動作が正しいかへ時間を使えるようにすることを目指します。


関連記事