目录
开发人员喜欢模式。我们可以使用或遵循许多模式。一些众所周知的模式是策略模式、观察者模式和构建器模式。还有很多,每个都有自己的优点和缺点。这一次,我想向你展示装饰器模式。此模式背后的思想是,您可以向现有对象添加行为,而不会影响同一类的其他对象。 听起来很复杂?好吧,事实并非如此......一旦你了解了它。
介绍
如果你想跳过我在接下来的章节中所做的所有艰苦工作,并且不知道我将要讲述什么,你可以简单地从我的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自己的装饰器实现,因此我们不必使用第三方包。
https://www.codeproject.com/Articles/5358474/Extending-Object-Behavior-with-the-Decorator-Patte