编码遵循五大设计原则创建出更加健壮、可维护和可扩展的软件系统

一、单一职责原则(SRP) 

  * 定义:一个类应该只有一个引起它变化的原因。

  * 解释:意味着一个类应该专注于做一件事情,当需求发生变化时,只影响到一个类。这有助于降低类间的耦合,使得代码更易于理解和维护。

​​​​​

示例场景:图书管理系统

假设我们正在设计一个图书管理系统的后端逻辑。在这个系统中,我们需要处理图书的添加、删除、查询等功能,同时也需要记录图书的借阅信息。

* 未按单一职责原则的示例:

在这个实现中,BookManager 类不仅负责图书的管理(添加、删除、查询),还负责图书借阅的记录。这意味着如果有需求变更,比如需要改进图书借阅的逻辑,那么这个类就需要改变,同时图书的基本管理功能也可能受到影响。

public class BookManager
{
    private List<Book> books;
    private Dictionary<int, BorrowRecord> borrowRecords;

    public BookManager()
    {
        books = new List<Book>();
        borrowRecords = new Dictionary<int, BorrowRecord>();
    }

    public void AddBook(Book book)
    {
        books.Add(book);
    }

    public void RemoveBook(int bookId)
    {
        books.RemoveAll(b => b.Id == bookId);
        borrowRecords.Remove(bookId);
    }

    public Book GetBookById(int bookId)
    {
        return books.FirstOrDefault(b => b.Id == bookId);
    }

    public void BorrowBook(int bookId, int userId)
    {
        var book = GetBookById(bookId);
        if (book != null && !borrowRecords.ContainsKey(bookId))
        {
            borrowRecords.Add(bookId, new BorrowRecord(bookId, userId));
        }
    }

    public void ReturnBook(int bookId)
    {
        borrowRecords.Remove(bookId);
    }
}
* 按单一职责原则的示例:

将归还和借阅的方法分解出来,单独成一个类

public class BookRepository
{
    private List<Book> books;

    public BookRepository()
    {
        books = new List<Book>();
    }

    public void AddBook(Book book)
    {
        books.Add(book);
    }

    public void RemoveBook(int bookId)
    {
        books.RemoveAll(b => b.Id == bookId);
    }

    public Book GetBookById(int bookId)
    {
        return books.FirstOrDefault(b => b.Id == bookId);
    }
}

public class BorrowService
{
    private Dictionary<int, BorrowRecord> borrowRecords;

    public BorrowService()
    {
        borrowRecords = new Dictionary<int, BorrowRecord>();
    }

    public void BorrowBook(int bookId, int userId)
    {
        if (!borrowRecords.ContainsKey(bookId))
        {
            borrowRecords.Add(bookId, new BorrowRecord(bookId, userId));
        }
    }

    public void ReturnBook(int bookId)
    {
        borrowRecords.Remove(bookId);
    }
}

  二、开放封闭原则(OCP)

     * 定义:软件实体(类、模块、函数等)应该是可扩展的,但是不可修改的。

     * 解释:当系统的需求发生变化时,我们应该能够通过增加新的代码来扩展原有的功能,而不是修改已有的代码。这有助于保持系统的稳定性,减少因为修改现有代码带来的风险。

         

示例场景:计算不同类型的订单折扣

假设我们正在开发一个电子商务平台,需要为不同类型的订单计算折扣。最初,我们的系统只支持两种类型的订单:标准订单和批量订单,其中批量订单可以享受额外的折扣。

* 未遵循开放封闭原则的示例:
public enum OrderType
{
    Standard,
    Bulk
}

public class OrderDiscountCalculator
{
    public decimal CalculateDiscount(Order order)
    {
        if (order.Type == OrderType.Standard)
        {
            return order.Total * 0.95m; // 标准订单95%的折扣
        }
        else if (order.Type == OrderType.Bulk)
        {
            return order.Total * 0.90m; // 批量订单90%的折扣
        }
        return order.Total;
    }
}

在这个实现中,OrderDiscountCalculator 类直接在 CalculateDiscount 方法中根据订单类型计算折扣。如果未来需要添加新的订单类型,比如会员订单,我们需要修改这个方法,这违反了OCP。

* 遵循开放闭合原则示例:
// 折扣策略接口
public interface IDiscountStrategy
{
    decimal CalculateDiscount(Order order);
}

// 标准订单折扣策略
public class StandardOrderDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order)
    {
        return order.Total * 0.95m;
    }
}

// 批量订单折扣策略
public class BulkOrderDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(Order order)
    {
        return order.Total * 0.90m;
    }
}

// 订单折扣计算器
public class OrderDiscountCalculator
{
    private readonly IDiscountStrategy _discountStrategy;

    public OrderDiscountCalculator(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }

    public decimal CalculateDiscount(Order order)
    {
        return _discountStrategy.CalculateDiscount(order);
    }
}

为了遵循OCP,我们可以使用策略模式,将折扣的计算逻辑封装到独立的策略类中,然后在 OrderDiscountCalculator 中使用这些策略。

现在,如果我们需要添加新的订单类型(比如会员订单),我们只需要实现 IDiscountStrategy 接口并创建一个新的策略类,然后在应用中适当地使用它,而不需要修改现有的 OrderDiscountCalculator 类。这样,我们的系统就对扩展开放,对修改封闭了。

为了在应用中使用这些策略,你可以使用依赖注入框架,根据订单类型动态地注入正确的策略实例。这种方式提高了代码的灵活性和可扩展性,同时减少了修改现有代码的风险。

* 那么我们现在用依赖注入的方式实现 【遵循OCP的策略模式】

步骤1:定义接口和策略类

我们已经定义了 IDiscountStrategy 接口和具体的策略类(StandardOrderDiscountBulkOrderDiscount),这里不再重复。

步骤2:配置依赖注入容器

public void ConfigureServices(IServiceCollection services)
{
    // 注册策略类
    services.AddTransient<IDiscountStrategy, StandardOrderDiscount>();
    services.AddTransient<IDiscountStrategy, BulkOrderDiscount>();
    // 可以继续注册更多策略类,如会员订单策略

    // 注册 OrderDiscountCalculator 类,它将依赖于策略类
    services.AddTransient<OrderDiscountCalculator>();
}

这里我们使用了 AddTransient 方法,这意味着每次请求一个新的服务实例时,都会创建一个新的实例。

步骤3:选择正确的策略

        为了让控制器能够根据订单类型选择正确的策略,我们需要在创建 OrderDiscountCalculator 实例时传入正确的策略。这可以通过创建工厂方法或使用条件逻辑来实现。例如,你可以创建一个 DiscountStrategyFactory 类,它根据订单类型返回相应的策略实例。

public class DiscountStrategyFactory
{
    private readonly IServiceProvider _serviceProvider;

    public DiscountStrategyFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IDiscountStrategy GetStrategy(Order order)
    {
        switch (order.Type)
        {
            case OrderType.Standard:
                return _serviceProvider.GetRequiredService<StandardOrderDiscount>();
            case OrderType.Bulk:
                return _serviceProvider.GetRequiredService<BulkOrderDiscount>();
            // 可以添加更多case处理其他类型的订单
            default:
                throw new ArgumentException("Invalid order type.");
        }
    }
}

在控制器中根据订单类型选择正确的类

public class OrdersController : Controller
{
    private readonly DiscountStrategyFactory _strategyFactory;
    private readonly OrderDiscountCalculator _discountCalculator;

    public OrdersController(IServiceProvider serviceProvider)
    {
        _strategyFactory = new DiscountStrategyFactory(serviceProvider);
        _discountCalculator = new OrderDiscountCalculator(_strategyFactory.GetStrategy(GetOrderById(orderId)));
    }

    // ...
}

 

 三、里氏替换原则(LSP)

    * 定义:子类必须能够替换其基类。

    * 解释:任何基类可以出现的地方,子类一定可以出现。这保证了在使用继承时,子类的行为与基类一致,不会破坏程序的正确性

示例:

首先定义下基类:

using System;

public abstract class Shape
{
    public abstract double GetArea();
}
 

两个实现类

public class Square : Shape
{
    private double _side;

    public Square(double side)
    {
        _side = side;
    }

    public override double GetArea()
    {
        return _side * _side;
    }
}
public class Rectangle : Shape
{
    private double _width;
    private double _height;

    public Rectangle(double width, double height)
    {
        _width = width;
        _height = height;
    }

    public override double GetArea()
    {
        return _width * _height;
    }
}

不同派生类的使用

public static void Main(string[] args)
{
    Shape rectangle = new Rectangle(4, 5);
    Shape square = new Square(4);

    Console.WriteLine("Rectangle Area: " + rectangle.GetArea());
    Console.WriteLine("Square Area: " + square.GetArea());
}
遵循 LSP 的关键点
  • 不改变接口:派生类不能改变基类的公共接口。
  • 预条件和后条件:如果基类的方法有预条件(例如参数约束),派生类也必须遵循这些预条件;后条件(方法执行后的状态)在派生类中也不能被改变。

 

四、接口隔离原则(ISP)

 * 定义:不应该强迫客户程序依赖于它们不用的方法。

 * 解释:一个类对另一个类的依赖应该建立在最小的接口上,即不应该为实现接口而实现接口,而是应该实现真正需要的方法。这有助于降低类间的耦合,使得接口更纯粹,更易于理解和使用。

示例场景:动物农场

假设我们正在开发一个动物农场的管理系统,我们需要处理各种动物,如鸟类、哺乳动物等。每种动物可能有不同的行为,例如飞行、游泳或奔跑。

未遵循示例:
public interface IAnimal
{
    void Eat();
    void Sleep();
    void Fly(); // 并非所有动物都能飞
    void Swim(); // 并非所有动物都能游泳
}

public class Eagle : IAnimal
{
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
    public void Fly() { /* ... */ }
    public void Swim() { /* 鹰不会游泳,这里的实现可能是抛出异常或空实现 */ }
}

public class Fish : IAnimal
{
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
    public void Fly() { /* 鱼不会飞,这里的实现可能是抛出异常或空实现 */ }
    public void Swim() { /* ... */ }
}

在这个设计中,IAnimal 接口包含了所有动物可能会做的动作,但并非所有动物都能执行所有动作。例如,鱼不会飞,鹰不会游泳,因此在实现这些方法时,我们可能不得不添加一些空实现或抛出异常,这显然不是最佳实践。

遵循示例:
public interface IEatable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public interface IFlyable
{
    void Fly();
}

public interface ISwimmable
{
    void Swim();
}

public class Eagle : IEatable, ISleepable, IFlyable
{
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
    public void Fly() { /* ... */ }
}

public class Fish : IEatable, ISleepable, ISwimmable
{
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
    public void Swim() { /* ... */ }
}
使用示例
public static void Main(string[] args)
{
    var eagle = new Eagle();
    var fish = new Fish();

    // 调用 Eat 方法
    eagle.Eat();
    fish.Eat();

    // 调用 Fly 方法
    ((IFlyable)eagle).Fly(); // 需要显式转换,也可以在需要的地方使用方法组语法

    // 调用 Swim 方法
    ((ISwimmable)fish).Swim();
}

 

五、依赖倒置原则(DIP)

* 定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。

* 解释:高层模块(如业务逻辑层)应依赖于抽象接口或基类,而不是具体的实现类。这有助于解耦系统中的各个部分,使得系统更灵活,更易于扩展和维护。

场景:日志记录服务

假设我们正在构建一个应用,需要一个日志记录服务,用于记录应用运行时的信息。我们希望日志服务是可替换的,例如,可以使用文件系统、数据库或网络服务进行日志记录。

 步骤1:定义抽象的日志记录接口
public interface ILogger
{
    void LogInformation(string message);
    void LogError(string message);
}
步骤2:创建具体实现的日志记录类

这里我们创建一个文件系统日志记录器的实现:

public class FileLogger : ILogger
{
    private readonly string _logFilePath;

    public FileLogger(string logFilePath)
    {
        _logFilePath = logFilePath;
    }

    public void LogInformation(string message)
    {
        File.AppendAllText(_logFilePath, $"[INFO] {DateTime.Now}: {message}\n");
    }

    public void LogError(string message)
    {
        File.AppendAllText(_logFilePath, $"[ERROR] {DateTime.Now}: {message}\n");
    }
}
步骤3:在服务中注入日志记录服务
public class MyService
{
    private readonly ILogger _logger;

    public MyService(ILogger logger)
    {
        _logger = logger;
    }

    public void DoWork()
    {
        try
        {
            // 执行一些可能出错的操作
            _logger.LogInformation("Operation started.");
            // 模拟可能的异常
            throw new Exception("Something went wrong.");
        }
        catch (Exception ex)
        {
            _logger.LogError($"An error occurred: {ex.Message}");
        }
    }
}
步骤4:配置依赖注入

在 .NET Core 的 Startup.cs 文件中,我们需要配置依赖注入,以便在创建 MyService 时自动注入 ILogger 的实现:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ILogger, FileLogger>();
    services.AddScoped<MyService>();
}
步骤5:使用服务

现在,你可以在控制器或任何需要的地方注入 MyService 并使用它:

public class HomeController : Controller
{
    private readonly MyService _myService;

    public HomeController(MyService myService)
    {
        _myService = myService;
    }

    public IActionResult Index()
    {
        _myService.DoWork();
        return View();
    }
}

通过这种方式,MyService 依赖于抽象的 ILogger 接口,而不是具体的 FileLogger 实现。这使得我们可以在不修改 MyService 的情况下,轻松地更换日志记录服务的实现,例如,如果将来需要使用数据库日志记录器,只需更改 DI 容器中的配置即可。这就是依赖倒置原则在 .NET Core 中的应用。

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值