/head-first-design-patterns-learning

Sherlocky learning in Head First 设计模式

Primary LanguageJavaMIT LicenseMIT

Sherlocky learning in Head First 设计模式

MIT Licence stable

本例代码基于 Gradle 4.10、Java 8 构建。

记住,知道抽象、继承、多态这些概念,并不会马上让你变成好的面向对象设计者。设计大师关心的是建立弹性的设计,可以维护,可以应付变化。
建立可维护的OO系统,要诀就在于随时想到系统以后可能需要的变化,以及应付变化的原则。
良好的OO设计必须具备可复用、可扩充、可维护三个特性。

使用模式最好的方式是:“把模式装进脑子里,然后在你的设计和已有的应用中,寻找何处可以使用他们。“以往是代码复用,现在是经验复用。

设计原则:

  • 封装变化(找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起)。

  • 针对接口编程,而不是针对实现编程。

  • 多用组合,少用继承。

  • 为了交互对象之间的**松耦合**设计而努力。

    松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的相互依赖降到了最低。

  • 类应该对扩展开放,对修改关闭。

    虽然似乎有点矛盾,但是的确有一些技术可以允许在不直接修改代码的情况下,对其进行扩展。
    在选择需要被扩展的代码部分时要小心。每个地方都采用开闭原则是一种浪费,也没必要,还会导致代码变得复杂且难以理解。
    遵循开闭原则通常会引入新的抽象层次,增加代码的复杂度。我们应该把注意力集中在设计中最有可能改变的地方,然后应用开闭原则。

  • 要依赖抽象,不要依赖具体类(依赖倒置原则)。

    不能让高层组件依赖底层组件,而且,不管高层或低层组件,“两者”都应该依赖于抽象。
    指导方针:

    • 变量不可以持有具体类的引用。
    • 不要让类派生自具体类。
    • 不要覆盖基类中已实现的方法。
  • 最少知识原则:只和你的密友谈话。

    不管是任何对象,你都要注意他所交互的类有哪些,并注意它和这些类是如何交互的。 我们在设计中不要让太多的类耦合在一起,免得修改系统中一部分,会影响到其他部分。

    方针:就任何对象而言,在该对象的方法内,我们只应该调用属于以下范围的方法:

    • 该对象本身
    • 被当做方法的参数而传递进来的对象
    • 此方法所创建或实例化的任何对象
    • 对象的任何组件
  • 好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。

我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。
换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你”。

  • 单一责任原则:一个类应该只有一个引起变化的原因。

类的每个责任都有改变的潜在区域。超过一个责任意味着超过一个改变的区域。(高内聚的)

策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户(使用组合,组合类实现了整个算法)。

观察者模式定义了对象之间一对多依赖(关系),这样一来,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

实现观察者模式的方法不只一种,但是以包含SubjectObserver接口的类设计的做法最常见。

当两个对象之间松耦合,他们依然可以交互,但是不太清楚彼此的细节。观察者模式提供了一种对象设计,让主题和观察者之间松耦合。主题(可观察者)用一个共同接口来更新观察者。观察者和可观察者之间用松耦合的方式结合(loosecoupling),可观察者不知道观察者的细节,只知道观察者实现了观察者接口。

使用此模式时,可以从被观察者处推(push)或拉(pull)数据(然而,推的方式被认为更”正确“)。

MVC 是观察者模式的代表人物,Swing也大量使用观察者模式(许多GUI框架也是如此)。此模式也被应用在如:JavaBeans、RMI 等地方。

装饰者模式动态地将责任附加到对象上。若要扩展功能,装饰者提供了比集成更有弹性的替代方案。

  • 集成属于扩展形式之一,但不见得是达到弹性设计的最佳方式。
  • 组合和委托可用于在运行时动态的加上新的行为。
  • 装饰者需要和被装饰者(亦即被包装的组件)拥有相同的“接口”,因为装饰者必须能够取代被装饰者。此处继承是为了有正确的类型(为了能够取代被装饰者),而不是继承他的行为。
  • 将装饰者与组件组合时,就是在加入新的行为。所得到的新行为,并不是继承自超类,而是由组合对象得来的。

Java I/O 中很多类都使用了装饰者模式。例如:

BufferedInputStream/LineNumberInputStream/PushbackInputStream/DataInputStream 等 都扩展自 FilterInputStream,而 FilterInputStream 是一个抽象的装饰类,对应的抽象组件是 InputStream。另外, Reader/Writer(基于字符数据的输入输出)和输入流/输出流的类相当类似(虽然有一些小的差异和不一致之处,但是相当雷同)。

Java I/O 也引出装饰者模式的一个“缺点”:利用装饰者模式,经常造成设计中有大量的小类,数量实在太多,可能会造成使用此API程序员的困扰。

所有的工厂都是用来封装对象的创建。

工厂模式可以分为三类:

  • 1)简单工厂(Simple Factory):又叫做静态工厂方法(Static Factory Method),其实并非一种模式,更多的是编程习惯。

    【符合单一职责原则。不符合开放-封闭原则】 用来生产同一等级结构中的任意产品。(对于增加新的产品,主要是新增产品,就要修改工厂类。

  • 2)工厂方法模式(Factory Method):用来生产同一等级结构中的固定产品。定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。(使用继承)

    【符合单一职责原则、符合开放-封闭原则。但是引入了复杂性】
    工厂方法让类把实例化推迟到子类。工厂方法模式能够封装具体类型的实例化(封装对象的创建过程)。

    简单工厂把全部的事情在一个地方都处理完了,然而工厂方法确实创建一个框架,让子类决定要如何实现。
    简单工厂的做法可以将对象的创建封装起来,但是简单工厂不具备工厂方法的弹性,因为简单工厂不能变更正在创建的产品。

  • 3)抽象工厂模式(Abstract Factory):(又称为Kit模式)用来生产不同产品族的全部产品。提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。(使用对象组合)

    【符合单一职责原则,部分符合开放-封闭原则,降低了复杂性。】
    抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道实际产出的具体产品是什么。
    客户就从具体的产品中被解耦。
    抽象工厂模式提供了一种方式,可以将同一产品族的单独的工厂封装起来。在正常使用中,客户端程序需要创建抽象工厂的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。
    客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。
    抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。

05 Singleton Pattern 单例模式

确保一个类只有一个实例,并提供一个全局访问点。

参见:sherlocky/interview/singleton

【问题】两个类加载器可能有机会各自创建自己的单例实例?
因为每个类加载器都定义了一个命名空间,如果有两个以上的类加载器, 不同的类加载器可能会加载同一个类,从整个程序来看,同一个类会被加载多次。
所以,如果你的程序有多个类加载器又同时使用了单例模式,请小心。
有一个解决办法:自行指定类加载器,并指定同一个类加载器。

将“请求”封装成对象,以便使用不同的请求,队列或者日志请求来参数化其他对象。
命令模式也支持可撤销的操作。

  • 命令模式将发出请求的对象和执行请求的对象解耦。
  • 在被解耦的两者之间是通过命令对象进行沟通的。命令对象封装了接收者和一个或一组动作。
  • 调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用。
  • 调用者可以接受命令当做参数,甚至在运行时动态地进行。
  • 命令可以支持撤销,做法是实现一个undo()方法来回到execute()被执行前的状态。
  • 宏命令是命令的一种简单的延伸,允许调用多个命令。宏方法也可以支持撤销。
  • 命令也可以用来实现日志和事务系统。

将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。

客户使用适配器(之间是解耦的)的过程:

  • 1.客户通过目标接口调用适配器的方法对适配器发出请求。
  • 2.适配器使用被适配者接口把请求转换成被适配者的一个或多个调用接口。
  • 3.客户接收到调用的结果,但并未察觉这一切是适配器在起转换作用。

外观模式也称为门面模式,提供了一个统一(简化)的接口,用来访问子系统中的一群接口,本质是:封装交互,简化调用。外观定义了一个高层接口,让子系统更容易使用。同时依然将系统完整的功能暴露出来,以供需要的人使用。

外观不只是简化了接口,也将客户从组建的子系统中解耦。

外观和适配器可以包装许多类,但是外观的意图是简化接口,而适配器的意图是将接口转换成不同接口。

装饰者模式 VS 适配器模式 VS 外观模式

装饰者将一个对象“包装”起来以增加新的行为和责任;
适配器将一个对象“包装”起来以改变其接口;
而外观将一群对象“包装”起来以简化其接口。

模板方法模式在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

这个模式是用来创建一个算法的模板。其实模板就是一个方法,更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。

JDK 中的 Arrays.sort() 方法可以被看做是一个模板方法(尽管不是教科书式的实现,但是依然符合模板方法的精神)。
数组所实现的排序算法并不完整,它需要一个类填补 compareTo() 方法的实现,因此我们认为这更像一个模板方法。
类似的还有:java.io.InputStream#read() 方法,是由子类实现的,而这个方法又被被 read(byte b[], int off, int len) 模板方法使用。

钩子方法(默认不做事):

是对于抽象方法或者接口中定义的方法的一个空的或者默认的实现,子类可以视情况决定要不要覆盖他们,如果不覆盖,就使用抽象类提供的默认实现(要不要挂钩由子类决定)。

模板方法模式 VS 策略模式

策略模式(通过组合封装算法):定义一个算法家族,并让这些算法可以互换(组合类实现了整个算法)。
模板方法(通过继承封装算法):定义一个算法大纲,而由子类定义其中某些步骤的内容(个别步骤可以有不同的实现细节,但算法结构维持不变)。
工厂方法是模板方法的一种特殊版本。

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示(实现)。

  • 1.迭代器模式让我们能游走于聚合内的每一个元素,而又不暴露其内部表示。
  • 2.把游走的任务交给迭代器,而不是聚合类。这样简化了聚合的接口和实现,也让责任各得其所。

组合模式允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

组合模式让我们能用树形方式创建对象的结构,树里面包含了组合以及个别的对象。
使用组合结构,我们能把相同的操作应用在组合和个别对象上。
换句话说,在大多数情况下,我们可以忽略对象组合和个别对象之间的差别。

组合持有一群孩子,这些孩子可以是别的组合或者叶节点元素。用这种方式组织数据,最终会得到由上而下的树形结构,根部是一个组合,而组合的分支逐渐往下延伸,直到叶节点为止。

组合模式以单一责任设计原则换取透明性(也就是说一个元素究竟是组合还是叶节点,对客户是透明的)。
组合模式是一个很典型的折衷案例,有时候,我们会故意做一些看似违反原则的事情。
我们要根据需要,平衡透明性和安全性。

状态模式允许对象在内部状态改变时改变他的行为,对象看起来好像修改了它的类。

这个模式将状态封装成为独立的类,并将动作委托到代表当前状态的对象。

类图分析:

1、Context(上下文)是一个类,它可以拥有一些内部状态。不管任何时候只要有人调用 Context 的 request() 方法,它就会被委托到状态来处理。
2、State 接口定义了一个所有具体状态的共同接口,任何状态都实现这个相同的 接口,这样一来,状态之间可以互相替换。
3、ConcreteState(具体状态)处理来自 Context 的请求,每一个具体状态都提供了它自己对于请求的实现。所以当 Context 改变状态时行为也跟着改变。 4、状态转换可以由 State 类或 Context 类控制。

状态模式 VS 策略模式

状态模式: 将一群行为封装在状态对象中,Context 的行为随时可以委托到那么状态对象中的一个。随着时间的流逝,当前状态在状态集合中游走改变,以反映出 Context 内部的状态,因此,Context 的行为也会跟着改变,但客户对于状态对象了解不多,甚至根本是浑然不觉。
我们把状态模式想成是不用在 Context 中放置许多条件判断的替代方案。通过将行为包装进状态对象中,你可以通过在 Context 内简单地改变状态对象来改变 Context 的行为。

策略模式: 客户通常主动指定 Context 所要组合的策略对象是哪一个。
一般来说,我们把策略模式想成是除了继承之外的一种弹性替代方案。如果你使用继承定义了一个类的行为,你将被这个行为困住,甚至想要修改都很难。有了策略模式,可以通过组合不同的对象来改变行为。

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。

使用代理模式创建代表对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。

代理模式可以以很多形式呈现。

远程代理管理客户和远程对象之间的交互。远程代理可以作为另一个 JVM 上对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。

通过调用代理的方法,远程调用可以跨过网路,返回字符串、整数等信息。调用的方法实际会在远程执行,客户端根本就不知道。

Java RMI

RMI 提供了客户辅助对象和服务辅助对象,为客户辅助对象创建和服务对象相同的方法。
RMI 将客户服务对象称为:stub(桩),服务辅助对象称为:skeleton(骨架)。

RMI 远程方法调用步骤:

  • 1、客户调用客户端辅助对象 stub 上的方法。
  • 2、客户端辅助对象 stub 打包调用信息(变量、方法名),通过网络发送给服务端辅助对象 skeleton。
  • 3、服务端辅助对象 skeleton 将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象。
  • 4、调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象 skeleton。
  • 5、服务端辅助对象将结果打包,发送给客户端辅助对象 stub。
  • 6、客户端辅助对象将返回值解包,返回给调用者。
  • 7、客户获得返回值。

虚拟代理控制访问实例化开销化大的对象,作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要一个对象的时候才去创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。

保护代理基于调用者控制对对象方法的访问,是一种根据访问权限决定客户可否访问对象的代理。可以借助 Java 的动态代理来实现保护代理。

Java 在java.lang.reflect包中有自己的代理支持,可以在运行时动态创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类时在运行时创建的,所以这个技术也被称为:动态代理。

Java 的动态代理依靠接口实现,如果有些类并没有实现接口,则不能使用 JDK 代理,可以使用 cglib 动态代理了。
cglib 是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

4.其他代理

  • 防火墙代理(Firewall Proxy)

控制网络资源的访问,保护主题免于“坏客户”的侵害。

  • 智能引用代理(Smart Reference Proxy)

当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数。

  • 缓存代理(Caching Proxy)

为开销大的运算结果提供暂时存储,它也允许多个客户共享结果,以减少计算或网络延迟。

  • 同步代理(Synchronization Proxy)

在多线程的情况下为主题提供安全的访问。

  • 复杂隐藏代理(Complexity Hiding Proxy)

用来隐藏一个类的复杂集合的复杂度,并进行访问控制。有时候也称为 外观代理(Facade Proxy)。复杂隐藏代理和外观模式是不一样的,因为代理控制访问,而外观模式只提供另一组接口。

  • 写入时复制代理(Copy-On-Write Proxy)

用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。可以参考java.util.concurrent.CopyOnWriteArrayList

代理在结构上类似装饰者,但是目的不同。装饰者为对象加上行为,而代理是控制访问。和其他包装者一样,代理会造成设计中类的数目增加。

模式通常被一起使用,并被组合在同一个设计解决方案中。复合模式在一个解决方案中结合两个或多个模式,以解决一般或重复发生的问题。

设计模式是 MVC 的钥匙。MVC 是由数个设计模式结合起来的模式。

采用模式时必须要考虑到这么做是否有意义,绝对不能为了使用模式而使用模式。