设计模式之美笔记15 我就是我 2022-12-01 05:16 142阅读 0赞 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 ### 文章目录 ### * * 访问者模式 * * 访问者模式的诞生 * 再看访问者模式 * 为什么支持双分派的语言不需要访问者模式 * 除了访问者模式,还有其他实现方案没 * 命令模式 * * 命令模式的原理解读 * 实战讲解 * 和策略模式的比较 ## 访问者模式 ## 较难理解,难实现,在实际的软件开发中很少被用到。 ### 访问者模式的诞生 ### 假设从网站上爬取了很多资源文件,格式有PDF、PPT、Word。现在开发一个工具处理这批资源文件,其中一个功能是,把资源文件的文本内容抽取出来放到TXT文件中。如何实现? 其中一种实现方式如下,其中,ResourceFile是个抽象类,包含一个抽象函数extract2txt()。PdfFile、PPTFile、WordFile都继承ResourceFile类,并重写extract2txt()方法,在ToolApplication中,利用多态特性,根据对象的实际类型,决定执行哪个方法。 public abstract class ResourceFile { protected String filePath; public ResourceFile(String filePath){ this.filePath = filePath; } public abstract void extract2txt(); } public class PPTFile extends ResourceFile { public PPTFile(String filePath){ super(filePath); } @Override public void extract2txt() { //...省略从ppt中抽取文本的代码... //...将抽取的文本保存在跟filePath同名的.txt文件中... System.out.println("Extract PPT."); } } public class PdfFile extends ResourceFile { public PdfFile(String filePath){ super(filePath); } @Override public void extract2txt() { //... System.out.println("Extract PDF."); } } public class WordFile extends ResourceFile { public WordFile(String filePath){ super(filePath); } @Override public void extract2txt() { //... System.out.println("Extract WORD."); } } public class ToolApplication { public static void main(String[] args) { List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]); for (ResourceFile resourceFile:resourceFiles){ resourceFile.extract2txt(); } } private static List<ResourceFile> listAllResourceFiles(String arg) { List<ResourceFile> resourceFiles = new ArrayList<>(); //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile) resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } } 如果工具的功能不停地扩展,不仅要能抽取文本内容,还要支持压缩、提取文件元信息(文件名、大小、更新时间等)构建索引等一些列的功能。如果继续按照上面的实现思路,会存在几个问题: 1. 违背开闭原则,添加一个新的功能,所有的类的代码都要改; 2. 随着功能增多,每个类的代码都不断膨胀,可读性和可维护性都变差 3. 所有较为上层的业务逻辑都耦合到PdfFile、PPTFile、WordFile类,导致这些类的职责不够单一 针对上述问题,常用的解决方法就是拆分解耦,把业务操作和具体的数据结构解耦,设计为独立的类,按照访问者模式的演进思路对上述代码重构: public abstract class ResourceFile { protected String filePath; public ResourceFile(String filePath){ this.filePath = filePath; } } public class PdfFile extends ResourceFile { public PdfFile(String filePath) { super(filePath); } //... } public class PPTFile extends ResourceFile { public PPTFile(String filePath) { super(filePath); } } public class WordFile extends ResourceFile { public WordFile(String filePath) { super(filePath); } } public class Extractor { public void extract2txt(PdfFile pdfFile) { //... System.out.println("Extract PDF."); } public void extract2txt(PPTFile pdfFile) { //... System.out.println("Extract PPT."); } public void extract2txt(WordFile pdfFile) { //... System.out.println("Extract word."); } } public class ToolApplication { public static void main(String[] args) { Extractor extractor = new Extractor(); List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]); for (ResourceFile resourceFile:resourceFiles){ extractor.extract2txt(resourceFile); } } private static List<ResourceFile> listAllResourceFiles(String arg) { List<ResourceFile> resourceFiles = new ArrayList<>(); //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象 resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } } 最关键的设计是,把抽取文本内容的操作,设计为三个重载函数。函数重载指的是同一类总函数名相同,参数不同的一组函数。不过,上述代码编译通不过,因为resourceFiles包含的对象的声明类型都是ResourceFile,而我们并没有在Extractor类中定义参数类型是ResourceFile的extract2txt()重载函数,因此编译通过不了。 解决的办法稍微有点难理解,先看代码: public abstract class ResourceFile { protected String filePath; public ResourceFile(String filePath){ this.filePath = filePath; } abstract public void accept(Extractor extractor); } public class PdfFile extends ResourceFile { public PdfFile(String filePath) { super(filePath); } @Override public void accept(Extractor extractor) { extractor.extract2txt(this); } } public class PPTFile extends ResourceFile { public PPTFile(String filePath) { super(filePath); } @Override public void accept(Extractor extractor) { extractor.extract2txt(this); } } public class WordFile extends ResourceFile { public WordFile(String filePath) { super(filePath); } @Override public void accept(Extractor extractor) { extractor.extract2txt(this); } } public class ToolApplication { public static void main(String[] args) { Extractor extractor = new Extractor(); List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]); for (ResourceFile resourceFile:resourceFiles){ resourceFile.accept(extractor); } } private static List<ResourceFile> listAllResourceFiles(String arg) { List<ResourceFile> resourceFiles = new ArrayList<>(); //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象 resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } } 执行第30行的时候,根据多态,程序会调用实际类型的accept函数,如PdfFile的accept函数,也即是16行代码,其中的this类型是PdfFile,编译的时候就确定了,所以会调用extractor的extract2txt(PdfFile pdfFile)这个重载函数。 如果要继续添加新的功能,如前面说的压缩功能,根据不同的文件类型,使用不同的压缩算法压缩资源文件,如何实现?要实现一个类似Extractor类的新类Compressor类,在其中定义三个重载函数,实现对不同类型资源文件的压缩。此外,还要在每个资源文件类中定义新的accept重载函数。但这样,如果添加一个新的业务,还要修改每个资源文件类,违反开闭原则,针对该问题,抽象一个Visitor接口,具体做什么业务处理,由实现该接口的具体的类决定。如Extractor负责抽取文本内容,Compressor负责压缩,添加新业务时,资源文件类不需要做任何修改,只需修改ToolApplication的代码即可。 public abstract class ResourceFile { protected String filePath; public ResourceFile(String filePath){ this.filePath = filePath; } abstract public void accept(Extractor extractor); } public class PdfFile extends ResourceFile { public PdfFile(String filePath) { super(filePath); } @Override public void accept(Visitor visitor) { visitor.visit(this); } } public class PPTFile extends ResourceFile { public PPTFile(String filePath) { super(filePath); } @Override public void accept(Visitor visitor) { visitor.visit(this); } } public class WordFile extends ResourceFile { public WordFile(String filePath) { super(filePath); } @Override public void accept(Visitor visitor) { visitor.visit(this); } } public interface Visitor { void visit(PdfFile pdfFile); void visit(PPTFile pptFile); void visit(WordFile wordFile); } public class Extractor implements Visitor { @Override public void visit(PdfFile pdfFile) { //... System.out.println("Extract pdf."); } @Override public void visit(PPTFile pptFile) { //... System.out.println("Extract ppt."); } @Override public void visit(WordFile wordFile) { //... System.out.println("Extract word."); } } public class Compressor implements Visitor { @Override public void visit(PdfFile pdfFile) { //... System.out.println("Compress pdf."); } @Override public void visit(PPTFile pptFile) { //... System.out.println("Compress ppt."); } @Override public void visit(WordFile wordFile) { //... System.out.println("Compress word."); } } public class ToolApplication { public static void main(String[] args) { Extractor extractor = new Extractor(); List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]); for (ResourceFile resourceFile:resourceFiles){ resourceFile.accept(extractor); } Compressor compressor = new Compressor(); for (ResourceFile resourceFile:resourceFiles){ resourceFile.accept(compressor); } } private static List<ResourceFile> listAllResourceFiles(String arg) { List<ResourceFile> resourceFiles = new ArrayList<>(); //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象 resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } } ### 再看访问者模式 ### 一步步还原访问者模式诞生的思维过程后,总结下该模式的原理和代码实现。 访问者模式,Visitor Design Pattern,定义是:Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure. 允许一个或多个操作应用到一组对象上,解耦操作和对象本身。 再看访问者模式的应用场景。 访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型不同,但继承相同的父类(ResourceFile)或实现相同的接口。不同的应用场景下,需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类不断膨胀,职责不单一,以及避免频繁的添加功能导致的频繁代码修改,使用访问者模式,将对象和操作解耦,将业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。 ### 为什么支持双分派的语言不需要访问者模式 ### 访问者模式,都会提到Double Dispatch,双分派,对应的有单分派。单分派,single dispatch,指的是执行哪个对象的方法,根据对象的运行时类型决定;执行对象的哪个方法,根据参数的编译时类型决定。双分派,指的是执行哪个对象的方法,根据对象的运行时类型决定;执行对象的哪个方法,根据方法参数的运行时类型决定。 如何理解dispatch?面向对象编程语言中,可把方法调用理解为一种消息传递,也即是dispatch,一个对象调用另一个对象的方法,相当于给它发送了一条消息。这条消息要包含对象名、方法名、方法参数。 如何理解single double呢?指的是执行哪个对象的那个方法,跟几个因素的运行时类型有关。single dispatch之所以称为single,因为执行哪个对象的哪个方法,只和“对象”的运行时类型有关。double dispatch之所以是double,是因为执行哪个对象的哪个方法,跟“对象”和“方法参数”两者的运行时类型有关。 具体到编程语言,主流的面向对象编程语言如java、C++、C\#都只支持single dispatch,不支持double dispatch。以java为例,java支持多态特性,代码可在运行时获得对象的实际类型(也就是运行时类型),然后根据实际类型决定调用哪个方法。尽管java支持函数重载,但java设计的函数重载的与法规则是,并不是在运行时,根据传递进函数的参数的实际类型,决定调用哪个重载函数,而是在编译时,根据传递进函数的参数的声明类型(也即是编译时类型),决定调用哪个重载函数。也就是说,具体执行哪个对象的哪个方法,只跟对象的运行时类型有关,跟参数的运行时类型无关。所以,java只支持single dispatch。 ### 除了访问者模式,还有其他实现方案没 ### 实际上,还有其他的实现方案,如,可利用工厂模式实现,定义一个包含extract2txt()接口函数的Extractor接口。PdfExtractor、PPTExtractor、WordExtractor类实现Extractor接口,且在各自的extract2txt()函数中分别实现pdf、ppt、Word格式文件的文本内容抽取。ExtractorFactory工厂类根据不同的文件类型,返回不同的Extractor。 public abstract class ResourceFile { protected String filePath; public ResourceFile(String filePath){ this.filePath = filePath; } public abstract ResourceFileType getType(); } public class PdfFile extends ResourceFile { public PdfFile(String filePath) { super(filePath); } @Override public ResourceFileType getType() { return ResourceFileType.PDF; } } public class PPTFile extends ResourceFile { public PPTFile(String filePath) { super(filePath); } @Override public ResourceFileType getType() { return ResourceFileType.PPT; } } public class WordFile extends ResourceFile { public WordFile(String filePath) { super(filePath); } @Override public ResourceFileType getType() { return ResourceFileType.WORD; } } public interface Extractor { void extract2txt(ResourceFile resourceFile); } public class PdfExtractor implements Extractor { @Override public void extract2txt(ResourceFile resourceFile) { //... } } public class PPTExtractor implements Extractor { @Override public void extract2txt(ResourceFile resourceFile) { //... } } public class WordExtractor implements Extractor { @Override public void extract2txt(ResourceFile resourceFile) { //... } } public class ExtractorFactory { private static final Map<ResourceFileType,Extractor> extractors = new HashMap<>(); static { extractors.put(ResourceFileType.PDF,new PdfExtractor()); extractors.put(ResourceFileType.PPT,new PPTExtractor()); extractors.put(ResourceFileType.WORD,new WordExtractor()); } public static Extractor getExtractor(ResourceFileType type){ return extractors.get(type); } } public class ToolApplication { public static void main(String[] args) { List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]); for (ResourceFile resourceFile:resourceFiles){ Extractor extractor = ExtractorFactory.getExtractor(resourceFile.getType()); extractor.extract2txt(resourceFile); } } private static List<ResourceFile> listAllResourceFiles(String arg) { List<ResourceFile> resourceFiles = new ArrayList<>(); //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象 resourceFiles.add(new PdfFile("a.pdf")); resourceFiles.add(new WordFile("b.word")); resourceFiles.add(new PPTFile("c.ppt")); return resourceFiles; } } 对于资源文件处理工具这个例子,如果工具提供的功能不是特别多,只有几个,更推荐使用工厂模式,更清晰易懂。相反,如果工具提供非常多的功能,如有十几个,更推荐使用访问者模式,因为访问者模式需要定义的类要比工厂模式的实现方式少很多,类太多影响代码的可维护性。 ## 命令模式 ## 后续只有命令模式、解释器模式、中介模式三种,使用频率低、理解难度大,只需稍微了解。 ### 命令模式的原理解读 ### 命令模式,command design pattern,定义:the command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations. 命令模式将请求(命令)封装为一个对象,这样可使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),且能支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。 具体解读,落实到编码实现,命令模式用的最核心的实现手段,是将函数封装为对象。C语言支持函数指针,可把函数当做变量传递,但大多数编程语言,函数没法作为参数传递给其他函数,也没法赋值给变量。借助命令模式,可将函数封装成对象。具体说,设计一个包含这个函数的类,实例化为一个对象传递,这样就可以实现把函数像对象一样使用。从实现的角度,类似回调。 当把函数封装为对象,对象就可以存储,方便控制执行。所以,命令模式的主要作用和应用场景,是用来控制命令的执行,如异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等,这种场景适合命令模式。 ### 实战讲解 ### 假设正在开发类似《天天酷跑》或者《QQ卡丁车》这样的手游。这种游戏本身的复杂度集中在客户端,后端基本只负责数据(如积分、生命值、装备)的更新和查询,后端逻辑相对于客户端简单的多。 为提高性能,把游戏玩家的信息保存到内存,游戏进行过程中,只更新内存中的数据,游戏结束,再将内存中的数据存档,也就是持久化到数据库。为降低实现的难度,一般同一个游戏场景的玩家,会分配到同一台服务上。这样,一个玩家拉取同一个游戏场景中的其他玩家的信息,就不需要跨服务器查找,实现比较简单。 一般,游戏客户端和服务器之间的数据交互较为频繁,为节省网络连接建立的开销,客户端和服务器之间采用长连接的方式通信,通信格式有多种,如protocol buffer、json、xml,甚至可以自定义格式。不管什么格式,客户端发送给服务器的请求,一般包括两部分:指令和数据。指令也可叫做事件,数据是执行该指令所需的数据。 服务器在接收到客户端的请求后,会解析出指令和数据,并根据指令的不同,执行不同的处理逻辑。对这样的业务场景,一般有两种架构实现思路。 常用的一种思路是利用多线程。一个线程接收请求,之后,启动一个新的线程处理请求。具体说,一般通过一个主线程接收客户端发来的请求,每当接收到一个请求后,就从专门处理请求的线程池中,捞出一个空闲线程处理。 另一种是在一个线程内轮询接收请求和处理请求。这种较为少见。尽管无法利用多线程多核处理的优势,但对于IO密集型的业务说,避免多线程不停切换对性能的损耗,且克服了多线程编程bug较难调试的缺点,也算是手游后端服务器开发中较为常见的架构模式。 重点说第二种。 整个手游后端服务器轮询获取客户端发来的请求,获取到请求后,借助命令模式,把请求包含的数据和处理逻辑封装为命令对象,并存储到内存队列。然后,再从队列中取出一定数量的命令执行。执行完成后,再重新开始新的一轮轮询。代码如下: public interface Command { void execute(); } public class GotDiamondCommand implements Command { //省略成员变量 public GotDiamondCommand(){ //... } @Override public void execute() { //执行相应的逻辑 } } public class GotStartCommand implements Command { //省略成员变量 public GotStartCommand(){ //... } @Override public void execute() { //执行相应的逻辑 } } public class ArchiveCommand implements Command { //省略成员变量 public ArchiveCommand(){ //... } @Override public void execute() { //执行相应的逻辑 } } public class GameApplication { private static final int MAX_HANDLED_REQ_COUNT_PER_LOOP=100; private Queue<Command> queue = new LinkedList<>(); public void mainloop(){ while (true){ List<Request> requests = new ArrayList<>(); //省略从epoll或select获取数据,并封装为Request的逻辑 //注意设置超时时间,如果很长时间没有接收到请求,就继续下面的逻辑处理 for (Request request:requests){ Event event = request.getEvent(); Command command = null; if (event.equals(Event.GOT_DIAMOND)){ command = new GotDiamondCommand(); }else if (event.equals(Event.GOT_STAR)){ command = new GotStartCommand(); }else if (event.equals(Event.HIT_OBSTACLE)){ command = new HitObstacleCommand(); }else if (event.equals(Event.ARCHIVE)){ command = new ArchiveCommand(); } queue.add(command); } int handledCount = 0; while (handledCount < MAX_HANDLED_REQ_COUNT_PER_LOOP){ if (queue.isEmpty()){ break; } Command command = queue.poll(); command.execute(); } } } } ### 和策略模式的比较 ### 可能看代码感觉和策略模式、工厂模式非常相似,区别在哪里呢? 实际上,每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可解决哪类问题;第二部分是解决方案,即设计思路和具体的代码实现。不过,代码实现并不是模式必须包含的,如果单纯只关注解决方案,甚至只关注代码实现,会产生大部分模式看起来都很相似的错觉。 实际上,设计模式之间的主要区别还在于设计意图,也就是应用场景。单纯看设计思路或代码实现,有些模式确实很相似,如策略模式和工厂模式。 策略模式包含策略的定义、创建和使用三部分,从代码结构上,非常像工厂模式,区别在于,策略模式侧重“策略”或“算法”这个特定的应用场景,用来解决根据运行时状态从一组策略汇总选择不同策略的问题,而工厂模式侧重封装对象的创建过程,这里的对象没有任何业务场景的限定,可以是策略,也可以是其他东西。从设计意图上说,这两个模式是两回事。 再看策略模式和命令模式的区别。在策略模式中,不同的策略具有相同的目的、不同的实现、相互之间可以替换。如BubbleSort、SelectionSort都是为了实现排序,只是一个用冒泡算法,一个用选择排序算法实现。而命令模式中,不同的命令有不同的目的,对应不同的处理逻辑,且相互之间不可替换。 > redis是否是用了命令模式处理指令的?
相关 设计模式之美笔记16 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 解释器模式 解释器模式的原理和实现 深藏阁楼爱情的钟/ 2022年12月01日 11:53/ 0 赞/ 136 阅读
相关 设计模式之美笔记15 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 访问者模式 访问者模式的诞生 我就是我/ 2022年12月01日 05:16/ 0 赞/ 143 阅读
相关 设计模式之美笔记14 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 状态模式 背景 什么 水深无声/ 2022年11月30日 15:51/ 0 赞/ 146 阅读
相关 设计模式之美笔记13 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 策略模式 策略模式的原理和实现 忘是亡心i/ 2022年11月30日 12:27/ 0 赞/ 145 阅读
相关 设计模式之美笔记12 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 观察者模式 原理及应用场景剖析 深碍√TFBOYSˉ_/ 2022年11月30日 04:18/ 0 赞/ 174 阅读
相关 设计模式之美笔记11 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 门面模式 门面模式的原理和实现 ゞ 浴缸里的玫瑰/ 2022年11月28日 13:41/ 0 赞/ 155 阅读
相关 设计模式之美笔记10 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 序言 代理模式 桥接模式 柔情只为你懂/ 2022年11月28日 10:36/ 0 赞/ 149 阅读
相关 设计模式之美笔记9 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 工厂模式 1. 简单工厂 待我称王封你为后i/ 2022年11月28日 00:41/ 0 赞/ 148 阅读
相关 设计模式之美笔记8 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 单例模式 1. 为什么要使用单例 柔光的暖阳◎/ 2022年11月26日 07:52/ 0 赞/ 153 阅读
相关 设计模式之美笔记7 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 实战1:id生成器的重构 1. 需求背景 女爷i/ 2022年11月25日 13:19/ 0 赞/ 186 阅读
还没有评论,来说两句吧...