这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战
1. 文章发布背后的秘密
2020年初的时候决定离职,专科学历的我压力山大,简历几乎没有任何优势,我已经有预感到时候简历会石沉大海。没办法,没有优势制造优势也要上,于是我决定利用清明节三天假期的时间写一个博客系统出来,我有自己的服务器和域名,投简历前我可以把这个项目部署上去,并写到简历上,相信会加分不少。
清明节三天时间我没有出过房门,每天做的事情就是:写代码、点外卖、睡觉。好在天道酬勤,项目写的比较顺利,三天的时间整体功能都完成了,最终部署上线并将访问链接写到了简历上。
这个博客项目中,我印象比较深的就是文章的发布了。用户发布一篇文章,系统需要完成三个主要动作:
- 文章数据保存到数据库。
- 文章内容进行中文分词,建立全文索引存储到ElasticSearch。
- Redis设置BitMap,用作布隆过滤器,防止缓存穿透。
我们试着用伪代码来描述这个过程:
DBClient
class DBClient {
void insert(){
print("文章保存到数据库");
}
void update(){
print("文章更新到数据库");
}
void delete(){
print("文章从数据库删除");
}
}
ElasticSearchClient
class ElasticSearchClient {
void addDocument(){
print("ElasticSearch建立文章索引...");
}
......
}
RedisClient
class RedisClient {
void setBitMap(){
print("设置位图...");
}
}
当客户端需要发布一篇文章时,需要这样调用:
DBClient dbClient = new DBClient();
ElasticSearchClient elasticSearchClient = new ElasticSearchClient();
RedisClient redisClient = new RedisClient();
// 保存到数据库
dbClient.insert();
// 分词,全文索引
elasticSearchClient.addDocument();
// 设置BitMap,用作布隆过滤器
redisClient.setBitMap();
功能是正常的,但是这样实现存在一个什么问题呢? 客户端需要依赖DBClient、ElasticSearchClient、RedisClient类,而且需要非常清楚的知道文章发布的所有过程,耦合性太高了,如果以后中间还要加个步骤呢?客户端会疯掉吧。
知道了不好的地方,现在我们来优化。
文章的发布、修改、删除是否都可以看成是一个命令呢?调用者发出一个命令,接收者去执行,至于命令执行的具体细节,调用者压根就不关心,以此来达到松散耦合的目的。
抽象命令类
abstract class Command {
protected DBClient dbClient = new DBClient();
protected ElasticSearchClient elasticSearchClient = new ElasticSearchClient();
protected RedisClient redisClient = new RedisClient();
// 命令的执行,子类实现
abstract void execute();
}
命令执行者
class Invoker {
Command command;
// 设置命令
void setCommand(Command command) {
this.command = command;
}
// 执行命令
void action(){
this.command.execute();
}
}
文章发布命令
class PublishCommand extends Command{
@Override
void execute() {
super.dbClient.insert();
super.elasticSearchClient.addDocument();
super.redisClient.setBitMap();
}
}
客户端的调用变得非常的简单,客户端只需要创建指定的命令,交给Invoker执行就OK了,至于命令执行的细节它是不关心的。
如果用户要修改文章呢?也非常简单,只需要再扩展一个修改命令即可:
class UpdateCommand extends Command{
@Override
void execute() {
// 更新数据库
super.dbClient.update();
// 更新索引
super.elasticSearchClient.updateDocument();
// 修改文章,文章ID不变,位图不用动。
}
}
客户端只需要创建UpdateCommand交给Invoker执行就好了,其他地方都不用动,非常方便。
这就是命令模式!
2. 命令模式的定义
将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
- Receiver:命令接收者,也是实际干活的角色。
- Command:抽象命令,定义所有命令的操作。
- ConcreteCommand:实际的命令,有N个命令就有N个命令类。
- Invoker:调用者,接收到命令并执行。
命令模式很简单,也很常用,它的封装性非常好,将请求者Invoker和执行者Receiver分离开了,扩展性也非常好,要想增加命令,只需要派生Command子类就可以了。
3. 命令模式的优缺点
优点
- 降低耦合,请求者和执行者没有任何依赖了,请求者只负责接收命令并执行,至于是谁执行的、怎么执行的,它并不关心。
- 扩展性好,扩展一个命令非常简单,派生
Command子类即可。 - 可以对命令的请求进行排队和记录日志。
缺点
如果有N个命令,就需要编写N个Command子类,如果命令太多会导致类的数量膨胀。
4. 总结
只要你认为是命令的地方就可以用命令模式,命令模式还可以和其他模式结合使用,例如结合「备忘录模式」来实现命令的撤销功能,结合「责任链模式」来实现命令族的解析。
命令模式很好的将命令的调用者和命令的执行者解耦了,客户端只关心要执行的命令,至于命令是由谁执行的,怎么执行的,它并不关心,这就是高内聚的体现。
另外,Invoker在执行命令时也可以做一些文章,例如对命令的执行进行排队,记录日志等。