通过


程序组织

小窍门

开发软件的新手? 首先开始 学习入门 教程。 随着项目的增长,你将自然地了解程序组织。

是否在其他语言中有经验? 如果你熟悉 Visual Studio 中的解决方案和项目,或者生成 Maven 或 Cargo 等系统,本文将这些概念映射到 .NET。

随着 C# 应用程序的发展,你需要组织代码。 .NET 提供组织工具(解决方案、项目、程序集、命名空间和类型)的层次结构,可协同工作,使大型代码库可管理。 此处所述的约定代表了 .NET 社区的广泛共识。 可能会因特定原因而偏离,但遵循这些约定可使代码更易于其他 .NET 开发人员理解和浏览。

组织层次结构

在层中组织典型的 .NET 应用程序,从最广泛到最具体:

级别 说明 示例
解决方案 对相关项目进行分组的容器。 MyApp.slnx
项目 用于产生一个程序集的构建单元。 MyApp.Web.csproj
Assembly 项目生成的编译 .dll.exe MyApp.Web.dll
命名空间 类型的逻辑分组。 MyApp.Web.Controllers
类型 类、结构、接口、枚举或委托。 OrderController

每个级别都有不同的用途。 解决方案组织开发工作流。 项目定义一起编译的内容,每个项目生成一个程序集。 程序集是部署和版本控制单元。 命名空间可防止命名冲突,并使类型易于查找。 单个程序集可以包含多个命名空间,单个命名空间可以跨越多个程序集。 类型定义实际行为和数据。

项目和程序集

每个项目编译为单个程序集:类库或可执行文件。 从单个项目开始,适用于小型应用程序 - 不要过早拆分。 创建单独的项目的主要原因是在多个应用程序中重复使用该代码。 除了重复使用之外,在有具体原因时添加项目:

  • 跨应用程序共享代码 — 将共享逻辑提取到多个应用引用的类库中。
  • 关注点分离 - 使数据访问、业务逻辑和呈现层保持独立。
  • 控制依赖项 - 项目只能使用它显式引用的项目的类型。

单个项目适用于许多应用程序。 抵制“以防万一”创建单独的项目的冲动。以后,当第二个应用程序需要相同的代码时,始终可以提取库。

将命名空间与文件夹结构匹配

命名空间名称应遵循项目的文件夹结构。 当你看到命名空间 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);

基于功能的组织将所需的一切放在一个位置,使代码更易于导航和推理。

访问修饰符和程序集

访问修饰符与项目和程序集结构配合使用,以控制访问权限:

对于其他项目不需要的类型,默认为 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。 一致的命名有助于开发人员在不搜索的情况下查找类型。
  • 保持项目的专注度。 每个项目都应有一个明确的责任。 当项目处理太多不相关的问题时,请将其拆分。
  • 使用文件范围的命名空间。 namespace MyApp.Services; 语法减少了缩进,是推荐的样式。 在所有新代码中使用它。
  • 默认为 internal 仅当其他程序集真正需要它们时标记类型 public 。 以后可以随时扩大访问权限;缩小访问权限是一项破坏性的变更。