65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 中的装饰器模式扩展对象行为

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2023 年 4 月 6 日

CPOL

4分钟阅读

viewsIcon

9946

C# 装饰器模式允许在运行时向对象添加行为。在本教程中,我将向您展示如何使用 C# 在最小 API 中实现装饰器模式。

引言

如果您想跳过我接下来的章节中的所有辛苦工作,并且不知道我将要讲什么,您可以直接从我的 GitHub 存储库下载最终产品。

如果您想跟上我所展示的所有内容,请确保打开 "StartProject" 分支。 "EndProject" 分支包含我将在本教程中添加的所有代码。

解释初始情况

我的初始项目非常直接。有两个项目;CachingDemo.BusinessCachingDemo.API。API 是一个 最小 API,只是为了展示一些 UI。业务逻辑是需要一些更改的,我将重点关注这一点。尤其是 MovieService.cs 类。

如果您打开这个类,您会找到 GetAll() 方法。它看起来像这样

public IEnumerable<Movie> GetAll()
{
    string key = "allmovies";

    Console.ForegroundColor = ConsoleColor.Red;

    if (!memoryCache.TryGetValue(key, out List<Movie>? movies))
    {
        Console.WriteLine("Key is not in cache.");
        movies = _dbContext.Set<Movie>().ToList();

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(10))
            .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

        memoryCache.Set(key, movies, cacheOptions);
    }
    else
    {
        Console.WriteLine("Already in cache.");
    }

    Console.ResetColor();

    return movies ?? new List<Movie>();
}

它实际上做了两件事:处理缓存数据,以及在缓存中不存在时处理实际数据。而这正是装饰器模式可以解决的问题。让 MovieService.cs 做它需要做的事情,让另一个类处理缓存。

一个缓存服务

我们需要做的第一件事是为缓存创建一个服务。每个服务都有自己的缓存服务。我有一个服务 MovieService,我创建了另一个类并称之为 MovieService_Cache.cs。之所以这样命名,原因很简单:它将直接放在原始 MovieService 文件下方。

我重复使用了与 MovieService 相同的接口。

public class MovieService_Cache : IMovieService
{
    public void Create(Movie movie)
    {
        throw new NotImplementedException();
    }

    public void Delete(int id)
    {
        throw new NotImplementedException();
    }

    public Movie? Get(int id)
    {
        throw new NotImplementedException();
    }

    public IEnumerable<Movie> GetAll()
    {
        throw new NotImplementedException();
    }
}

依赖注入

我使用 IMemoryCache 将缓存机制注入 MovieService 类,所以我在缓存版本中也做了同样的事情。

这是技巧部分 1:我将 IMovieService 也注入到这个类中。IMovieService 连接到 MovieService,而不是 MovieService_Cache,所以这是安全的。更改后的缓存类看起来像这样

public class MovieService_Cache : IMovieService
{
    private readonly IMemoryCache memoryCache;
    private readonly IMovieService movieService;

    public MovieService_Cache(IMemoryCache memoryCache, IMovieService movieService)
    {
        this.memoryCache = memoryCache;
        this.movieService = movieService;
    }

    public void Create(Movie movie)
    {
        throw new NotImplementedException();
    }

    public void Delete(int id)
    {
        throw new NotImplementedException();
    }

    public Movie? Get(int id)
    {
        throw new NotImplementedException();
    }

    public IEnumerable<Movie> GetAll()
    {
        throw new NotImplementedException();
    }
}

为方法添加一些主体

是时候为方法添加一些代码了。从上到下

Create 方法不需要缓存。它所做的就是将数据发送到数据库,然后完成。所以我将返回注入的 MovieService 实例的结果。

Delete 也是同样的思路:不需要缓存,所以只需重用原始 MovieService 实例。

Get(int id) 方法可以使用缓存。在这里,我使用了缓存机制。代码如下。但是,一旦缓存中没有该键(缓存中不存在该项),我就需要从数据库中检索它。这是原始 MovieService 所做的,而不是缓存版本。看我是如何创建和使用 **单一职责原则** 的?

我将对 GetAll() 方法做同样的事情。

这是代码

public class MovieService_Cache : IMovieService
{
    private readonly IMemoryCache memoryCache;
    private readonly IMovieService movieService;

    public MovieService_Cache(IMemoryCache memoryCache, IMovieService movieService)
    {
        this.memoryCache = memoryCache;
        this.movieService = movieService;
    }

    public void Create(Movie movie)
    {
        movieService.Create(movie);
    }

    public void Delete(int id)
    {
        movieService.Delete(id);
    }

    public Movie? Get(int id)
    {
        string key = $"movie_{id}";

        if (memoryCache.TryGetValue(key, out Movie? movie))
            return movie;

        movie = movieService.Get(id);

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(10))
            .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

        memoryCache.Set(key, movie, cacheOptions);

        return movie;
    }

    public IEnumerable<Movie> GetAll()
    {
        string key = $"movies";

        if (memoryCache.TryGetValue(key, out List<Movie>? movies))
            return movies ?? new List<Movie>();

        movies = movieService.GetAll().ToList();

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(10))
            .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

        memoryCache.Set(key, movies, cacheOptions);

        return movies;
    }
}

如果您查看 Get(int id) 方法,您会发现没有什么特别之处。如果缓存中不存在该键,它将从原始 MovieService 获取电影,将其放入缓存,然后返回结果。

有一点:我从 MovieService.cs 中删除了所有缓存,包括 IMemoryCache 的注入。MovieService.cs 现在看起来像这样

public class MovieService : IMovieService
{
    private readonly DataContext _dbContext;

    public MovieService(DataContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void Create(Movie movie)
    {
        _dbContext.Set<Movie>().Add(movie);
        _dbContext.SaveChanges();
    }

    public void Delete(int id)
    {
        Movie? toDelete = Get(id);

        if (toDelete == null)
            return;

        _dbContext.Remove(toDelete);
        _dbContext.SaveChanges();
    }

    public Movie? Get(int id) => _dbContext.Set<Movie>().FirstOrDefault(x => x.Id == id);

    public IEnumerable<Movie> GetAll() => _dbContext.Set<Movie>().ToList();
}

为 MovieService 添加装饰

如果现在启动 API,它将不使用任何缓存方法。它只会一遍又一遍地从数据库中获取所有电影。我们需要装饰 MovieService 类。这是一个依赖注入配置。

为了实现这一点,我安装了 Scrutor 包。该包包含 Decorate 扩展方法,帮助我们用 MovieService_Cache 装饰 MovieService

所以,首先,安装包

Install-Package Scrutor

然后我们转到 API 并打开 Program.cs。找到将 IMovieService 连接到 MovieService 的那一行。在该行下方添加以下代码

builder.Services.Decorate<IMovieService, MovieService_Cache>();

就这样,各位!

没有别的需要做的了。装饰器模式已准备好发挥作用。

为了测试它,我建议在 MovieService_Cache 的一个方法上设置一个断点,启动 API,然后使用断点执行该方法。

发生的情况是,API 将调用 MovieService,但由于它被 MovieService_Cache 装饰,它将首先执行 MovieService_Cache 中的方法,覆盖原始类。

结论

这只是装饰器模式的一个小例子。但它可能非常强大。我不会过度使用它;不要装饰您拥有的每一个类。您不必更改现有类中的任何内容,这使得在大型应用程序中,当一个类已经可以工作但只需要做微小更改时,会更安全。

我希望看到 .NET 自己实现装饰器,这样我们就无需使用第三方包了。

历史

  • 2023 年 4 月 6 日:初始版本
© . All rights reserved.