プログラムの編成

ヒント

ソフトウェアの開発は初めてですか? 最初に、 作業の開始 に関するチュートリアルから始めます。 プロジェクトの成長に合わせて、プログラムの編成について自然に学習します。

別の言語で経験がありますか? Visual Studio のソリューションとプロジェクトに慣れている場合、または Maven や Cargo などのシステムを構築する場合は、これらの概念を .NET にマップします。

C# アプリケーションが拡大するにつれて、コードを整理する必要があります。 .NET には、大規模なコードベースを管理しやすくするために連携する、組織のツール (ソリューション、プロジェクト、アセンブリ、名前空間、型) の階層が用意されています。 ここで説明する規則は、.NET コミュニティでの広範なコンセンサスを表しています。 特定の理由から逸脱する可能性がありますが、これらの規則に従うと、コードは他の .NET 開発者にとってなじみがあり、ナビゲートしやすくなります。

組織階層

最も広範なものから最も具体的なものまで、一般的な .NET アプリケーションをレイヤーで整理します。

レベル 説明
Solution 関連するプロジェクトをグループ化するコンテナー。 MyApp.slnx
プロジェクト 1 つのアセンブリを生成するビルド ユニット。 MyApp.Web.csproj
アセンブリ プロジェクトによって生成されたコンパイル済みの .dll または .exe MyApp.Web.dll
名前空間 型の論理的なグループ化。 MyApp.Web.Controllers
タイプ クラス、構造体、インターフェイス、列挙型、またはデリゲート。 OrderController

各レベルは異なる目的を果たします。 ソリューションによって開発ワークフローが整理されます。 プロジェクトはコンパイルされる内容を定義し、各プロジェクトは 1 つのアセンブリを生成します。 アセンブリは、展開とバージョン管理の単位です。 名前空間は名前の競合を防ぎ、型を見つけやすくします。 1つのアセンブリは複数の名前空間を含むことができ、1つの名前空間は複数のアセンブリにまたがることができます。 型は、実際の動作とデータを定義します。

プロジェクトとアセンブリ

各プロジェクトは、クラス ライブラリまたは実行可能ファイルという 1 つのアセンブリにコンパイルされます。 小規模なアプリケーション用の 1 つのプロジェクトから開始します。途中で分割しないでください。 別のプロジェクトを作成する主な理由は、そのコードを複数のアプリケーションで再利用することです。 再利用の他に、具体的な理由がある場合はプロジェクトを追加します。

  • アプリケーション間でコードを共有する - 複数のアプリが参照するクラス ライブラリに共有ロジックを抽出します。
  • 個別の懸念事項 — データ アクセス、ビジネス ロジック、プレゼンテーション レイヤーを独立させておく。
  • 依存関係を制御 する — プロジェクトは、明示的に参照するプロジェクトからのみ型を使用できます。

1 つのプロジェクトは、多くのアプリケーションに適しています。 「念の場合に備えて」別のプロジェクトを作成する促しに抵抗します。2 つ目のアプリケーションで同じコードが必要な場合は、後でいつでもライブラリを抽出できます。

名前空間をフォルダー構造に一致させる

名前空間名は、プロジェクトのフォルダー構造に従う必要があります。 名前空間の MyApp.Services.Paymentsが表示されたら、その名前空間で定義されている型のソース コードを Services/Payments フォルダーで確認する必要があります。 .NET SDK はこの規則をサポートしており、それに違反すると他の開発者を積極的に混乱させるほど広く従っています。

// File: Services/OrderService.cs
// Namespace mirrors the folder path
using MyApp.Core;

namespace MyApp.Services;

public class OrderService
{
    public Order CreateOrder(string product, int quantity, decimal price) =>
        new() { ProductName = product, Quantity = quantity, UnitPrice = price };

    public string FormatSummary(Order order) =>
        $"{order.Quantity}x {order.ProductName} = {order.Total:C}";
}

ルート名前空間は、プロジェクト ファイルの名前に自動的に設定されます。 サブフォルダー内の型では、サブ名前空間は自動的に取得されません。各ファイルで名前空間を明示的に宣言しますが、常に同期を保ちます。

ヒント

プロジェクト ファイルで <RootNamespace> を設定することで、ルート名前空間を変更できます。

<Project Sdk="Microsoft.NET.Sdk">
 <PropertyGroup>
   <RootNamespace>MyCompany.MyApp</RootNamespace>
 </PropertyGroup>
</Project>

型の種類ではなく、機能別に名前空間を整理する

関連する型を機能または責任別に名前空間にグループ化します。 インターフェイス、その実装、サポート型を一緒に配置します。

// Good: group by feature
namespace MyApp.Payments;

public interface IPaymentProcessor
{
    bool ProcessPayment(decimal amount);
}

public class CreditCardProcessor : IPaymentProcessor
{
    public bool ProcessPayment(decimal amount)
    {
        Console.WriteLine($"Processing credit card payment of {amount:C}");
        return true;
    }
}

public record PaymentResult(bool Success, string? TransactionId);

機能ベースの組織では、必要なものがすべて 1 か所に保持されるため、コードの移動と推論が容易になります。

アクセス修飾子とアセンブリ

アクセス修飾子は、プロジェクトとアセンブリ構造と連携してアクセシビリティを制御します。

  • public — このアセンブリを参照するすべてのアセンブリからアクセスできます。
  • internal — 同じアセンブリ内でのみアクセスできます (最上位レベルの型の既定値)。
  • privateprotectedprivate protectedprotected internal - 包含型、アセンブリ、または派生型に基づいてアクセスできます。

他のプロジェクトで必要とされない型には、既定でinternalを使用します。 この方法では、実装の詳細が非表示になり、コンシューマーを壊すことなくリファクタリングする自由が得られます。 共有ライブラリでは特に重要です。

namespace MyApp.Inventory;

// Public — other projects can use this type
public class InventoryService
{
    public int GetStockLevel(string productName) =>
        StockDatabase.Lookup(productName);
}

// Internal — only visible within this assembly
internal static class StockDatabase
{
    private static readonly Dictionary<string, int> _stock = new()
    {
        ["Widget"] = 42,
        ["Gadget"] = 17
    };

    internal static int Lookup(string productName) =>
        _stock.GetValueOrDefault(productName);
}
  • 名前空間に一貫した名前を付けます。 名前付けパターンとして CompanyName.ProductName.Feature を使用します。 たとえば、 Contoso.Inventory.Shippingを使用します。 一貫性のある名前付けは、開発者が検索せずに型を見つけるのに役立ちます。
  • プロジェクトのフォーカスを維持します。 各プロジェクトには、明確な責任が 1 つ必要です。 プロジェクトが関連のない懸念事項を処理しすぎる場合は、分割します。
  • ファイル スコープの名前空間を使用します。 namespace MyApp.Services;構文はインデントを減らし、推奨されるスタイルです。 すべての新しいコードで使用します。
  • 既定値は internal です。 他のアセンブリで本当に必要な場合にのみ public 型をマークします。 後でいつでもアクセスを拡大できます。縮小は破壊的変更です。