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





5.00/5 (6投票s)
C# 装饰器模式允许在运行时向对象添加行为。在本教程中,我将向您展示如何使用 C# 在最小 API 中实现装饰器模式。
引言
如果您想跳过我接下来的章节中的所有辛苦工作,并且不知道我将要讲什么,您可以直接从我的 GitHub 存储库下载最终产品。
如果您想跟上我所展示的所有内容,请确保打开 "StartProject
" 分支。 "EndProject
" 分支包含我将在本教程中添加的所有代码。
解释初始情况
我的初始项目非常直接。有两个项目;CachingDemo.Business
和 CachingDemo.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 日:初始版本