统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息 UML从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等
类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部构造以及它们与其他类的关系。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
在UML类图中,类使用包含类名、属性(field)和方法(method),且带有分割线的矩形来表示。
属性/方法前的加减号表示可见性
属性的完整表示:可见性 名称:类型 [ = 初始化值] 方法的完整表示:可见性 名称(参数) [ :返回值类型]
关联关系是对象之间的一种引用关系,用于表示一类对象和另一类对象之间的联系,分为一般关联关系、聚合关系和组合关系。
一般关联关系分为单项关联、双向关联、自关联
关联关系有单向关联和双向关联。如果两个对象都知道(即可以调用)对方的公共属性和操作,那么二者就是双向关联。如果只有一个对象知道(即可以调用)另一个对象的公共属性和操作,那么就是单向关联。大多数关联都是单向关联,单向关联关系更容易建立和维护,有助于寻找可重用的类。
在UML图中,双向关联关系用带双箭头的实线或者无箭头的实线双线表示。单向关联用一个带箭头的实线表示,箭头指向被关联的对象,自关联用指向自己的带箭头实线表。 这就是导航性(Navigatity)。
一个对象可以持有其它对象的数组或者集合。在UML中,通过放置多重性(multipicity)表达式在关联线的末端来表示。多重性表达式可以是一个数字、一段范围或者是它们的组合。多重性允许的表达式示例如下:
聚合(Aggregation)是关联关系的一种特例,它体现的是整体与部分的拥有关系,即 “has a” 的关系。 聚合对象也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体而独立存在,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享,所以聚合关系也常称为共享关系。 例如,公司部门与员工的关系,一个员工可以属于多个部门,一个部门撤消了,员工可以转到其它部门。
聚合关系用空心菱形加实线箭头表示,空心菱形在整体一方,箭头指向部分一方。
组合(Composition)也是关联关系的一种特例,它同样体现整体与部分间的包含关系,即 “contains a” 的关系。 但此时整体与部分是不可分的,部分也不能给其它整体共享,作为整体的对象负责部分的对象的生命周期。这种关系比聚合更强,也称为强聚合。 如果A组合B,则A需要知道B的生存周期,即可能A负责生成或者释放B,或者A通过某种途径知道B的生成和释放。
例如,人包含头、躯干、四肢,它们的生命周期一致。当人出生时,头、躯干、四肢同时诞生。当人死亡时,作为人体组成部分的头、躯干、四肢同时死亡。
组合关系用实心菱形加实线箭头表示,实心菱形在整体一方,箭头指向部分一方。
在Java代码形式上,聚合和组合关系中的部分对象是整体对象的一个成员变量。但是,在实际应用开发时,两个对象之间的关系到底是聚合还是组合,有时候很难区别。在Java中,仅从类代码本身是区分不了聚合和组合的。如果一定要区分,那么如果在删除整体对象的时候,必须删掉部分对象,那么就是组合关系,否则可能就是聚合关系。从业务角度上来看,如果作为整体的对象必须要部分对象的参与,才能完成自己的职责,那么二者之间就是组合关系,否则就是聚合关系。
依赖(Dependency)关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。如果对象A用到对象B,但是和B的关系不是太明显的时候,就可以把这种关系看作是依赖关系。如果对象A依赖于对象B,则 A “use a” B。
例如驾驶员和汽车的关系,驾驶员使用汽车,二者之间就是依赖关系。
依赖关系用一个带虚线的箭头表示,由使用方指向被使用方,表示使用方对象持有被使用方对象的引用。
依赖关系在Java中的具体代码表现形式为 B为A的构造器 或 方法中的局部变量、方法 或 构造器的参数、方法的返回值 ,或者 A调用B的静态方法。
泛化关系(Generalization)是指对象与对象之间的继承关系。如果对象A和对象B之间的“is a”关系成立,那么二者之间就存在继承关系,对象B是父对象,对象A是子对象。 例如,一个年薪制员工“is a”员工,很显然年薪制员工Salary对象和员工Employee对象之间存在继承关系,Employee对象是父对象,Salary对象是子对象。
泛化关系用空心三角和实线组成的箭头表示,从子类指向父类。 在Java代码中,对象之间的泛化关系可以直接翻译为关键字 extends
。
实现关系是指接口及其实现类之间的关系。
实现关系用空心三角和虚线组成的箭头来表示,从实现类指向接口。 在Java代码中,实现关系可以直接翻译为关键字 implements
。
在软件开发中,为了提高软件系统的可维护性和复用性,增加软件的可拓展性和灵活性,程序员要尽量根据7条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
单一职责原则(Single Responsibility Principle,SRP)是指:所有的对象都应该有单一的职责,它提供的所有的服务也都仅围绕着这个职责。换句话说就是:一个类而言,应该仅有一个引起它变化的原因,永远不要让一个类存在多个改变的理由。
要理解单一职责原则,首先我们要理解什么是类的职责。类的职责是由该类的对象在系统中的角色所决定的。
举例来讲,教学管理系统中,老师就代表着一种角色,这个角色决定老师的职责就是教学。而要完成教学的职责,老师需要讲课、批改作业,而讲课、批改作业的行为就相当于我们在程序中类的方法,类的方法和属性就是为了完成这个职责而设置的。
类的单一职责是说一个类应该只做一件事情。如果类中某个方法或属性与它所要完成的职责无关,或是为了完成另外的职责,那么这样的设计就不符合类的单一职责原则。而这样的设计的缺点是降低了类的内聚性,增强了类的耦合性。由此带来的问题是当我们使用这个类时,会把原本不需要的功能也带到了代码中,从而造成冗余代码或代码的浪费。单一职责原则并不是极端地要求我们只能为对象定义一个职责,而是利用极端的表述方式重点强调:在定义对象职责时,必须考虑职责与对象之间的所属关系。
对拓展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有代码,实现一个热插拔的效果。简言之,是为了使程序的拓展性好,易于维护和升级。
要达到这样的效果,我们需要使用接口和抽象类。 因为抽象灵活性好,适用性广,只要抽象的合理,基本可以保持软件架构的稳定。而软件中易变的细节可以从抽象派生的实现类进行拓展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来进行拓展就可以了。
里氏替换原则译自Liskov substitution principle。Liskov是一位计算机科学家,也就是Barbara Liskov,麻省理工学院教授,也是美国第一个计算机科学女博士,师从图灵奖得主John McCarthy教授,人工智能概念的提出者。
里氏代换原则是面向对象设计的基本原则之一,一种关于其内容的描述是Robert Martin在《敏捷软件开发:原则、模式与实践》一书中对原论文的解读:子类型(subtype)必须能够替换掉他们的基类型(base type)。这个是更简明的一种表述。 换言之,子类可以拓展父类的功能,但不能改变父类原有的功能,也就是说 把父类替换成它的子类,程序的行为没有变化。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
下面是里氏代换原则的经典例子:正方形不是长方形
/*代码清单1 Rectangle.java
* 矩形类的实现代码,用于演示LSP
*/
public class Rectangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
/*代码清单2 Square.java
* 正方形的实现代码,用于演示LSP
*/
public class Square extends Rectangle {
public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
}
下面是测试用例
public class TestRect {
public static void main(String[] args) {
TestRect tr = new TestRect();
Rectangle r = new Rectangle();
tr.g(r);
// 如果替换成下面的代码,则报错
// Rectangle s = new Square();
// tr.g(s);
}
public void g(Rectangle r) {
r.setWidth(5);
r.setLength(4);
if (r.getWidth()*r.getLength()!=20) {
throw new RuntimeException();
}
}
}
由此,我们得出结论:父类Rectangle不能被子类Square替换,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏替换原则,它们之间的继承关系不成立,正方形不是长方形。
“正方形不是长方形”,正方形是长方形也不是长方形,这样结论似乎就是个悖论。 产生这种混乱的原因有两个:
正方形在设置长度和宽度这两个行为上,与长方形显然是不同的。所以,如果我们把这种行为加到父类长方形的时候,就导致了正方形无法继承这种行为。 我们“强行”把正方形从长方形继承过来,就造成无法达到预期的结果。
这里我们以另一个理解里氏替换原则的经典例子“鸵鸟非鸟”来做示例。生物学中对于鸟类的定义是“恒温动物,卵生,全身披有羽毛,身体呈流线形,有角质的喙,眼在头的两侧。前肢退化成翼,后肢有鳞状外皮,有四趾”。从生物学角度来看,鸵鸟肯定是一种鸟,是一种继承关系。但是根据上一个“正方形非长方形”的例子,鸵鸟和鸟之间的继承关系又可能不成立。那么,鸵鸟和鸟之间到底是不是继承关系如何判断呢?这需要根据用户需求来判断。
A需求期望鸟类提供与飞翔有关的行为,即使鸵鸟跟普通的鸟在外观上就是100%的相像,但在A需求范围内,鸵鸟在飞翔这一点上跟其它普通的鸟是不一致的,它没有这个能力,所以,鸵鸟类无法从鸟类派生,鸵鸟不是鸟。
B需求期望鸟类提供与羽毛有关的行为,那么鸵鸟在这一点上跟其它普通的鸟一致的。虽然它不会飞,但是这一点不在B需求范围内,所以,它具备了鸟类全部的行为特征,鸵鸟类就能够从鸟类派生,鸵鸟就是鸟。
所有子类的行为功能必须和使用者对其父类的期望保持一致,如果子类达不到这一点,那么必然违反里氏替换原则。在实际的开发过程中,不正确地滥用继承关系是非常有害的。伴随着软件开发规模的扩大,参与的开发人员也越来越多,每个人都在使用别人提供的组件,也会为别人提供组件。最终,所有人的开发的组件经过层层包装和不断组合,被集成为一个完整的系统。每个开发人员在使用别人的组件时,只需知道组件的对外裸露的接口,那就是它全部行为的集合,至于内部到底是怎么实现的,无法知道,也无须知道。所以,对于使用者而言,它只能通过接口实现自己的预期,如果组件接口提供的行为与使用者的预期不符,错误便产生了。里氏替换原则就是在设计时避免出现子类与父类不一致的行为。
对于“正方形非长方形”问题,既然二者之间的继承关系违反了里氏替换原则,我们就应该重新设计二者之间的关系。我们可以采用第一种方案,正方形和长方形的共同行为(getLength()、getWidth()方法)抽象并封装转移到一个抽象类或者接口中,比如一个“四方形”接口或者抽象类,然后让正方形和长方形分别实现四方形接口或者继承四方形抽象类,如下图所示。
一般来说,只要有可能,就不要从具体类继承。下图就给出了一个继承形成的等级结构的典型例子。从图可以看出,所有的继承都是从抽象类开始,而所有的具体类都没有子类。也就是说,在一个由继承关系形成的等级结构中,树叶节点都应当是具体类,树枝节点都应该是抽象类或者接口。
里氏替换原则实现了开闭原则中的对扩展开放。实现开闭原则的关键步骤是抽象化,父类与子类之间的继承关系就是一种抽象化的体现。 因此,里氏替换原则是实现抽象化的一种规范。违反里氏替换原则意味着违反了开闭原则,反之未必。里氏替换原则是使代码符合开闭原则的一个重要保证。
依赖倒转原则带来的一个启示是:针对接口编程,而不是针对实现编程。也就是说,当客户要使用一个接口的实现类功能时,应该针对定义这些功能的接口编程,而不是针对该接口的实现类编程。 依赖倒转原则用于指导我们如何正确地消除模块间的依赖关系。
所谓依赖是指如果一个模块A使用另一个模块B,我们称模块A依赖模块B。在应用程序中,有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外,有一些高层次的类,这些类封装了某些复杂的逻辑,这些类我们称之为高层模块。高层次模块要完成自己封装的功能,就必须要使用低层模块,于是高层模块就依赖于低层模块。
那么,如何让低层模块依赖于高层模块呢?我们知道,高层模块肯定要使用低层模块提供的服务,不可能不让二者之间完全不存在依赖关系。
如果高层模块直接调用低层模块提供的服务,那么就是具体耦合关系,这样高层模块依赖于低层模块就不可避免。但是,如果我们使用抽象耦合关系,在高层模块和低层模块之间定义一个抽象接口,高层模块调用抽象接口定义的方法,低层模块实现该接口。这样,就消除了高层模块和低层模块之间的直接依赖关系。现在,高层模块就不依赖于低层模块了,二者都依赖于抽象。同时也实现了“抽象不应该依赖于细节,细节应该依赖于抽象”。
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。 换句话说,就是不能强迫用户去依赖那些他们不使用的接口。
接口隔离原则实际上包含了两层意思:
迪米特法则(Law of Demeter,简称LOD),又称为“最少知识原则”,它的定义为:一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易。迪米特法则是对软件实体之间通信的限制,它对软件实体之间通信的宽度和深度做出了要求。
迪米特的其它表述方式为:
那么,如何界定朋友圈和陌生人呢?迪米特法则指出,做为“朋友”的条件为:
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。
迪米特法则指出:就任何对象而言,在该对象的方法内,我们只应该调用属于上述“朋友圈”对象的方法。也就是说:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。不要同陌生人说话,也就是不要调用陌生人的方法。
迪米特法则是一种面向对象系统设计风格的一种法则,尤其适合做大型复杂系统设计指导原则。但是也会造成系统的不同模块之间的通信效率降低,使系统的不同模块之间不容易协调等缺点。同时,因为迪米特法则要求类与类之间尽量不直接通信,如果类之间需要通信就通过第三方转发的方式,这就直接导致了系统中存在大量的中介类,这些类存在的唯一原因是为了传递类与类之间的相互调用关系,这就毫无疑问的增加了系统的复杂度。解决这个问题的方式是:使用依赖倒转原则,这要就可以是调用方和被调用方之间有了一个抽象层,被调用方在遵循抽象层的前提下就可以自由的变化,此时抽象层成了调用方的朋友。
组合/聚合复用原则(Composite/Aggregation Reuse Principle,CARP)是指要尽量使用组合/聚合而非继承来达到复用目的。另一种解释是在一个新的对象中使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象委托功能达到复用这些对象的目的。
继承复用虽然后简单和易实现的特点,但也存在以下缺点:
正是因为继承有上述缺点,所以应首先使用组合/聚合,其次才考虑继承,达到复用的目的。并且在使用继承时,要严格遵循里氏替换原则。有效地使用继承会有助于对问题的理解,降低复杂度,而滥用继承会增加系统构建、维护时的难度及系统的复杂度。
采用组合或聚合复用时,可已经已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
当然,这种复用也有缺点。其中最主要的缺点就是系统中会有较多的对象需要管理。
要正确的选择组合/聚合和继承,必须透彻的理解里氏代换原则和Coad法则。里氏代换原则前面学习过,Coad法则由Peter Coad提出,总结了一些什么时候使用继承作为复用工具的条件。只有当以下的Coad条件全部被满足时,才应当使用继承关系:
错误的使用继承而不是组合/聚合的一个常见原因是错误的把“has-a”当成了“is-a”。“is-a”代表一个类是另外一个类的一种;“has-a”代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。
我们看一个例子。如果我们把“人”当成一个类,然后把“雇员”、“经理”、“学生”当成是“人”的子类,如下图所示。这种设计的错误在于把“角色”的等级结构和“人”的等级结构混淆了。“经理”、“雇员”、“学生”是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是雇员的话,就不可能是经理,也不可能是学生,这显然不合理。
正确的设计是有个抽象类“角色”,“人”可以拥有多个“角色”(聚合),“雇员”、“经理”、“学生”是“角色”的子类,如下图所示
此外,只有两个类满足里氏替换原则的时候,才可能是“is-a”关系。也就是说,如果两个类是“has-a”关系,但是设计成了继承,那么肯定违反里氏代换原则。
用于描述“怎样创建对象”,主要特点是“将对象的创建与使用分离”。如:单例、原型、工厂方法、抽象工厂、建造者等
单例模式是Java中最简单的设计模式之一。这种类型的设计模式属于创建者模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式分为两种:
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getSingleton() {
return instance;
}
}
public class Singleton {
private static Singleton instance;
static {
this.instance = new Singleton();
}
private Singleton() {}
public static Singleton getSingleton() {
return instance;
}
}
public enum Singleton {
INSTANCE;
}
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getSingleton() {
if (instance == null) {
this.instance = new Singleton();
}
return instance;
}
}
public class Singleton4 {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getSingleton() {
if (instance == null) {
this.instance = new Singleton();
}
return instance;
}
}
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
this.instance = new Singleton();
}
}
}
return instance;
}
}
static
修饰,保证了指令顺序和实例化次数。public class Singleton {
private Singleton() {}
private static class SingletonHolder() {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton() {
return SingletonHolder.INSTANCE;
}
}
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了线程安全,并且没有任何性能影响和空间的浪费。
使用反射或序列化可以破坏除枚举外的单例模式
测试单例类:
public class Singleton implement Serializable {
private Singleton() {}
private static class SingletonHolder() {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton() {
return SingletonHolder.INSTANCE;
}
}
public class test {
public static void main(String...args) {
serializeObject();
deserializeObject();
System.out.println(deserializeObject().equals(deserializeObject()));
// 输出false
}
public static Singleton deserializeObject() throw Exception {
var ois = new ObjectInputStream(new FileInputStream("./test.text"));
Singleton instance = (Singleton) ois.readObject();
ois.close();
return instance;
}
public static void serializeObject() throw Exception {
Singleton instance = Singleton.getSingleton();
var oos = new ObjectOutputStream(new FileOutputStream("./test.txt"));
oos.writeObject(instance);
oos.close;
}
}
public class test {
public static void main(String...args) {
Class clazz = Singleton.class;
var constructs = clazz.getDeclaredConstructor();
constructs.setAccessible(true);
Singleton s1 = (Singleton) constructs.newInstance();
System.out.println(s1.equals(Singleton.getSingleton());
// 输出 false
}
}
反射无解 序列化反序列化解决:
public class Singleton implement Serializable {
private Singleton() {}
private static class SingletonHolder() {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getSingleton() {
return SingletonHolder.INSTANCE;
}
// 增加如下代码
public Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
工厂模式最大的优点是与具体对象解耦
简单工厂不是一种设计模式,反而比较像是编程习惯。
/** 咖啡工厂 */
public class CoffeeFactory {
// 这里是静态工厂
public static Coffee createCoffee(String type) {
Coffee coffee;
if ("american".equals(type)) {
coffee = new AmericanCoffee();
} else if ("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使用一个产品类的实例化延迟到其工厂的子类。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品
纵向是产品族,横向是产品等级
优: 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象
劣: 当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改
Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类对象。
> 补充:
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象
clone()
方法clone()
方法,它是可被复制的对象clone()
方法来复制新的对象原型模式的克隆分为浅拷贝和深拷贝
Java的Object类提供的 clone()
方法实现的是浅拷贝。Cloneable
接口是抽象原型类,实现了 Cloneable
接口的是具体原型类
通过序列化反序列化
将一个复杂对象的结构与表示分离,使得同样的构造过程可以创建不同的表示
package com.example.factory.builder.traditional;
import lombok.Data;
/**
* @author 墨
*/
@Data
public class Computer {
private String cpu;
private String motherboard;
private String monitor;
private String keyboard;
private String mouse;
}
Computer即Product,这个电脑产品就是一个我们最终所需要的,它由CPU、主板、显示器、键盘、鼠标组成。
package com.example.factory.builder.traditional;
/**
* @author 墨
*/
public interface ComputerBuilder {
Computer computer = new Computer();
default Computer getComputer() {
return computer;
}
void buildCpu(String cpu);
void buildMotherboard(String motherboard);
void buildMonitor(String monitor);
void buildKeyboard(String keyboard);
void buildMouse(String mouse);
}
ComputerBuilder 是一个Builder类,我们在里面定义了建造所需要的方法,包含了构建CPU、主板、显示器、键盘、鼠标的方法。
下面我们来创建一个ConcreteBuilder,SpecificComputerBuilder实现了ComputerBuilder,并且实现了抽象建造的一些细节。
package com.example.factory.builder.traditional;
/**
* @author 墨
*/
public class SpecificComputerBuilder implements ComputerBuilder {
@Override
public void buildCpu(String cpu) {
computer.setCpu(cpu);
}
@Override
public void buildMotherboard(String motherboard) {
computer.setMotherboard(motherboard);
}
@Override
public void buildMonitor(String monitor) {
computer.setMonitor(monitor);
}
@Override
public void buildKeyboard(String keyboard) {
computer.setKeyboard(keyboard);
}
@Override
public void buildMouse(String mouse) {
computer.setMouse(mouse);
}
}
package com.example.factory.builder.traditional;
/**
* @author 墨
*/
public class TestBuilder {
public static void main(String[] args) {
SpecificComputerBuilder computerBuilder = new SpecificComputerBuilder();
computerBuilder.buildCpu("i5 cpu");
computerBuilder.buildMotherboard("蓝天主板");
computerBuilder.buildMonitor("小米显示器");
computerBuilder.buildMouse("双飞燕鼠标");
computerBuilder.buildKeyboard("双飞燕键盘");
Computer computer = computerBuilder.getComputer();
System.out.println(computer);
}
}
前文我们谈论到建造时,创建一个具体建造者每次都使用SpecificComputerBuilder 的方法去构建一个产品的属性,这不是很符合我们编码的习惯,也不符合建造者一步步建造的思想,特别是当一个产品的属性多起来之后我们要写很多行这样类似的代码,不优雅,也不美观,而链式建造刚好能解决这样的问题。
修改 接口(抽象建造者)的方法,令其组装过程中返回自身,同时修改具体的建造类。
package com.example.factory.builder.traditional;
/**
* @author 墨
*/
public interface ComputerBuilder {
Computer computer = new Computer();
default Computer setComputer() {
return computer;
}
ComputerBuilder buildCpu(String cpu);
ComputerBuilder buildMotherboard(String motherboard);
ComputerBuilder buildMonitor(String monitor);
ComputerBuilder buildKeyboard(String keyboard);
ComputerBuilder buildMouse(String mouse);
}
package com.example.factory.builder.traditional;
/**
* @author 墨
*/
public class SpecificComputerBuilder implements ComputerBuilder {
@Override
public ComputerBuilder buildCpu(String cpu) {
computer.setCpu(cpu);
return this;
}
@Override
public ComputerBuilder buildMotherboard(String motherboard) {
computer.setMotherboard(motherboard);
return this;
}
@Override
public ComputerBuilder buildMonitor(String monitor) {
computer.setMonitor(monitor);
return this;
}
@Override
public ComputerBuilder buildKeyboard(String keyboard) {
computer.setKeyboard(keyboard);
return this;
}
@Override
public ComputerBuilder buildMouse(String mouse) {
computer.setMouse(mouse);
return this;
}
}
测试类
package com.example.factory.builder.traditional;
/**
* @author 墨
*/
public class TestBuilder {
public static void main(String[] args) {
SpecificComputerBuilder computerBuilder = new SpecificComputerBuilder();
computerBuilder.buildCpu("i5 cpu");
computerBuilder.buildMotherboard("蓝天主板");
computerBuilder.buildMonitor("小米显示器");
computerBuilder.buildMouse("双飞燕鼠标");
computerBuilder.buildKeyboard("双飞燕键盘");
Computer computer = computerBuilder.setComputer();
System.out.println(computer);
Computer chain = new SpecificComputerBuilder().buildCpu("i3").buildMotherboard("白云主板").buildMonitor("大米显示器")
.buildMouse("单飞燕鼠标").buildKeyboard("单飞燕键盘").setComputer();
System.out.println(chain);
}
}
OkHttp3 的 OkHttpClient 创建
public static Result<List<CourseDto>> sendCourseReq(CourseSearchDto courseSearchDto) {
try {
OkHttpClient httpClient = new OkHttpClient();
Request request = new Request.Builder()
.url(ConfigFile.CONFIG.getUrl() + "/api/sel")
.addHeader("Content-Type", "application/json")
.post(RequestBody.create("aaa".getBytes()))
.build();
} catch (Exception e) {
e.printStackTrace();
}
}
package com.example.factory.builder.chain;
import lombok.ToString;
/**
* @author 墨
*/
@ToString
public class ComputerBuilder {
private String cpu;
private String motherboard;
private String monitor;
private String keyboard;
private String mouse;
public ComputerBuilder(String motherboard, String monitor, String keyboard, String mouse,String cpu) {
this.motherboard = motherboard;
this.monitor = monitor;
this.keyboard = keyboard;
this.mouse = mouse;
this.cpu = cpu;
}
public static Builder newBuilder() {
return new Builder();
}
public static class Builder {
private String cpu;
private String motherboard;
private String monitor;
private String keyboard;
private String mouse;
public Builder() {
}
public Builder setCpu(String cpu) {
this.cpu = cpu;
return this;
}
public Builder setMotherboard(String motherboard) {
this.motherboard = motherboard;
return this;
}
public Builder setMonitor(String monitor) {
this.monitor = monitor;
return this;
}
public Builder setKeyboard(String keyboard) {
this.keyboard = keyboard;
return this;
}
public Builder setMouse(String mouse) {
this.mouse = mouse;
return this;
}
public ComputerBuilder builder() {
return new ComputerBuilder(motherboard,monitor,keyboard,mouse,cpu);
}
}
}
public Builder() {
cpu = "默认cpu";
motherboard = "默认主板";
monitor = "默认显示器";
keyboard = "默认键盘";
mouse = "默认内存";
}
public ComputerBuilder builder() {
if ("i5".equals(cpu)) {
cpu = "i3";
}
return new ComputerBuilder(motherboard,monitor,keyboard,mouse,cpu);
}
优点
缺点 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其适用范围受到一定的限制。
建造者模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以通常在以下场合使用:
用于描述如何将类或对象按某种布局组成更大的结构。如代理、适配器、桥接、装饰、外观、享元、组合等
由于某些原因需要给某对象提供一个代理以控制该对象的访问。这时,访问对象不适合或不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同分为静态代理和动态代理。静态代理代理类在编译时就生成,而动态代理代理类是在Java运行时动态生成。动态代理又有JDK代理和CGLib两种。
以租房为例,我们一般用租房软件、找中介或者找房东。这里的中介就是代理者。
首先定义一个提供了租房方法的接口
public interface IRentHouse {
void rentHouse();
}
定义租房的实现类
public class RentHouse implements IRentHouse {
@Override
public void rentHouse() {
System.out.println("租了一间房子。。。");
}
}
我要租房,房源都在中介手中,所以找中介
public class IntermediaryProxy implements IRentHouse {
private IRentHouse rentHouse;
public IntermediaryProxy(IRentHouse irentHouse){
rentHouse = irentHouse;
}
@Override
public void rentHouse() {
System.out.println("交中介费");
rentHouse.rentHouse();
System.out.println("中介负责维修管理");
}
}
测试类
public class Main {
public static void main(String[] args){
//定义租房
IRentHouse rentHouse = new RentHouse();
//定义中介
IRentHouse intermediary = new IntermediaryProxy(rentHouse);
//找中介租房
intermediary.rentHouse();
}
}
这就是静态代理,因为中介这个代理类已经事先写好了,只负责代理租房业务
Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理方法 (newProxyInstance方法 来获取代理对象
public interface Skill {
void run();
void swim();
}
public class Althletes implements Skill{
@Override
public void run() {
System.out.println("Run fast");
}
@Override
public void swim() {
System.out.println("Swim fast");
}
}
只是代理工厂,代理类在内存中动态生成
public class AlthleteProxy {
public Skill AlthleteProxy(Althletes obj) {
return (Skill) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces()
, ((proxy, method, args) -> {
System.out.println("Start");
Object result = method.invoke(proxy, args);
System.out.println("Finish");
return result;
};));
}
}
public class Main {
public static void main(String[] args) {
Skill s = new AlthleteProxy(new Althletes);
s.run();
s.swim();
}
}
CGLib动态代理的实现机制是生成目标类的子类,通过调用父类(目标类)的方法实现,在调用父类方法时再代理中进行增强。
基本类
package top.ytao.demo.proxy;
/**
* Created by YangTao
*/
public class Cat {
public void call() {
System.out.println("喵喵喵 ~");
}
}
相比于 JDK 动态代理的实现,CGLIB 动态代理不需要实现与目标类一样的接口,而是通过方法拦截的方式实现代理,代码实现如下,首先方法拦截接口 net.sf.cglib.proxy.MethodInterceptor
/**
* Created by YangTao
*/
public class TargetInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("CGLIB 调用前");
Object result = proxy.invokeSuper(obj, args);
System.out.println("CGLIB 调用后");
return result;
}
}
通过方法拦截接口调用目标类的方法,然后在该被拦截的方法进行增强处理,实现方法拦截器接口的 intercept 方法里面有四个参数:
创建 CGLIB 动态代理类使用 net.sf.cglib.proxy.Enhancer 类进行创建,它是 CGLIB 动态代理中的核心类,首先创建个简单的代理类:
/**
* Created by YangTao
*/
public class CglibProxy {
public static Object getProxy(Class<?> clazz) {
Enhancer enhancer = new Enhancer();
// 设置类加载
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new TargetInterceptor());
// 创建代理类
return enhancer.create();
}
}
测试类
public class Main {
@Test
public void dynamicProxy() throws Exception {
Animal cat = (Animal) CglibProxy.getProxy(Cat.class);
cat.call();
}
}
Enhancer 在使用过程中,常用且有特色功能还有回调过滤器 CallbackFilter 的使用,它在拦截目标对象的方法时,可以有选择性的执行方法拦截,也就是选择被代理方法的增强处理。使用该功能需要实现 net.sf.cglib.proxy.CallbackFilter 接口。不做展开,需要了解可以看参考12。
优
劣
将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类能一起工作。
适配器模式(Adapter)分为类适配器模式和对象适配器模式,前者类之间的耦合度比较高,且要求程序员了解现有组件库中的相关组件内部接口,所以应用相对较少。
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已存在的组件
/**
* 两孔插座
*/
public class TwoHoleSocket {
public void twoHole() {
System.out.println("插入两孔插座");
}
}
/**
* 三孔插座
*/
public interface ThreeHoleSocket {
void threeHole();
}
/**
* 适配器:可以同时使用两孔和三孔插座
*/
public class Adapter extends TwoHoleSocket implements ThreeHoleSocket{
@Override
public void threeHole() {
System.out.println("插入三孔插座");
}
}
public static void main(String[] args) {
Adapter adapter = new Adapter();
adapter.twoHole();//使用两孔插座
adapter.threeHole();//使用三孔插座
}
类适配器模式违背了合成复用原则,仅在目标接口规范的情况下可用
通过组合关系来实现适配器功能
/**
* 适配器
*/
public class Adapter implements ThreeHoleSocket{
//通过组合持有两孔插座的对象,内部引用两孔插座来适配
private TwoHoleSocket twoHoleSocket;
public Adapter(TwoHoleSocket twoHoleSocket) {
this.twoHoleSocket = twoHoleSocket;
}
public void twoHole() {
twoHoleSocket.twoHole();
}
@Override
public void threeHole() {
System.out.println("插入三孔插座");
}
}
测试方法
public static void main(String[] args) {
Adapter adapter = new Adapter(new TwoHoleSocket());
adapter.twoHole();//两孔插座
adapter.threeHole();//三孔插座
}
接口适配器模式也称作缺省适配模式,就是有时候一个接口的方法太多,我只想用其中的一两个,不想为其他方法提供实现,就可以通过一个抽象类为这个接口的所有方法,提供空实现,如果想用哪个方法,再提供一个子类继承这个抽象类,覆盖父类某个方法即可。
Reader(字符流)、InputStream(字节流)的适配使用的就是 InputStreamReader 和 OutputStreamWriter
InputStreamReader 和 OutputStreamWriter 分别继承自 java.io包下的 Reader 和 Writer,对它们中的抽象的未实现的方法给出实现。
InputStreamReader 做了 InputStream字节流 到 Reader字符流 之间的转换。 从类图来看,StreamDecoder 的设计实现采用了(对象)适配器模式。
在不改变现有对象结构的情况下,动态地给该对象增加一些职责(额外功能)
优
劣
I/O流中的包装类使用了装饰者模式。BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter
BufferedInputStream 使用装饰者模式对 InputStream 的子实现类 FilterInputStream 进行增强,添加了缓冲区,提高了写效率
根据上图可以看出:
将抽象和实现分离,是它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
外观模式,又称门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心系统内部的具体实现,这样会大大降低程序的复杂度,提高程序的可维护性。
外观模式是 “迪米特法则” 的典型应用
优
劣
使用 Tomcat 作为Web容器时,接收浏览器发送过来的请求,Tomcat 会将请求信息封装成 ServletRequest
对象。 但 ServletRequest
是一个接口,它还有一个子接口 HttpServletRequest
,而我们知道该 reqest 对象肯定是 HttpServletRequest
对象的子实现类对象,即 RequestFacade
对象
组合模式,又称部分整体模式,是用于把一组相似的对象当做一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,这样做的好处是确保所有的构建类都有相同的接口。透明组合模式也是组合模式的标准形式。
在安全组合模式中,抽象构建角色中没有声明任何用于管理成员对象的方法,而是在树枝节点中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构建和容器构建具有不同的方法,且容器构建中那些用于管理成员对象的方法没有在抽象构建类中定义,因此客户端不能完全针对抽象编程,必须有区别的对待叶子构件和容器构件。
优
组合模式正是对应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。如:文件目录显示,多级目录呈现等。
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。
享元模式(Fluweight)存在以下两种状态:
主要角色:
优
缺
int 的包装类 Integer类 使用了享元模式
public static void main(String...args) {
Integer i1 = 127;
Integer i2 = 127;
Integer i3 = 128;
Integer i4 = 128;
// true
System.out.println(i1 == i2);
// false
System.out.println(i3 == i4);
}
直接给 Integer 类型的变量赋值基本数据类型的操作,底层调用了 Integer.valueOf()
方法
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到 Integer.valueOf()
先进行了一个判断,判断入参 i 是否在 IntegerCache
的 low
和 high
之间
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
for(int i = 0; i < c.length; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
可以看到,如果 Integer.valueOf()
的入参在 -128 ~ 127
之间,会直接返回 IntegerCache.archivedCache
这个数组中已经存在的对象,而不会新建一个对象。
Java 虚拟机会在启动时先初始化一些系统类,例如 java.lang.Integer
类,而 IntegerCache.archivedCache
数组就是在这个时候被初始化的。
用于描述类或对象之间怎样相互协作共同完成单个对象无法完成的任务,以及怎样分配职责。如:模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等
行为型模式分为 类行为型模式 和 对象行为型模式,前者采用继承机制在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足 合成复用原则,所以对象行为模式比 类行为型模式 具有更大的灵活性。
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使子类可以在不改变该算法结构的情况下重定义改算法的某些步骤。
优
劣
InputStream类 使用了模板方法模式,在 InputStream类 定义了多个 read()
方法,具体实现由子类实现。
将每个算法封装起来,使它们可以互相替换,且算法的变化不会影响使用算法的客户。 策略模式属于对象行为模式,它通过对算法的封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
优
劣
Comparator 类中的策略模式。 在 Arrays 类中的 sort()
方法
public static <T> void sort(T[] a, Comparator<? super T> c) {
...
}
参数列表里可以接一个 Comparator 对象,即新的策略
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,方便将命令对象进行存储、传递、调用与管理
优
劣
Runnable 是典型的命令模式,Runnable是命令对象,Thread充当调用者, start方法就是其执行方法
责任链模式,又称职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成的一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止
优
劣
Servlet 的 FilterChain过滤器 是责任链模式的典型应用
对有状态的对象,把复杂的逻辑判断提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为
优
劣
观察者模式,又称为 发布-订阅模式(Publish/Subscribe),它定义了一种一对多的依赖关系,让多个观察者对象同时监听某个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使它们能够自动更新自己。
优
劣
通过 java.util.Observable
类 和 java.util.Observer
接口 定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例
Observable类 是抽象目标类(被观察者),它有一个 Vector 集合成员变量,用于保存所有要通知的观察者对象
Observer接口 是抽象观察者,它检视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应的工作
又名 调停模式,顶定义一个中介角色来封装一系列对象的交互,使原有对象之间的耦合松散,且可以独立改变它们之间的交互。
抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
如 Mirai QQ 的 Message 设计
优
中介者模式通过把多个同事对象之间的交互封装到中介者对象内,使得同事对象之间的耦合度降低,基本可以做到依赖互补。这样一来,同事对象就可以独立的变化和复用。
多个同事对象的交互,被封装在中介者对象内集中管理,使得这些交互行为变化时,只要修改中介者对象即可。
劣
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示
优
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新操作
优
劣
访问者模式存在一个叫"伪动态双分派”的技术,访问者模式之所以是最复杂的设计模式与其有很大的关系。
什么叫分派?根据对象的类型而对方法进行的选择,就是分派(Dispatch)。
单分派与多分派
先看在 BigHuYouCompany 类里的分派代码:slave.accept(visitor);
中 accept方法的分派是由 slave的运行时类型 决定的。若slave是Programer就执行Programer的accept方法。若slave是Tester那么就执行Tester的accept方法。(具体可见参考16)
简单来说就是根据入参对象的实际类型执行不同的逻辑
又名 快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,以便以后当需要时能够将该对象恢复到保存的状态
备忘录有两个等效接口:
优
发起人
类。发起人不需要管理和保存其内部状态的各个部分,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则劣
给定一个语言,定义它的语法表示,并定义一个解释器,这个解释器使用语法解释语言中的句子
如 逆波兰表达式 转换成 普通表达式,定义一个使用栈的算法
expression ::= value | plus | minus
plus ::= expression '+' expression
minus ::= expression '-' expression
value ::= integer
> 注: 这里的 ::=
表示 定义为
, |
表示 或
上面的规则描述为: 表达是可以是一个值,也可以是 plus 或 minus 运算,而 plus 和 minus 又是由表达式结合运算符构成,值的类型为 Integer
在计算机学科中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(SyntaxTree),是源代码语法的一种抽象表示。 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码的一种结构。
优
缺
<>
括号标识不同的结点含义