装饰者模式是一种设计模式,它允许用户在不修改现有对象代码的情况下,通过创建一个包装对象来扩展对象的行为。在本文中,将探讨如何在C#中使用装饰者模式来实现缓存功能,并通过依赖注入配置装饰者。
起始项目相当简单。有两个项目: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类中,所以在缓存版本中也这样做。这是第一部分的技巧:也在这个类中注入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;
}
}
如果现在启动API,它不会使用任何缓存方法。它将一遍又一遍地从数据库中获取所有电影。需要装饰MovieService类。这是一个依赖注入配置。
为了实现这一点,安装了Scrutor包。这个包包含了扩展方法Decorate,它帮助用MovieService_Cache装饰MovieService。
所以,首先,安装包:
Install-Package Scrutor
然后前往API并打开Program.cs。找到连接IMovieService到MovieService的行。在下面添加以下代码行:
builder.Services.Decorate<IMovieService, MovieService_Cache>();
这只是装饰者模式的一个小例子。但它可以非常强大。不会过度使用它;不要装饰拥有的每个类。不必改变现有类中的任何内容,这使得它在在一个大应用程序中工作时更安全,如果一个类已经工作了,但只需要一点点变化。