diff --git a/Chapter6 - Design Pattern/6.14.md b/Chapter6 - Design Pattern/6.14.md index 3cdf4dc..203729e 100644 --- a/Chapter6 - Design Pattern/6.14.md +++ b/Chapter6 - Design Pattern/6.14.md @@ -6,8 +6,7 @@ 这其中“最纯正”的理解方式,当属 GoF 的《设计模式》一书中对桥接模式的定义。毕竟,这 23 种经典的设计模式,最初就是由这本书总结出来的。在 GoF 的《设计模式》一书中,桥接模式是这么定义的:“Decouple an abstraction from its implementation so that the two can vary independently。”翻译成中文就是:“**将抽象和实现解耦,让它们可以独立变化**” -很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通 -过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,我们之前讲过的“组合优于继承”设计原则 +很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”通过组合关系来替代继承关系,避免继承层次的指数级爆炸。这种理解方式非常类似于,我们之前讲过的“组合优于继承”设计原则 GoF 给出的定义非常的简短,单凭这一句话,估计没几个人能看懂是什么意思。所以,我们通过 JDBC 驱动的例子来解释一下。JDBC 驱动是桥接模式的经典应用。我们先来看一下,如何利用 JDBC 驱动来查询数据库。具体的代码如下所示 ``` diff --git a/Chapter6 - Design Pattern/6.2.md b/Chapter6 - Design Pattern/6.2.md index 3f8b414..b1efcf2 100644 --- a/Chapter6 - Design Pattern/6.2.md +++ b/Chapter6 - Design Pattern/6.2.md @@ -81,7 +81,7 @@ public class Ostrich extends AbstractBird { // 鸵鸟 这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 fly() 方法,抛出异常。这样的设计,一方面,徒增了编码的工作量;另一方面,也违背了我们之后要讲的最小知识原则(Least Knowledge Principle,也叫最少知识原则或者迪米特法则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。 你可能又会说,那我们再通过 AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类,不就可以了吗?具体的继承关系如下图所示: -![](./../assets/oop-mixBetterThanSuper.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/oop-mixBetterThanSuper.png) 从图中我们可以看出,继承关系变成了三层。不过,整体上来讲,目前的继承关系还比较简单,层次比较浅,也算是一种可以接受的设计思路。我们再继续加点难度。在刚刚这个场景中,我们只关注“鸟会不会飞”,但如果我们还关注“鸟会不会叫”,那这个时候,我们又该如何设计类之间的继承关系呢? diff --git a/Chapter6 - Design Pattern/6.20.md b/Chapter6 - Design Pattern/6.20.md index 47ccd4e..0b72a37 100644 --- a/Chapter6 - Design Pattern/6.20.md +++ b/Chapter6 - Design Pattern/6.20.md @@ -282,8 +282,8 @@ public DObserver { ## 实现 EventBus 框架 EventBus 中两个核心函数 register() 和 post() 的实现原理。弄懂了它们,基本上就弄懂了整个 EventBus 框架。下面两张图是这两个函数的实现原理图。 -![](./../assets/EventBus-ObserverRegisterTable.png) -![](./../assets/EventBus-Post.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/EventBus-ObserverRegisterTable.png) +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/EventBus-Post.png) 最关键的一个数据结构是 Observer 注册表,记录了消息类型和可接收消息函数的对应关系。当调用 register() 函数注册观察者的时候,EventBus 通过解析 @Subscribe 注解,生成 Observer 注册表。 diff --git a/Chapter6 - Design Pattern/6.24.md b/Chapter6 - Design Pattern/6.24.md new file mode 100644 index 0000000..4db2b42 --- /dev/null +++ b/Chapter6 - Design Pattern/6.24.md @@ -0,0 +1,302 @@ +# 状态模式 + +状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。今天,我们就详细讲讲这几种实现方式 + +## 什么是状态机? +状态机也叫有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作 + +超级马里奥”游戏不知道你玩过没有?在游戏中,马里奥可以变身为多种形态,比如小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)等等。在不同的游戏情节下,各个形态会互相转化,并相应的增减积分。比如,初始形态是小马里奥,吃了蘑菇之后就会变成超级马里奥,并且增加 100 积分。 + +实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分) + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StateMachinePatter-Mariogame.png) + +``` +public enum State { + SMALL(0), + SUPER(1), + FIRE(2), + CAPE(3); + private int value; + private State(int value) { + this.value = value; + } + public int getValue() { + return this.value; + } +} + +public class MarioStateMachine { + private int score; + private State currentState; + public MarioStateMachine() { + this.score = 0; + this.currentState = State.SMALL; + } + public void obtainMushRoom() { + //TODO + } + public void obtainCape() { + //TODO + } + public void obtainFireFlower() { + //TODO + } + public void meetMonster() { + //TODO + } + public int getScore() { + return this.score; + } + public State getCurrentState() { + return this.currentState; + } +} + +public class ApplicationDemo { + public static void main(String[] args) { + MarioStateMachine mario = new MarioStateMachine(); + mario.obtainMushRoom(); + int score = mario.getScore(); + State state = mario.getCurrentState(); + System.out.println("mario score: " + score + "; state: " + state); + } +} +``` + +## 实现 + +### 方法1:分支逻辑法 +对于如何实现状态机,我总结了三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移,原模原样地直译成代码。这样编写的代码会包含大量的 if-else 或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑,所以,我把这种方法暂且命名为分支逻辑法 +``` +public class MarioStateMachine { + private int score; + private State currentState; + public MarioStateMachine() { + this.score = 0; + this.currentState = State.SMALL; + } + public void obtainMushRoom() { + if (currentState.equals(State.SMALL)) { + this.currentState = State.SUPER; + this.score += 100; + } + } + public void obtainCape() { + if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) + this.currentState = State.CAPE; + this.score += 200; + } + } + public void obtainFireFlower() { + if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) + this.currentState = State.FIRE; + this.score += 300; + } + } + public void meetMonster() { + if (currentState.equals(State.SUPER)) { + this.currentState = State.SMALL; + this.score -= 100; + return; + } + if (currentState.equals(State.CAPE)) { + this.currentState = State.SMALL; + this.score -= 200; + return; + } + if (currentState.equals(State.FIRE)) { + this.currentState = State.SMALL; + this.score -= 300; + return; + } + } + public int getScore() { + return this.score; + } + public State getCurrentState() { + return this.currentState; + } +} +``` +对于简单的状态机来说,分支逻辑这种实现方式是可以接受的。但是,对于复杂的状态机来说,这种实现方式极易漏写或者错写某个状态转移。除此之外,代码中充斥着大量的 if-else 或者 switch-case 分支判断逻辑,可读性和可维护性都很差。如果哪天修改了状态机中的某个状态转移,我们要在冗长的分支逻辑中找到对应的代码进行修改,很容易改错,引入 bug。 + +### 方法2:查表法 +上面这种实现方法有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,我们就一块儿来看下,如何利用查表法来补全骨架代码。 + +实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作 +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/StateMachine-TabMethod.png) +相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了 + +``` +public enum Event { + GOT_MUSHROOM(0), + GOT_CAPE(1), + GOT_FIRE(2), + MET_MONSTER(3); + private int value; + private Event(int value) { + this.value = value; + } + public int getValue() { + return this.value; + } +} +public class MarioStateMachine { + private int score; + private State currentState; + private static final State[][] transitionTable = { + {SUPER, CAPE, FIRE, SMALL}, + {SUPER, CAPE, FIRE, SMALL}, + {CAPE, CAPE, CAPE, SMALL}, + {FIRE, FIRE, FIRE, SMALL} + }; + private static final int[][] actionTable = { + {+100, +200, +300, +0}, + {+0, +200, +300, -100}, + {+0, +0, +0, -200}, + {+0, +0, +0, -300} + }; + public MarioStateMachine() { + this.score = 0; + this.currentState = State.SMALL; + } + public void obtainMushRoom() { + executeEvent(Event.GOT_MUSHROOM); + } + public void obtainCape() { + executeEvent(Event.GOT_CAPE); + } + public void obtainFireFlower() { + executeEvent(Event.GOT_FIRE); + } + public void meetMonster() { + executeEvent(Event.MET_MONSTER); + } + private void executeEvent(Event event) { + int stateValue = currentState.getValue(); + int eventValue = event.getValue(); + this.currentState = transitionTable[stateValue][eventValue]; + this.score = actionTable[stateValue][eventValue]; + } + public int getScore() { + return this.score; + } + public State getCurrentState() { + return this.currentState; + } +} +``` +### 方法3: 状态模式 +在查表法的代码实现中,事件触发的动作只是简单的积分加减,所以,我们用一个 int 类型的二维数组 actionTable 就能表示,二维数组中的值表示积分的加减值。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减积分、写数据库,还有可能发送消息通知等等),我们就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性 + +虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。实际上,针对分支逻辑法存在的问题,我们可以使用状态模式来解决。 + +状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。我们还是结合代码来理解这句话 + +其中,IMario 是状态的接口,定义了所有的事件。SmallMario、SuperMario、CapeMario、FireMario 是 IMario 接口的实现类,分别对应状态机中的 4 个状态。原来所有的状态转移和动作执行的代码逻辑,都集中在 MarioStateMachine 类中,现在,这些代码逻辑被分散到了这 4 个状态类中 + +``` +public interface IMario { //所有状态类的接口 + State getName(); + //以下是定义的事件 + void obtainMushRoom(); + void obtainCape(); + void obtainFireFlower(); + void meetMonster(); +} + +public class SmallMario implements IMario { + private MarioStateMachine stateMachine; + public SmallMario(MarioStateMachine stateMachine) { + this.stateMachine = stateMachine; + } + @Override + public State getName() { + return State.SMALL; + } + @Override + public void obtainMushRoom() { + stateMachine.setCurrentState(new SuperMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 100); + } + @Override + public void obtainCape() { + stateMachine.setCurrentState(new CapeMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 200); + } + @Override + public void obtainFireFlower() { + stateMachine.setCurrentState(new FireMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 300); + } + @Override + public void meetMonster() { + // do nothing... + } +} +public class SuperMario implements IMario { + private MarioStateMachine stateMachine; + public SuperMario(MarioStateMachine stateMachine) { + this.stateMachine = stateMachine; + } + @Override + public State getName() { + return State.SUPER; + } + @Override + public void obtainMushRoom() { + // do nothing... + } + @Override + public void obtainCape() { + stateMachine.setCurrentState(new CapeMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 200); + } + @Override + public void obtainFireFlower() { + stateMachine.setCurrentState(new FireMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() + 300); + } + @Override + public void meetMonster() { + stateMachine.setCurrentState(new SmallMario(stateMachine)); + stateMachine.setScore(stateMachine.getScore() - 100); + } +} +// 省略CapeMario、FireMario类... +public class MarioStateMachine { + private int score; + private IMario currentState; // 不再使用枚举来表示状态 + public MarioStateMachine() { + this.score = 0; + this.currentState = new SmallMario(this); + } + public void obtainMushRoom() { + this.currentState.obtainMushRoom(); + } + public void obtainCape() { + this.currentState.obtainCape(); + } + public void obtainFireFlower() { + this.currentState.obtainFireFlower(); + } + public void meetMonster() { + this.currentState.meetMonster(); + } + public int getScore() { + return this.score; + } + public State getCurrentState() { + return this.currentState.getName(); + } + public void setScore(int score) { + this.score = score; + } + public void setCurrentState(IMario currentState) { + this.currentState = currentState; + } +} +``` +实际上,像游戏这种比较复杂的状态机,包含的状态比较多,我优先推荐使用查表法,而状态模式会引入非常多的状态类,会导致代码比较难维护。相反,像电商下单、外卖下单这种类型的状态机,它们的状态并不多,状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能会比较复杂,所以,更加推荐使用状态模式来实现 + diff --git a/Chapter6 - Design Pattern/6.25.md b/Chapter6 - Design Pattern/6.25.md new file mode 100644 index 0000000..58cf98f --- /dev/null +++ b/Chapter6 - Design Pattern/6.25.md @@ -0,0 +1,131 @@ +# 迭代器模式 + +迭代器模式。它用来遍历集合对象。不过,很多编程语言都将迭代器作为一个基础的类库,直接提供出来了。在平时开发中,特别是业务开发,我们直接使用即可,很少会自己去实现一个迭代器。不过,知其然知其所以然,弄懂原理能帮助我们更好的使用这些工具类,所以,我觉得还是有必要学习一下这个模式 + +## 定义 +迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。 +它用来遍历集合对象。这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一 + +迭代器是用来遍历容器的,所以,一个完整的迭代器模式一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类 + +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/IteratorPatter-structure.png) + +## 实现 +我们针对 ArrayList 和 LinkedList 两个线性容器,设计实现对应的迭代器。按照之前给出的迭代器模式的类图,我们定义一个迭代器接口 Iterator,以及针对两种容器的具体的迭代器实现类 ArrayIterator 和 ListIterator + +Iterator 接口的定义。具体的代码如下所示 +``` +// 接口定义方式一 +public interface Iterator { +boolean hasNext(); +void next(); +E currentItem(); +} +// 接口定义方式二 +public interface Iterator { +boolean hasNext(); +E next(); +} +``` +在第一种定义中,next() 函数用来将游标后移一位元素,currentItem() 函数用来返回当前游标指向的元素。在第二种定义中,返回当前元素与后移一位这两个操作,要放到同一个函数 next() 中完成 + +第一种定义方式更加灵活一些,比如我们可以多次调用 currentItem() 查询当前元素,而不移动游标。所以,在接下来的实现中,我们选择第一种接口定义方式。 + +ArrayIterator +``` +public class ArrayIterator implements Iterator { + private int cursor; + private ArrayList arrayList; + public ArrayIterator(ArrayList arrayList) { + this.cursor = 0; + this.arrayList = arrayList; + } + @Override + public boolean hasNext() { + return cursor != arrayList.size(); //注意这里,cursor在指向最后一个元素的时候,ha + } + @Override + public void next() { + cursor++; + } + @Override + public E currentItem() { + if (cursor >= arrayList.size()) { + throw new NoSuchElementException(); + } + return arrayList.get(cursor); + } +} +public class Demo { + public static void main(String[] args) { + ArrayList names = new ArrayList<>(); + names.add("xzg"); + names.add("wang"); + names.add("zheng"); + Iterator iterator = new ArrayIterator(names); + while (iterator.hasNext()) { + System.out.println(iterator.currentItem()); + iterator.next(); + } + } +} +``` +在上面的代码实现中,我们需要将待遍历的容器对象,通过构造函数传递给迭代器类。实际上,为了封装迭代器的创建细节,我们可以在容器中定义一个 iterator() 方法,来创建对应的迭代器。为了能实现基于接口而非实现编程,我们还需要将这个方法定义在 List 接口中 +``` +public interface List { + Iterator iterator(); + //...省略其他接口函数... +} +public class ArrayList implements List { + //... + public Iterator iterator() { + return new ArrayIterator(this); + } + //...省略其他代码 +} +public class Demo { + public static void main(String[] args) { + List names = new ArrayList<>(); + names.add("xzg"); + names.add("wang"); + names.add("zheng"); + Iterator iterator = names.iterator(); + while (iterator.hasNext()) { + System.out.println(iterator.currentItem()); + iterator.next(); + } + } +} +``` + +## 优势 +一般来讲,遍历集合数据有三种方法:for 循环、foreach 循环、iterator 迭代器。对于这三种方式,我拿 Java 语言来举例说明一下。具体的代码如下所示 + +``` +List names = new ArrayList<>(); +names.add("xzg"); +names.add("wang"); +names.add("zheng"); +// 第一种遍历方式:for循环 +for (int i = 0; i < names.size(); i++) { + System.out.print(names.get(i) + ","); +} +// 第二种遍历方式:foreach循环 +for (String name : names) { + System.out.print(name + ",") +} +// 第三种遍历方式:迭代器遍历 +Iterator iterator = names.iterator(); +while (iterator.hasNext()) { + System.out.print(iterator.next() + ",");//Java中的迭代器接口是第二种定义方式,next +} +``` +实际上,foreach 循环只是一个语法糖而已,底层是基于迭代器来实现的。也就是说,上面代码中的第二种遍历方式(foreach 循环代码)的底层实现,就是第三种遍历方式(迭代器遍历代码)。这两种遍历方式可以看作同一种遍历方式,也就是迭代器遍历方式。 + + +意义: +对于类似数组和链表这样的数据结构,遍历方式比较简单,直接使用 for 循环来遍历就足够了。但是,对于复杂的数据结构(比如树、图)来说,有各种复杂的遍历方式。比如,树有前中后序、按层遍历,图有深度优先、广度优先遍历等等。如果由客户端代码来实现这些遍历算法,势必增加开发成本,而且容易写错。如果将这部分遍历的逻辑写到容器类中,也会导致容器类代码的复杂性。 + +前面也多次提到,应对复杂性的方法就是拆分。我们可以将遍历操作拆分到迭代器类中。比如,针对图的遍历,我们就可以定义 DFSIterator、BFSIterator 两个迭代器类,让它们分别来实现深度优先遍历和广度优先遍历。 + +其次,将游标指向的当前位置等信息,存储在迭代器类中,每个迭代器独享游标信息。这样,我们就可以创建多个不同的迭代器,同时对同一个容器进行遍历而互不影响。最后,容器和迭代器都提供了抽象的接口,方便我们在开发的时候,基于接口而非具体的实现编程。当需要切换新的遍历算法的时候,比如,从前往后遍历链表切换成从后往前遍历链表,客户端代码只需要将迭代器类从 LinkedIterator 切换为 ReversedLinkedIterator 即可,其他代码都不需要修改。除此之外,添加新的遍历算法,我们只需要扩展新的迭代器类,也更符合开闭原则。 \ No newline at end of file diff --git a/Chapter6 - Design Pattern/6.26.md b/Chapter6 - Design Pattern/6.26.md new file mode 100644 index 0000000..63a2510 --- /dev/null +++ b/Chapter6 - Design Pattern/6.26.md @@ -0,0 +1,320 @@ +# 访问者模式 + +> 访问者模式可以算是 23 种经典设计模式中最难理解的几个之一。因为它难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式 + +## 定义 +访问者者模式的英文翻译是 Visitor Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的: +> Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure. +翻译成中文就是:允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。 + +## 实现 +### 场景一 +假设我们从网站上爬取了很多资源文件,它们的格式有三种:PDF、PPT、Word。我们现在要开发一个工具来处理这批资源文件。这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放到 txt 文件中。 + +``` +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 resourceFiles = listAllResourceFiles(args[0]); + for (ResourceFile resourceFile : resourceFiles) { + resourceFile.extract2txt(); + } + } + private static List listAllResourceFiles(String resourceDirectory) { + List 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; + } +} +``` +问题分析: +如果工具的功能不停地扩展,不仅要能抽取文本内容,还要支持压缩、提取文件元信息(文件名、大小、更新时间等等)构建索引等一系列的功能,那如果我们继续按照上面的实现思路,就会存在这样几个问题 +- 违背开闭原则,添加一个新的功能,所有类的代码都要修改 +- 功能增多,每个类的代码都不断膨胀,可读性和可维护性都变差了 +- 把所有比较上层的业务逻辑都耦合到 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); + } + //... +} +//...PPTFile、WordFile代码省略... + +public class Extractor { + public void extract2txt(PPTFile pptFile) { + //... + System.out.println("Extract PPT."); + } + public void extract2txt(PdfFile pdfFile) { + //... + System.out.println("Extract PDF."); + } + public void extract2txt(WordFile wordFile) { + //... + System.out.println("Extract WORD."); + } +} +public class ToolApplication { + public static void main(String[] args) { + Extractor extractor = new Extractor(); + List resourceFiles = listAllResourceFiles(args[0]); + for (ResourceFile resourceFile : resourceFiles) { + extractor.extract2txt(resourceFile); + } + } + private static List listAllResourceFiles(String resourceDirecto + List 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; + } +} +``` +其中核心的就是将抽取文本的操作,设计成了3个重载函数(在同一个类中,函数名相同、参数不同的一组函数) +不过上面的代码还是有问题 `extractor.extract2txt(resourceFile)` 会报错。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); + } + //... +} +//...PPTFile、WordFile跟PdfFile类似,这里就省略了... +//...Extractor代码不变... + + public class ToolApplication { + public static void main(String[] args) { + Extractor extractor = new Extractor(); + List resourceFiles = listAllResourceFiles(args[0]); + for (ResourceFile resourceFile : resourceFiles) { + resourceFile.accept(extractor); + } + } + private static List listAllResourceFiles(String resourceDirectory) { + List 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; + } +} +``` +`resourceFile.accept(extractor);` 根据多态特性,会调用实际的 accept 函数,比如 PdfFile 的 accept 函数,其中的 this 也就是 PdfFile 类型的,然后调用 accept 传递进来的 extractor 的 extract2txt 方法的 this 也就是 PdfFile,调用到 extractor 的 `public void extract2txt(PPTFile pptFile)` 方法。这也是访问者模式的精髓(也是实际场景中难以想到要问访问者模式的原因,可能是随机想到的刚好命中访问者模式) + + +### 场景二 +如果要继续添加新的功能,比如前面提到的压缩功能,根据不同的文件类型,使用不同的压缩算法来压缩资源文件,那我们该如何实现呢? +1. 需要实现一个类似 Extractor 类的新类 Compressor 类,在其中定义三个重载函数,实现对不同类型资源文件的压缩 +2. 还要在每个资源文件类中定义新的 accept 重载函数 + +``` +public abstract class ResourceFile { + protected String filePath; + public ResourceFile(String filePath) { + this.filePath = filePath; + } + abstract public void accept(Extractor extractor); + abstract public void accept(Compressor compressor); +} +public class PdfFile extends ResourceFile { + public PdfFile(String filePath) { + super(filePath); + } + @Override + public void accept(Extractor extractor) { + extractor.extract2txt(this); + } + @Override + public void accept(Compressor compressor) { + compressor.compress(this); + } + //... +} + +//...PPTFile、WordFile跟PdfFile类似,这里就省略了... +//...Extractor代码不变 + +public class ToolApplication { + public static void main(String[] args) { + Extractor extractor = new Extractor(); + List 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 listAllResourceFiles(String resourceDirectory) { + List 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; + } +} +``` +问题分析:代码还存在一些问题,添加一个新的业务,还是需要修改每个资源文件类,违反了开闭原则。 + +针对这个问题,我们抽象出来一个 Visitor 接口,包含是三个命名非常通用的 visit() 重载函数,分别处理三种不同类型的资源文件。具体做什么业务处理,由实现这个 Visitor 接口的具体的类来决定,比如 Extractor 负责抽取文本内容,Compressor 负责压缩。当我们新添加一个业务功能的时候,资源文件类不需要做任何修改,只需要修改 ToolApplication 的代码就可以了。 +``` +public abstract class ResourceFile { + protected String filePath; + public ResourceFile(String filePath) { + this.filePath = filePath; + } + abstract public void accept(Visitor vistor); +} + +public class PdfFile extends ResourceFile { + public PdfFile(String filePath) { + super(filePath); + } + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + //... +} +//...PPTFile、WordFile跟PdfFile类似,这里就省略了... + +public interface Visitor { + void visit(PdfFile pdfFile); + void visit(PPTFile pdfFile); + void visit(WordFile pdfFile); +} + +public class Extractor implements Visitor { + @Override + public void visit(PPTFile pptFile) { + //... + System.out.println("Extract PPT."); + } + @Override + public void visit(PdfFile pdfFile) { + //... + System.out.println("Extract PDF."); + } + @Override + public void visit(WordFile wordFile) { + //... + System.out.println("Extract WORD."); + } +} + +public class Compressor implements Visitor { + @Override + public void visit(PPTFile pptFile) { + //... + System.out.println("Compress PPT."); + } + @Override + public void visit(PdfFile pdfFile) { + //... + System.out.println("Compress PDF."); + } + @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 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 listAllResourceFiles(String resourceDirectory) { + List 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; + } +} +``` + +## 使用场景 +访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中 + + +访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。 + diff --git a/Chapter6 - Design Pattern/6.27.md b/Chapter6 - Design Pattern/6.27.md new file mode 100644 index 0000000..0b1d516 --- /dev/null +++ b/Chapter6 - Design Pattern/6.27.md @@ -0,0 +1,159 @@ +# 备忘录模式 + +对于大对象的备份和恢复,如何优化内存和时间的消耗 + +## 定义 +备忘录模式,也叫快照(Snapshot)模式,英文翻译是 Memento Design Pattern。在 GoF 的《设计模式》一书中,备忘录模式是这么定义的: +> Captures and externalizes an object’s internal state so that it can be restored later, all without violating encapsulation. +翻译成中文就是:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态 +核心就是2点: +- 存储副本以便后期恢复 +- 在不违背封装原则的前提下,进行对象的备份和恢复 + +## 如何实现 +- 为什么存储和恢复副本会违背封装原则? +- 备忘录模式是如何做到不违背封装原则的? + +编写一个小程序,可以接收命令行的输入。用户输入文本时,程序将其追加存储在内存文本中;用户输入“:list”,程序在命令行中输出内存文本的内容;用户输入“:undo”,程序会撤销上一次输入的文本,也就是从内存文本中将上次输入的文本删除掉。 +``` +>hello +>:list +hello +>world +>:list +helloworld +>:undo +>:list +hello +``` +简易版本 +``` +public class InputText { + private StringBuilder text = new StringBuilder(); + public String getText() { + return text.toString(); + } + public void append(String input) { + text.append(input); + } + public void setText(String text) { + this.text.replace(0, this.text.length(), text); + } +} +public class SnapshotHolder { + private Stack snapshots = new Stack<>(); + public InputText popSnapshot() { + return snapshots.pop(); + } + public void pushSnapshot(InputText inputText) { + InputText deepClonedInputText = new InputText(); + deepClonedInputText.setText(inputText.getText()); + snapshots.push(deepClonedInputText); + } +} + +public class ApplicationMain { + public static void main(String[] args) { + InputText inputText = new InputText(); + SnapshotHolder snapshotsHolder = new SnapshotHolder(); + Scanner scanner = new Scanner(System.in); + while (scanner.hasNext()) { + String input = scanner.next(); + if (input.equals(":list")) { + System.out.println(inputText.toString()); + } else if (input.equals(":undo")) { + InputText snapshot = snapshotsHolder.popSnapshot(); + inputText.setText(snapshot.getText()); + } else { + snapshotsHolder.pushSnapshot(inputText); + inputText.append(input); + } + } + } +} +``` +问题分析:仔细想想遵循备忘录模式吗(要在不违背封装原则的前提下,进行对象的备份和恢复)? +- 为了实现快照恢复功能,在 InputText 类中定义了 `setText` 方法,这个方法名字很普通,就是一个设置文本的方法,而且还是公有方法,外部很容易误调用,也不符合封装原则 +- 快照应该是不可变的,所以不需要对外暴露 set 等用于修改内部状态的方法。且上面的实现复用了 InputText 这个类,用于充当快照类。InputText 同时存在一系列修改内部状态的方法,违背封装原则 + +改进: +- 单独抽取 SnapShot 类,用于快照数据的保存,对外只暴露 get 方法,没有 set 方法去修改内部状态 +- Input 类中,把 setText 改为针对特定场景的 resetSnapShot 方法,标识特定场景,只用于恢复快照数据 + +``` +public class InputText { + private StringBuilder text = new StringBuilder(); + public String getText() { + return text.toString(); + } + public void append(String input) { + text.append(input); + } + public Snapshot createSnapshot() { + return new Snapshot(text.toString()); + } + public void restoreSnapshot(Snapshot snapshot) { + this.text.replace(0, this.text.length(), snapshot.getText()); + } +} + +public class Snapshot { + private String text; + public Snapshot(String text) { + this.text = text; + } + public String getText() { + return this.text; + } +} + +public class SnapshotHolder { + private Stack snapshots = new Stack<>(); + public Snapshot popSnapshot() { + return snapshots.pop(); + } + public void pushSnapshot(Snapshot snapshot) { + snapshots.push(snapshot); + } +} + +public class ApplicationMain { + public static void main(String[] args) { + InputText inputText = new InputText(); + SnapshotHolder snapshotsHolder = new SnapshotHolder(); + Scanner scanner = new Scanner(System.in); + while (scanner.hasNext()) { + String input = scanner.next(); + if (input.equals(":list")) { + System.out.println(inputText.toString()); + } else if (input.equals(":undo")) { + Snapshot snapshot = snapshotsHolder.popSnapshot(); + inputText.restoreSnapshot(snapshot); + } else { + snapshotsHolder.pushSnapshot(inputText.createSnapshot()); + inputText.append(input); + } + } + } +} + +``` +上面的代码实现就是典型的备忘录模式的代码实现,也是很多书籍(包括 GoF 的《设计模式》)中给出的实现方法。 + +## QA +备忘录模式,还有一个跟它很类似的概念,“备份”,有何异同? +两者的应用场景很类似,都应用在防丢失、恢复、撤销等场景中。备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计 + +## 优化内存和时间消耗 +上面的实现存在问题:如果要备份的对象数据比较大,备份频率又比较高,那快照占用的内存会比较大,备份和恢复的耗时会比较长。这个问题该如何解决呢? + +不同的应用场景下有不同的解决方法。比如,我们前面举的那个例子,应用场景是利用备忘录来实现撤销操作,而且仅仅支持顺序撤销,也就是说,每次操作只能撤销上一次的输入,不能跳过上次输入撤销之前的输入。在具有这样特点的应用场景下,为了节省内存,我们不需要在快照中存储完整的文本,只需要记录少许信息,比如在获取快照当下的文本长度,用这个值结合 InputText 类对象存储的文本来做撤销操作。 + +我们再举一个例子。假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘)的消耗,还是对 +时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会采用“**低频率全量备份**”和“**高频率增量备份**”相结合的方法。 + +全量备份就不用讲了,它跟我们上面的例子类似,就是把所有的数据“拍个快照”保存下来。所谓“增量备份”,指的是记录每次操作或数据变动。 + +当我们需要恢复到某一时间点的备份的时候,如果这一时间点有做全量备份,我们直接拿来恢复就可以了。如果这一时间点没有对应的全量备份,我们就先找到最近的一次全量备份, +然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动。这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。 + diff --git a/Chapter6 - Design Pattern/6.28.md b/Chapter6 - Design Pattern/6.28.md new file mode 100644 index 0000000..f72e374 --- /dev/null +++ b/Chapter6 - Design Pattern/6.28.md @@ -0,0 +1,94 @@ +# 命令模式 + +如何利用命令模式实现一个手游后端架构 + +## 定义 +命令模式的英文翻译是 Command Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的: +> 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() { + // 执行相应的逻辑 + } +} +//GotStartCommand、HitObstacleCommand、ArchiveCommand类省略 + +public class GameApplication { + private static final int MAX_HANDLED_REQ_COUNT_PER_LOOP = 100; + private Queue queue = new LinkedList<>(); + public void mainloop() { + while (true) { + List 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(/*数据*/); + } // ...一堆else if... + queue.add(command); + } + + int handledCount = 0; + while (handledCount < MAX_HANDLED_REQ_COUNT_PER_LOOP) { + if (queue.isEmpty()) { + break; + } + Command command = queue.poll(); + command.execute(); + } + } +} +``` + +## 命令模式 VS 策略模式 +命令模式跟策略模式、工厂模式非常相似啊,那它们的区别在哪里呢? + + +实际上,每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可以解决哪类问题;第二部分是解决方案,即这个模式的设计思路和具体的代码实现。不过,代码实现并不是模式必须包含的。如果你单纯地只关注解决方案这一部分,甚至只关注代码实现,就会产生大部分模式看起来都很相似的错觉 + +实际上,设计模式之间的主要区别还是在于设计意图,也就是应用场景。单纯地看设计思路或者代码实现,有些模式确实很相似,比如策略模式和工厂模式。 + +之前讲策略模式的时候,我们有讲到,策略模式包含策略的定义、创建和使用三部分,从代码结构上来,它非常像工厂模式。它们的区别在于,策略模式侧重“策略”或“算法”这个特定的应用场景,用来解决根据运行时状态从一组策略中选择不同策略的问题,而工厂模式侧重封装对象的创建过程,这里的对象没有任何业务场景的限定,可以是策略,但也可以是其他东西。从设计意图上来,这两个模式完全是两回事儿。 + +有了刚刚的铺垫,接下来,我们再来看命令模式跟策略模式的区别。你可能会觉得,命令的执行逻辑也可以看作策略,那它是不是就是策略模式了呢?实际上,这两者有一点细微的区别。 + +在策略模式中,不同的策略具有相同的目的、不同的实现、互相之间可以替换。比如,BubbleSort、SelectionSort 都是为了实现排序的,只不过一个是用冒泡排序算法来实现的,另一个是用选择排序算法来实现的。而在命令模式中,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相之间不可替换。 + diff --git a/Chapter6 - Design Pattern/6.29.md b/Chapter6 - Design Pattern/6.29.md new file mode 100644 index 0000000..81131a6 --- /dev/null +++ b/Chapter6 - Design Pattern/6.29.md @@ -0,0 +1,138 @@ +# 解释器模式 + +## 定义 +解释器模式的英文翻译是 Interpreter Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的 +> Interpreter pattern is used to defines a grammatical representation for a language and provides an interpreter to deal with this grammar. +翻译成中文就是:解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法 + +这里面有很多我们平时开发中很少接触的概念,比如“语言”“语法”“解释器”。实际上,这里的“语言”不仅仅指我们平时说的中、英、日、法等各种语言。从广义上来讲,只要是能承载信息的载体,我们都可以称之为“语言”,比如,古代的结绳记事、盲文、哑语、摩斯密码等。 + +要想了解“语言”表达的信息,我们就必须定义相应的语法规则。这样,书写者就可以根据语法规则来书写“句子”(专业点的叫法应该是“表达式”),阅读者根据语法规则来阅读“句子”,这样才能做到信息的正确传递。而我们要讲的解释器模式,其实就是用来实现根据语法规则解读“句子”的解释器。这个过程类似加解密。通信前,Alice 使用约定好的密钥(语法、文法)进行加密,得到秘文。然后 Bob 收到秘文后同样利用约定好的加密方式(语法、文法)去解密,得到原始信息。 + +## 实现 +假设我们定义了一个新的加减乘除计算“语言”,语法规则如下: +- 运算符只包含加、减、乘、除,并且没有优先级的概念; +- 表达式(也就是前面提到的“句子”)中,先书写数字,后书写运算符,空格隔开; +按照先后顺序,取出两个数字和一个运算符计算结果,结果重新放入数字的最头部位置,循环上述过程,直到只剩下一个数字,这个数字就是表达式最终的计算结果。 + +我们举个例子来解释一下上面的语法规则:比如“ 8 3 2 4 - + * ”这样一个表达式,我们按照上面的语法规则来处理,取出数字“8 3”和“-”运算符,计算得到 5,于是表达式就变成了“ 5 2 4 + * ”。然后,我们再取出“ 5 2 ”和“ + ”运算符,计算得到 7,表达式就变成了“ 7 4 * ”。最后,我们取出“ 7 4”和“ * ”运算符,最终得到的结果就是 28。 + +``` +public class ExpressionInterpreter { + private Deque numbers = new LinkedList<>(); + public long interpret(String expression) { + String[] elements = expression.split(" "); + int length = elements.length; + for (int i = 0; i < (length+1)/2; ++i) { + numbers.addLast(Long.parseLong(elements[i])); + } + for (int i = (length+1)/2; i < length; ++i) { + String operator = elements[i]; + boolean isValid = "+".equals(operator) || "-".equals(operator) || "*".equals(operator) || "/".equals(operator); + if (!isValid) { + throw new RuntimeException("Expression is invalid: " + expression); + } + long number1 = numbers.pollFirst(); + long number2 = numbers.pollFirst(); + long result = 0; + if (operator.equals("+")) { + result = number1 + number2; + } else if (operator.equals("-")) { + result = number1 - number2; + } else if (operator.equals("*")) { + result = number1 * number2; + } else if (operator.equals("/")) { + result = number1 / number2; + } + numbers.addFirst(result); + } + if (numbers.size() != 1) { + throw new RuntimeException("Expression is invalid: " + expression); + } + return numbers.pop(); + } +} +``` +问题分析: +上面的代码实现中,语法解析都写在了 if 判断中了,如果简单的语法规则解析,这样的设计就足够了。但是,对于简单的语法规则解析,这样就足够了。但是,对于复杂的语法规则的解析,逻辑耦合在一起很复杂了,代码量会很多,这在设计上是不合理的。往往这个时候就需要拆分代码,将解析逻辑拆分到独立的子类中。 + +如何拆分? +解释器模式的实现比较灵活,没有固定的模版,主要是思想。核心是将语法解析的工作拆分到各个子类中,以此来避免大而全的解析类。 + +前面定义的语法规则有两类表达式:一类是数字、一类是运算符。运算符包括加减乘除,利用解析器模式,把解析工作拆分到 NumberExpression、AdditionExpression、SubstractionExpress、MultiplicationExpression、DivisionExpression 解析类。 + +代码如下: +``` +public interface Expression { + long interpret(); +} + +public class NumberExpression implements Expression { + private long number; + public NumberExpression(long number) { + this.number = number; + } + public NumberExpression(String number) { + this.number = Long.parseLong(number); + } + @Override + public long interpret() { + return this.number; + } +} + +public class AdditionExpression implements Expression { + private Expression exp1; + private Expression exp2; + public AdditionExpression(Expression exp1, Expression exp2) { + this.exp1 = exp1; + this.exp2 = exp2; + } + @Override + public long interpret() { + return exp1.interpret() + exp2.interpret(); + } +} +// SubstractionExpression、MultiplicationExpression、DivisionExpression、AdditionExpression +public class ExpressionInterpreter { + private Deque numbers = new LinkedList<>(); + public long interpret(String expression) { + String[] elements = expression.split(" "); + int length = elements.length; + for (int i = 0; i < (length+1)/2; ++i) { + numbers.addLast(new NumberExpression(elements[i])); + } + for (int i = (length+1)/2; i < length; ++i) { + String operator = elements[i]; + boolean isValid = "+".equals(operator) || "-".equals(operator) || "*".equals(operator) || "/".equals(operator); + if (!isValid) { + throw new RuntimeException("Expression is invalid: " + expression); + } + Expression exp1 = numbers.pollFirst(); + Expression exp2 = numbers.pollFirst(); + Expression combinedExp = null; + if (operator.equals("+")) { + combinedExp = new AdditionExpression(exp1, exp2); + } else if (operator.equals("-")) { + combinedExp = new AdditionExpression(exp1, exp2); + } else if (operator.equals("*")) { + combinedExp = new AdditionExpression(exp1, exp2); + } else if (operator.equals("/")) { + combinedExp = new AdditionExpression(exp1, exp2); + } + long result = combinedExp.interpret(); + numbers.addFirst(new NumberExpression(result)); + } + if (numbers.size() != 1) { + throw new RuntimeException("Expression is invalid: " + expression); + } + return numbers.pop().interpret(); + } +} +``` + +## 场景 +- Java 中注解处理器做的就是解释功能 +- 前端编译时的语法分析、语义分析 +- 语言编译时生成的中间代码 IR,用于编译后端的更多编译优化 +- 自定义一门语言,然后通过语法解析器去分析读入 \ No newline at end of file diff --git a/Chapter6 - Design Pattern/6.30.md b/Chapter6 - Design Pattern/6.30.md new file mode 100644 index 0000000..ed5a68f --- /dev/null +++ b/Chapter6 - Design Pattern/6.30.md @@ -0,0 +1,163 @@ +# 中介模式 +什么时候用中介模式?什么时候用观察者模式 + +## 定义 +中介模式的英文翻译是 Mediator Design Pattern。在 GoF 中的《设计模式》一书中,它是这样定义的: +> Mediator pattern defines a separate (mediator) object that encapsulates the interaction between a set of objects and the objects delegate their interaction to a mediator object instead of interacting with each other directly. +翻译成中文就是:中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。 + +实际上,中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。 +![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/Mediator-Pattern-advantage.png) + +## 实现 +提到中介模式,有一个比较经典的例子不得不说,那就是航空管制。为了让飞机在飞行的时候互不干扰,每架飞机都需要知道其他飞机每时每刻的位置,这就需 +要时刻跟其他飞机通信。飞机通信形成的通信网络就会无比复杂。这个时候,我们通过引入“塔台”这样一个中介,让每架飞机只跟塔台来通信,发送自己的位置给塔台,由塔台来负责每架飞机的航线调度。这样就大大简化了通信网络 + +再一个例子:对话框中有很多控件,比如按钮、文本框、下拉框等。当我们对某个控件进行操作的时候,其他控件会做出相应的反应,比如,我们在下拉框中选择“注册”,注册相关的控件就会显示在对话框中。如果我们在下拉框中选择“登陆”,登陆相关的控件就会显示在对话框中。 + +``` +public class UIControl { + private static final String LOGIN_BTN_ID = "login_btn"; + private static final String REG_BTN_ID = "reg_btn"; + private static final String USERNAME_INPUT_ID = "username_input"; + private static final String PASSWORD_INPUT_ID = "pswd_input"; + private static final String REPEATED_PASSWORD_INPUT_ID = "repeated_pswd_input + private static final String HINT_TEXT_ID = "hint_text"; + private static final String SELECTION_ID = "selection"; + public static void main(String[] args) { + Button loginButton = (Button)findViewById(LOGIN_BTN_ID); + Button regButton = (Button)findViewById(REG_BTN_ID); + Input usernameInput = (Input)findViewById(USERNAME_INPUT_ID); + Input passwordInput = (Input)findViewById(PASSWORD_INPUT_ID); + Input repeatedPswdInput = (Input)findViewById(REPEATED_PASSWORD_INPUT_ID); + Text hintText = (Text)findViewById(HINT_TEXT_ID); + Selection selection = (Selection)findViewById(SELECTION_ID); + loginButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + String username = usernameInput.text(); + String password = passwordInput.text(); + //校验数据... + //做业务处理... + } + }); + regButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + //获取usernameInput、passwordInput、repeatedPswdInput数据... + //校验数据... + //做业务处理... + } + }); + //...省略selection下拉选择框相关代码.... + } +} +``` +问题分析:登陆按钮的点击事件要处理 usernameInput、passwordInput、repeatedPasswordInput 等对象,其他按钮的点击事件也是一样。也就是不同对象存在互相通信(事件、访问),可以通过中介者模式来解决 +``` +public interface Mediator { + void handleEvent(Component component, String event); +} + +public class LandingPageDialog implements Mediator { + private Button loginButton; + private Button regButton; + private Selection selection; + private Input usernameInput; + private Input passwordInput; + private Input repeatedPswdInput; + private Text hintText; + + @Override + public void handleEvent(Component component, String event) { + if (component.equals(loginButton)) { + String username = usernameInput.text(); + String password = passwordInput.text(); + //校验数据... + //做业务处理... + } else if (component.equals(regButton)) { + //获取usernameInput、passwordInput、repeatedPswdInput数据... + //校验数据... + //做业务处理... + } else if (component.equals(selection)) { + selectedItem = selection.select(); + if (selectedItem.equals("login")) { + usernameInput.show(); + passwordInput.show(); + repeatedPswdInput.hide(); + hintText.hide(); + //...省略其他代码 + } else if (selectedItem.equals("register")) { + //.... + } + } + } +} + +public class UIControl { + private static final String LOGIN_BTN_ID = "login_btn"; + private static final String REG_BTN_ID = "reg_btn"; + private static final String USERNAME_INPUT_ID = "username_input"; + private static final String PASSWORD_INPUT_ID = "pswd_input"; + private static final String REPEATED_PASSWORD_INPUT_ID = "repeated_pswd_input + private static final String HINT_TEXT_ID = "hint_text"; + private static final String SELECTION_ID = "selection"; + + public static void main(String[] args) { + Button loginButton = (Button)findViewById(LOGIN_BTN_ID); + Button regButton = (Button)findViewById(REG_BTN_ID); + Input usernameInput = (Input)findViewById(USERNAME_INPUT_ID); + Input passwordInput = (Input)findViewById(PASSWORD_INPUT_ID); + Input repeatedPswdInput = (Input)findViewById(REPEATED_PASSWORD_INPUT_ID); + Text hintText = (Text)findViewById(HINT_TEXT_ID); + Selection selection = (Selection)findViewById(SELECTION_ID); + + Mediator dialog = new LandingPageDialog(); + dialog.setLoginButton(loginButton); + dialog.setRegButton(regButton); + dialog.setUsernameInput(usernameInput); + dialog.setPasswordInput(passwordInput); + dialog.setRepeatedPswdInput(repeatedPswdInput); + dialog.setHintText(hintText); + dialog.setSelection(selection); + + loginButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + dialog.handleEvent(loginButton, "click"); + } + }); + regButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + dialog.handleEvent(regButton, "click"); + } + }); + //.... + } +} +``` +分析:从代码中我们可以看出,原本业务逻辑会分散在各个控件中,现在都集中到了中介类中。 +实际上,这样做既有好处,也有坏处。好处是简化了控件之间的交互,坏处是中介类有可能会变成大而复杂的“上帝类”(God Class)。 +所以,在使用中介模式的时候,我们要根据实际的情况,平衡对象之间交互的复杂度和中介类本身的复杂度 + +## 中介模式 VS 观察者模式 +观察者模式有多种实现方式。虽然经典的实现方式没法彻底解耦观察者和被观察者,观察者需要注册到被观察者中,被观察者状态更新需要调用观察者的 update() 方法。但是,在跨进程的实现方式中,我们可以利用消息队列实现彻底解耦,观察者和被观察者都只需要跟消息队列交互,观察者完全不知道被观察者的存在,被观察者也完全不知道观察者的存在。 + +中介模式也是为了解耦对象之间的交互,所有的参与者都只与中介进行交互。而观察者模式中的消息队列,就有点类似中介模式中的“中介”,观察者模式的中观察者和被观察者,就有点类似中介模式中的“参与者”。 + +那问题来了:中介模式和观察者模式的区别在哪里呢?什么时候选择使用中介模式?什么时候选择使用观察者模式呢? + +在观察者模式中,尽管一个参与者既可以是观察者,同时也可以是被观察者,但是,大部分情况下,交互关系往往都是单向的,一个参与者要么是观察者,要么是被观察者,不会兼具两种身份。也就是说,在观察者模式的应用场景中,参与者之间的交互关系比较有条理。 + +而中介模式正好相反。只有当参与者之间的交互关系错综复杂,维护成本很高的时候,我们才考虑使用中介模式。毕竟,中介模式的应用会带来一定的副作用,前面也讲到,它有可能会产生大而复杂的上帝类。除此之外,如果一个参与者状态的改变,其他参与者执行的操作有一定先后顺序的要求,这个时候,中介模式就可以利用中介类,通过先后调用不同参与者的方法,来实现顺序的控制,而观察者模式是无法实现这样的顺序要求的。 + +总结:观察者模式和中介模式都是为了实现参与者之间的解耦,简化交互关系。两者的不同在于应用场景上。在观察者模式的应用场景中,参与者之间的交互比较有条理,一般都是单向的,一个参与者只有一个身份,要么是观察者,要么是被观察者。而在中介模式的应用场景中,参与者之间的交互关系错综复杂,既可以是消息的发送者、也可以同时是消息的接收者。 + +想到了现在流行的微服务,注册中心可以理解为广义的中介模式,防止各个服务间错综复杂的调用。 + +## QA + +EventBus 框架。当时我们认为它是观察者模式的实现框架。EventBus 作为一个事件处理的中心,事件的派送、订阅都通过这个中心来完成,那是不是更像中介模式的实现框架呢? + +eventbus更属于观察者模式。首先eventbus中不处理业务逻辑,只提供了对象与对象之间交互的管道;而中介模式为了解决多个对象之间交互的问题,将多个对象的行为封装到一起(中介),然后任意对象和这个中介交互,中介中包含了具体业务逻辑。其次从其实现的思路上,EventBus 和观察者都需要定义 Observer,并且通过 register() 函数注册 Observer,也都需要通过调用某个函数(比如,EventBus 中的 post() 函数)来给 Observer 通信 \ No newline at end of file diff --git a/Chapter6 - Design Pattern/6.31.md b/Chapter6 - Design Pattern/6.31.md new file mode 100644 index 0000000..8bcf7c5 --- /dev/null +++ b/Chapter6 - Design Pattern/6.31.md @@ -0,0 +1,36 @@ +# 设计模式怎么应用?如何避免过度设计、又如何避免设计不足 + +## 现状 +面对代码,经常存在两种情况。 + +一种是过度设计,在开始写代码之前,会花很长时间做技术方案上(技术方案时间长短没有统一标准),会花很多时间在代码设计上,打算用到各种设计模式,美其名曰“未雨绸缪、面向未来、业务可迁移”,但可能伴随的问题是,未来的业务需求怎么演进是个未知数,领域如何发展也是未知数,在最早期就过度设计会导致大大增加代码复杂度,以后的开发都要站在这套复杂的设计之上进行开发、设计、维护、修改了(举个例子:Redux 分层太多、角色太多的好处之一是角色清晰、各司其职,但坏处也是显而易见的,简单的场景,杀鸡用了牛刀) + +另一种是不咋设计,技术/业务项目以来,简单分析后,立马操刀就干,代码能跑就行,业务项目甚至是面向 PRD 编程,PRD 罗列的 case 直接用代码翻译一下,没有设计思想,谈何面向未来、高内聚低耦合、开闭、里氏替换?不存在的。改动一个小点,可能要改很多处代码,前一发而动全身,后人接手也不敢碰。 + +那如何做到不过度设计,也避免设计不足的问题? + +## 设计初衷是提高代码质量 +马斯克经常会提到“第一性原理”,我也赞同这个研究方式和思维方式。写代码,包括应用设计模式也是如此。设计模式只是方法,只是工具,最后的目的,核心的诉求是:提高代码的可读性、可拓展性、可维护性。所有的设计都围绕这个目的来展开。 + +所以拿到项目或者问题,先问几个问题:为什么要这样设计、为什么要用这个设计模式、这样做是否真正的提高代码质量、提高代码哪些方面的质量、怎么样提高可维护性、可拓展性。如果这几个问题回答清楚了,那基本就没什么问题了(为什么是基本?软件工程需要考虑 ROI)。如果答案是觉得还行,但是解决的问题不够痛、价值没那么大,那可能当前的方案有点为了设计而设计了。 + +换个角度思考,设计模式是招式、设计原则/思想是心法。低阶的江湖侠客追求的是招式,大宗师追求的是心法。掌握心法,以不变应万变,无招胜有招。所以掌握设计思想更重要,设计思想比设计模式更普适,23种设计模式是之前的人总结出来的,说不定过个几十年又诞生几种新的设计模式,掌握设计思想的话可以面向未来,甚至你之前写的某个设计,刚好和新诞生的设计模式有异曲同工之妙,甚至后来才意识到,我的这个实现原来是最近新出来的某某某设计模式 + +## 先有问题,再有方案 +如果我们把写出的代码看作产品,那做产品的时候,我们先要思考痛点在哪里,用户的真正需求在哪里,然后再看要开发哪些功能去满足,而不是先拍脑袋想出一个花哨的功能,再去东搬西凑硬编出一个需求来。 + +代码设计也是类似的。我们先要去分析代码存在的痛点,比如可读性不好、可扩展性不好等等,然后再针对性地利用设计模式去改善,而不是看到某个场景之后,觉得跟之前在某本书中看到的某个设计模式的应用场景很相似,就套用上去,也不考虑到底合不合适,最后如果有人问起了,就再找几个不痛不痒、很不具体的伪需求来搪塞,比如提高了代码的扩展性、满足了开闭原则等等 + +## 设计的应用场景是复杂的代码 +很多设计模式相关的书籍都会举一些简单的例子,这些例子仅仅具有教学意义,只是为了讲解设计模式的原理和实现,力求在有限篇幅内给你讲明白。而很多人就会误以为这些简单的例子就是这些设计模式的典型应用场景,常常照葫芦画瓢地应用到自己的项目中,用复杂的设计模式去解决简单的问题,还振振有词地说某某经典书中就是这么写的。在我看来,这是很多初学者因为缺乏经验,在学完设计模式之后,在项目中过度设计的首要原因。 + +设计模式要干的事情就是解耦,也就是利用更好的代码结构将一大坨代码拆分成职责更单一的小类,让其满足高内聚低耦合等特性。创建型模式是将创建和使用代码解耦,结构型模式是将不同的功能代码解耦,行为型模式是将不同的行为代码解耦。而解耦的主要目的是应对代码的复杂性。**设计模式就是为了解决复杂代码问题而产生的** + +对于复杂代码,比如项目代码量多、开发周期长、参与开发的人员多,我们前期要多花点时间在设计上,越是复杂代码,花在设计上的时间就要越多。 +不仅如此,每次提交的代码,都要保证代码质量,都要经过足够的思考和精心的设计,这样才能避免烂代码效应(每次提交的代码质量都不是太好,最终积累起来整个项目的质量就变得很差)。 + +## 持续重构能有效避免过度设计 +比如一个很简单的场景简单的 MVC 加几个 Handler 就可以实现,但是有些小伙伴学完前端 Redux 觉得好酷,就要在客户端落地这个实现,导致平白增加几个角色,带来的好处就是角色多、各司其职、边界清晰、增加可拓展性,但缺点也很明显,降低代码的可读性、提高复杂度。一旦引入新的某个设计,不可能就可以随意下掉,也就是说在接下来的很长一段时间里,修改、维护某个简单的功能就需要遵循这个复杂的设计(框架苦开发久已) + +所以为了避免过度设计,避免因需求误判而导致的过度设计,推荐持续重构的方法。持续重构不仅可以保证代码质量,也可以避免过度设计。面对真正有痛点的设计,再去用合适的设计模式去解决,而不是一下子全部推翻,从项目管理上和质量方面来说,这个做法很冒险、不够健康 + diff --git a/SUMMARY.md b/SUMMARY.md index f20e6ce..869a3f0 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -206,7 +206,14 @@ * [21、模板模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.21.md) * [22、模板模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.22.md) * [23、职责链模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.23.md) - + * [24、状态模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.24.md) + * [25、迭代器模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.25.md) + * [26、访问者模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.26.md) + * [27、备忘录模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.27.md) + * [28、命令模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.28.md) + * [29、解释器模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.29.md) + * [30、中介模式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.30.md) + * [31、设计模式怎么应用?如何避免过度设计、又如何避免设计不足](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter6%20-%20Design%20Pattern/6.31.md) * [Chapter7 - Geek Talk](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/chapter7.md) * [1、命令行文件查找](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.1.md) * [2、Charles 从入门到精通](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.2.md) diff --git a/assets/IteratorPatter-structure.png b/assets/IteratorPatter-structure.png new file mode 100644 index 0000000..697ccc8 Binary files /dev/null and b/assets/IteratorPatter-structure.png differ diff --git a/assets/Mediator-Pattern-advantage.png b/assets/Mediator-Pattern-advantage.png new file mode 100644 index 0000000..9516cae Binary files /dev/null and b/assets/Mediator-Pattern-advantage.png differ diff --git a/assets/StateMachine-TabMethod.png b/assets/StateMachine-TabMethod.png new file mode 100644 index 0000000..b819f80 Binary files /dev/null and b/assets/StateMachine-TabMethod.png differ diff --git a/assets/StateMachinePatter-Mariogame.png b/assets/StateMachinePatter-Mariogame.png new file mode 100644 index 0000000..cb30c12 Binary files /dev/null and b/assets/StateMachinePatter-Mariogame.png differ