一,设计模式的七大原则
1, 设计模式的目的
编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性 等多方面的
挑战,设计模式是为了让程序(软件),具有更好
- 代码复用性(相同功能的代码只需要写一次)
- 可读性(代码简单易读)
- 可扩展性(当我们后期需要添加功能时,可以很容易实现,且不会代码的可用性)
- 可靠性(当我们增加功能后,不会对原有的功能影响)
- 高内聚低耦合
分享金句:
- 设计模式包含了面向对象的精髓,“懂了设计模式,你就懂了面向对象分析和设计(OOA/D)的精要”
- Scott Mayers 在其巨著《Effective C++》就曾经说过:C++老手和 C++新手的区别就是前者手背上有很多伤疤
2, 设计模式七大原则
设计模式原则,其实就是程序员在编程时,应当遵守的原则,也是各种设计模式的基础(即:设计模式为什么
这样设计的依据)
设计模式常用的七大原则有:
- 单一职责原则
- 接口隔离原则
- 依赖倒转(倒置)原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
- 合成复用原则
3, 单一职责原则
3.1, 基本介绍
对于类来说,就是一个类对应一项职责。如果一个类有多个不同二点职责,且每个职责发生错误的时候都可能会影响到别的职责,那么这个类就应该将这些职责分开。
3.2, 应用实例
这里设置一个交通工具的类来阐述
版本一
package 单一原则;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingleResponsibility1
* @Description: 最初是的代码
* @Author: ming
* @Date: 2022/4/10 17:09
*
* 问题在于飞机本应该是天上飞的,但是这里写的是地上跑的,所以因该根据不同的交通工具进行划分方法
*/
public class SingleResponsibility1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Vehicle vehicle = new Vehicle();
vehicle.run("摩托车");
vehicle.run("汽车");
vehicle.run("飞机");
}
}
// 交通工具类
// 方式1
// 1. 在方式1 的run方法中,违反了单一职责原则
// 2. 解决的方案非常的简单,根据交通工具运行方法不同,分解成不同类即可
class Vehicle {
public void run(String vehicle) {
System.out.println(vehicle + " 在公路上运行....");
}
}
问题在于飞机本应该是天上飞的,但是这里写的是地上跑的,所以因该根据不同的交通工具进行划分方法.
版本二
package 单一原则;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingleResponsibility2
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/10 17:11
*
* 版本二,虽然完成了这个方法,但是他的代价太大了,他重新创建了三个类,且还需要就该main方法
* 但是他确实完美符合单一原则
*/
public class SingleResponsibility2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
RoadVehicle roadVehicle = new RoadVehicle();
roadVehicle.run("摩托车");
roadVehicle.run("汽车");
AirVehicle airVehicle = new AirVehicle();
airVehicle.run("飞机");
}
}
//方案2的分析
//1. 遵守单一职责原则
//2. 但是这样做的改动很大,即将类分解,同时修改客户端
//3. 改进:直接修改Vehicle 类,改动的代码会比较少=>方案3
class RoadVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "公路运行");
}
}
class AirVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "天空运行");
}
}
class WaterVehicle {
public void run(String vehicle) {
System.out.println(vehicle + "水中运行");
}
}
版本二,虽然完成了这个方法,但是他的代价太大了,他重新创建了三个类,且还需要就该main方法,但是他确实完美符合单一原则
版本三
package 单一原则;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingleResponsibility3
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/10 17:12
* 这个方法的话他其实是不遵守类的单一原则的,他只完成了方法的单一原则,但是却减少了资源的浪费
* 但是这个方法的问题是,如果Vehicle2中有别的衍生方法,就会产生很大的问题,这个写法只能在方法很少的情况下用
*/
public class SingleResponsibility3 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Vehicle2 vehicle2 = new Vehicle2();
vehicle2.run("汽车");
vehicle2.runWater("轮船");
vehicle2.runAir("飞机");
}
}
//方式3的分析
//1. 这种修改方法没有对原来的类做大的修改,只是增加方法
//2. 这里虽然没有在类这个级别上遵守单一职责原则,但是在方法级别上,仍然是遵守单一职责
class Vehicle2 {
public void run(String vehicle) {
//处理
System.out.println(vehicle + " 在公路上运行....");
}
public void runAir(String vehicle) {
System.out.println(vehicle + " 在天空上运行....");
}
public void runWater(String vehicle) {
System.out.println(vehicle + " 在水中行....");
}
//方法2.
//..
//..
//...
}
这个方法的话他其实是不遵守类的单一原则的,他只完成了方法的单一原则,但是却减少了资源的浪费,但是这个方法的问题是,如果Vehicle2中有别的衍生方法,就会产生很大的问题,这个写法只能在方法很少的情况下用。
3.3, 单一职责原则注意事项和细节
- 降低复杂度,一个类只负责一项职责。
- 提高类的可读性,可维护性。
- 降低变更引起的风险。
- 通常情况下,我们因该遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则。只有类的方法数量足够少,可以在方法级保持单一职责原则。
4, 接口隔离原则
4.1, 基本介绍
-
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
-
先看一张图:
-
类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,如果接口 Interface1 对于类 A 和类C来说不是最小接口,那么类 B 和类 D 必须去实现他们不需要的方法。
-
按隔离原则应当这样处理:
- 将接口 Interface1 拆分为独立的几个接口(这里我们拆分成 3 个接口),类 A 和类 C 分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
理解
如果有几个类都要通过一个接口去以来别的类,但是我们仅需要的是接口中的几个方法,这个时候,我们可以将接口的方法拆分开,这样子就可以让其选择性继承接口。
4.2,应用实例
- 类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,请编写代码完成此应用实例。
- 具体的依赖图如上
package 接口隔离原则;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Segregation1
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/10 21:50
*/
public class Segregation1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
//接口
interface Interface1 {
void operation1();
void operation2();
void operation3();
void operation4();
void operation5();
}
class B implements Interface1 {
@Override
public void operation1() {
}
@Override
public void operation2() {
}
@Override
public void operation3() {
}
@Override
public void operation4() {
}
@Override
public void operation5() {
}
}
class D implements Interface1 {
@Override
public void operation1() {
}
@Override
public void operation2() {
}
@Override
public void operation3() {
}
@Override
public void operation4() {
}
@Override
public void operation5() {
}
}
class A { //A 类通过接口Interface1 依赖(使用) B类,但是只会用到1,2,3方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend2(Interface1 i) {
i.operation2();
}
public void depend3(Interface1 i) {
i.operation3();
}
}
class C { //C 类通过接口Interface1 依赖(使用) D类,但是只会用到1,4,5方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend4(Interface1 i) {
i.operation4();
}
public void depend5(Interface1 i) {
i.operation5();
}
}
4.3, 应传统方法的问题和使用接口隔离原则改进
-
类 A 通过接口 Interface1 依赖类 B,类 C 通过接口 Interface1 依赖类 D,如果接口 Interface1 对于类 A 和类 C来说不是最小接口,那么类 B 和类 D 必须去实现他们不需要的方法
-
将接口 Interface1 拆分为独立的几个接口,类 A 和类 C 分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则
-
接口 Interface1 中出现的方法,根据实际情况拆分为三个接口
package 接口隔离原则.IMPROVE;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Segregation1
* @Description: 实现接口分离原则后的代码
* @Author: ming
* @Date: 2022/4/10 21:57
将之前的Interface1的五个方法分为三个接口,将方法1赋予接口1,将方法2,3赋予接口2,将方法4,5赋予接口3
*/
public class Segregation1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 使用一把
A a = new A();
a.depend1(new B()); // A类通过接口去依赖B类
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D()); // C类通过接口去依赖(使用)D类
c.depend4(new D());
c.depend5(new D());
}
}
// 接口1
interface Interface1 {
void operation1();
}
// 接口2
interface Interface2 {
void operation2();
void operation3();
}
// 接口3
interface Interface3 {
void operation4();
void operation5();
}
class B implements Interface1, Interface2 {
@Override
public void operation1() {
System.out.println("B 实现了 operation1");
}
@Override
public void operation2() {
System.out.println("B 实现了 operation2");
}
@Override
public void operation3() {
System.out.println("B 实现了 operation3");
}
}
class D implements Interface1, Interface3 {
@Override
public void operation1() {
System.out.println("D 实现了 operation1");
}
@Override
public void operation4() {
System.out.println("D 实现了 operation4");
}
@Override
public void operation5() {
System.out.println("D 实现了 operation5");
}
}
class A { // A 类通过接口Interface1,Interface2 依赖(使用) B类,但是只会用到1,2,3方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend2(Interface2 i) {
i.operation2();
}
public void depend3(Interface2 i) {
i.operation3();
}
}
class C { // C 类通过接口Interface1,Interface3 依赖(使用) D类,但是只会用到1,4,5方法
public void depend1(Interface1 i) {
i.operation1();
}
public void depend4(Interface3 i) {
i.operation4();
}
public void depend5(Interface3 i) {
i.operation5();
}
}
5, 依赖倒转原则
5.1, 基本介绍
依赖倒转原则(Dependence Inversion Principle)是指 :
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象 (有一个person类喝一个student类,不因该让person类去依赖student类)
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 依赖倒转(倒置)的中心思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在 java 中,抽象指的是接口或抽象类,细节就是具体的实现类
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
5.2, 应用实例
请编程完成 Person 接收消息的功能。
package 依赖倒置原则;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: DependecyInversion
* @Description: 未使用依赖倒置原则的代码
* @Author: ming
* @Date: 2022/4/10 22:59
*
* 缺点:
* 1,如果我们要多一些别的消息源,就需要创建很多类,而且也要修改接受方的代码,最重要的是还要修改主方法。
*/
public class DependecyInversion {
public static void main(String[] args) {
Person person = new Person();
person.receive(new Email());
}
}
class Email {
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
//完成Person接收消息的功能
//方式1分析
//1. 简单,比较容易想到
//2. 如果我们获取的对象是 微信,短信等等,则新增类,同时Perons也要增加相应的接收方法
//3. 解决思路:引入一个抽象的接口IReceiver, 表示接收者, 这样Person类与接口IReceiver发生依赖
// 因为Email, WeiXin 等等属于接收的范围,他们各自实现IReceiver 接口就ok, 这样我们就符号依赖倒转原则
class Person {
public void receive(Email email ) {
System.out.println(email.getInfo());
}
}
- 实现方案 2(依赖倒转) + 分析说明
package 依赖倒置原则.improve;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: DependecyInversion
* @Description: 使用依赖倒置后的代码
* @Author: ming
* @Date: 2022/4/10 23:03
* 好处是:我们消息源都因为继承了接口从而规范了他们的行为,且我们的接受放可以只使用一个方法就接受所有的消息。
*/
public class DependecyInversion {
public static void main(String[] args) {
//客户端无需改变
Person person = new Person();
person.receive(new Email());
person.receive(new WeiXin());
}
}
//定义接口 定义了一个接受消息的接口,用于制定接受消息的行为
interface IReceiver {
public String getInfo();
}
//发送消息源需要实现上面的接口
class Email implements IReceiver {
@Override
public String getInfo() {
return "电子邮件信息: hello,world";
}
}
//增加微信
class WeiXin implements IReceiver {
@Override
public String getInfo() {
return "微信信息: hello,ok";
}
}
//方式2
class Person {
//这里我们是对接口的依赖,这里需要传入的是接口,这样子只要是这个接口的实现类就都可以使用
public void receive(IReceiver receiver ) {
System.out.println(receiver.getInfo());
}
}
5.2, 依赖关系传递的三种方式和应用案例
-
接口传递
public class DependencyPass { public static void main(String[] args) { // TODO Auto-generated method stub ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(); openAndClose.open(changHong); } } // 方式1: 通过接口传递实现依赖 // 开关的接口 interface IOpenAndClose { //这方法我们传入参数的时候将ITV接口传入,之后就在使用该方法是需要他的实现类 public void open(ITV tv); //抽象方法,接收接口 } interface ITV { //ITV接口 public void play(); } class ChangHong implements ITV { @Override public void play() { // TODO Auto-generated method stub System.out.println("长虹电视机,打开"); } } // 实现接口 class OpenAndClose implements IOpenAndClose{ public void open(ITV tv){ tv.play(); } }
-
构造方法传递
public class DependencyPass { public static void main(String[] args) { //通过构造器进行依赖传递 ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(changHong); openAndClose.open(); } } // 方式2: 通过构造方法依赖传递 interface IOpenAndClose { public void open(); //抽象方法 } interface ITV { //ITV接口 public void play(); } class OpenAndClose implements IOpenAndClose{ public ITV tv; //成员 public OpenAndClose(ITV tv){ //构造器 需要注入一个具体的ITV接口的实现类 this.tv = tv; } @Override public void open(){ this.tv.play(); } } class ChangHong implements ITV { @Override public void play() { // TODO Auto-generated method stub System.out.println("长虹电视机,打开"); } }
-
setter方式传递
public class DependencyPass { public static void main(String[] args) { //通过setter方法进行依赖传递 ChangHong changHong = new ChangHong(); OpenAndClose openAndClose = new OpenAndClose(); openAndClose.setTv(changHong); openAndClose.open(); } } //方式3 , 通过setter方法传递 interface IOpenAndClose { public void open(); // 抽象方法 public void setTv(ITV tv); } interface ITV { // ITV接口 public void play(); } class OpenAndClose implements IOpenAndClose { private ITV tv; public void setTv(ITV tv) { this.tv = tv; } public void open() { this.tv.play(); } }
5.3, 依赖倒转原则的注意事项和细节
- 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好.
- 变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展
和优化 - 继承时遵循里氏替换原则
总结
其实依赖倒转原则就是指,在可以的情况下不要让一个类孤零零的存在,最好要有接口或者抽象类来规范他。同时这样做也可以提高代码的复用性。就例如上面应用实例,最初我们没有用接口,那么我们没创建一个消息源就需要创建一个消息源,且要修改接受者的方法,还要修改main方法,但是用接口后,我们可以直接通过传入接口实现上面的操作。同时,我们还可以进行功能的扩展,只要在接口中实现即可。
6, 里氏替换原则
6.1, OO 中的继承性的思考和说明
- 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。 (这里的意思就是指,他们在非必要的情况下不要去修改父类方法,举一个例子,加入子类把所有的父类的方法都修改完了,那么还有什么必要去在继承这个父类呢?)
- 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。(如果a类继承了b类,那么b类进行修改的话就会对a产生影响)
- 问题提出:在编程中,如何正确的使用继承? => 里氏替换原则
6.2, 基本介绍
- 里氏替换原则(Liskov Substitution Principle)在 1988 年,由麻省理工学院的以为姓里的女士提出的。
- 如果对每个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法 。
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖 来解决问题。 (如果你真的需要继承,那么可以吧这两个类进行抽出一个更基本的类)。
6.3, 一个程序引出的问题和思考
package 里氏替换原则;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Liskov
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 21:32
* 在这个类中,我们发现因为我们的重写父类方法导致在后面的使用中不小心用错了方法,产生了一些错误。
* 所以解决的办法是创建一个更为基础的类让其进行继承。当需要其他类的方法使用组合的方式
*/
public class Liskov {
public static void main(String[] args) {
// TODO Auto-generated method stub
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------");
B b = new B();
System.out.println("11-3=" + b.func1(11, 3));//这里本意是求出11-3
System.out.println("1-8=" + b.func1(1, 8));// 1-8
System.out.println("11+3+9=" + b.func2(11, 3));
}
}
// A类
class A {
// 返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2;
}
}
// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends A {
//这里,重写了A类的方法, 可能是无意识
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
}
6.3, 解决方法
-
我们发现原来运行正常的相减功能发生了错误。原因就是类 B 无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的 复用性会比较差。特别是运行多态比较频繁的时候
-
通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替
-
改进方案
package 里氏替换原则.improve;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Liskov
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 21:32
*
* 在这里我们创建了一个基础的此类Base,让A类和B类进行继承,同时因为b类需要用到A类的方
* 法,所以我们可以吧A类注入,从而调用A类的方法
*/
public class Liskov {
public static void main(String[] args) {
// TODO Auto-generated method stub
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("-----------------------");
B b = new B();
//因为B类不再继承A类,因此调用者,不会再func1是求减法
//调用完成的功能就会很明确
System.out.println("11+3=" + b.func1(11, 3));//这里本意是求出11+3
System.out.println("1+8=" + b.func1(1, 8));// 1+8
System.out.println("11+3+9=" + b.func2(11, 3));
//使用组合仍然可以使用到A类相关方法
System.out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3
}
}
//创建一个更加基础的基类
class Base {
//把更加基础的方法和成员写到Base类
}
// A类
class A extends Base {
// 返回两个数的差
public int func1(int num1, int num2) {
return num1 - num2;
}
}
// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends Base {
//如果B需要使用A类的方法,使用组合关系
private A a = new A();
//这里,重写了A类的方法, 可能是无意识
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 9;
}
//我们仍然想使用A的方法
public int func3(int a, int b) {
return this.a.func1(a, b);
}
}
7, 开闭原则
7.1, 基本介绍
- 开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则
- 一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实
现扩展细节。 - 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。
7.2, 看下面一段代码
看一个画图形的功能
package 开闭原则;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Ocp
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 22:06
* 这个中的问题是,如果提供放多提供了一个类,这个时候使用方如果要使用的话,还需要将修改,破坏了开闭原则
*/
public class Ocp {
public static void main(String[] args) {
//使用看看存在的问题
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
}
}
//这是一个用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,然后根据type,来绘制不同的图形
public void drawShape(Shape s) {
if (s.m_type == 1)
drawRectangle(s);
else if (s.m_type == 2)
drawCircle(s);
else if (s.m_type == 3)
drawTriangle(s);
}
//绘制矩形
public void drawRectangle(Shape r) {
System.out.println(" 绘制矩形 ");
}
//绘制圆形
public void drawCircle(Shape r) {
System.out.println(" 绘制圆形 ");
}
//绘制三角形
public void drawTriangle(Shape r) {
System.out.println(" 绘制三角形 ");
}
}
//Shape类,基类
class Shape {
int m_type;
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
}
//新增画三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
}
7.3, 方式 1 的优缺点
- 优点是比较好理解,简单易操作。
- 缺点是违反了设计模式的 ocp 原则,即对扩展开放(提供方),对修改关闭(使用方)。即当我们给类增加新功能的时候,尽量不修改代码,或者尽可能少修改代码.
- 比如我们这时要新增加一个图形种类 三角形,我们需要做如下修改,修改的地方较多
7.4, 改进的思路分析
思路:把创建 Shape 类做成抽象类,并提供一个抽象的 draw 方法,让子类去实现即可,这样我们有新的图形
种类时,只需要让新的图形类继承 Shape,并实现 draw 方法即可,使用方的代码就不需要修 -> 满足了开闭原则
package 开闭原则.improve;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Ocp
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 22:08
* 解决办法:
* 我们将基础类中在添加一个画画的方法。在每个子类中进行定制化的实现。这样子就不用在父类中进行
*/
public class Ocp {
public static void main(String[] args) {
//使用看看存在的问题
GraphicEditor graphicEditor = new GraphicEditor();
graphicEditor.drawShape(new Rectangle());
graphicEditor.drawShape(new Circle());
graphicEditor.drawShape(new Triangle());
graphicEditor.drawShape(new OtherGraphic());
}
}
//这是一个用于绘图的类 [使用方]
class GraphicEditor {
//接收Shape对象,调用draw方法
public void drawShape(Shape s) {
s.draw();
}
}
//Shape类,基类
abstract class Shape {
int m_type;
public abstract void draw();//抽象方法
}
class Rectangle extends Shape {
Rectangle() {
super.m_type = 1;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制矩形 ");
}
}
class Circle extends Shape {
Circle() {
super.m_type = 2;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制圆形 ");
}
}
//新增画三角形
class Triangle extends Shape {
Triangle() {
super.m_type = 3;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制三角形 ");
}
}
//新增一个图形
class OtherGraphic extends Shape {
OtherGraphic() {
super.m_type = 4;
}
@Override
public void draw() {
// TODO Auto-generated method stub
System.out.println(" 绘制其它图形 ");
}
}
8, 迪米特法则
8.1, 基本介绍
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
- 迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息
- 迪米特法则还有个更简单的定义:只与直接的朋友通信
- 直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间
是朋友关系。耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称****出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。 - 总结:这个原则就是类似与我们写项目,user类的所有的方法最好都放入user类中,不要放入别的类里,如果别的类要用的话,直接吧这个类注入使用其方法。
8.2, 应用实例
- 有一个学校,下属有各个学院和总部,现要求打印出学校总部员工 ID 和学院员工的 id
- 编程实现上面的功能, 看代码演示
package 迪米特原则;
import java.util.ArrayList;
import java.util.List;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Demeter1
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 22:46
*/
//客户端
public class Demeter1 {
public static void main(String[] args) {
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学校总部员工类
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 这里的 CollegeEmployee 不是 SchoolManager的直接朋友
//2. CollegeEmployee 是以局部变量方式出现在 SchoolManager
//3. 违反了 迪米特法则
//获取到学院员工
List<CollegeEmployee> list1 = sub.getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
8.3, 应用实例改进
- 前面设计的问题在于 SchoolManager 中,CollegeEmployee 类并不是 SchoolManager 类的直接朋友 (分析)
- 按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合
- 对代码按照迪米特法则 进行改进.
- 就是将SchoolManager 的输出所有的学院员工的方法放入学院管理类中
package 迪米特原则.improve;
import java.util.ArrayList;
import java.util.List;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Demeter1
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/11 22:46
*/
//客户端
public class Demeter1 {
public static void main(String[] args) {
System.out.println("~~~使用迪米特法则的改进~~~");
//创建了一个 SchoolManager 对象
SchoolManager schoolManager = new SchoolManager();
//输出学院的员工id 和 学校总部的员工信息
schoolManager.printAllEmployee(new CollegeManager());
}
}
//学校总部员工类
class Employee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//学院的员工类
class CollegeEmployee {
private String id;
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
//管理学院员工的管理类
class CollegeManager {
//返回学院的所有员工
public List<CollegeEmployee> getAllEmployee() {
List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
CollegeEmployee emp = new CollegeEmployee();
emp.setId("学院员工id= " + i);
list.add(emp);
}
return list;
}
//输出学院员工的信息
public void printEmployee() {
//获取到学院员工
List<CollegeEmployee> list1 = getAllEmployee();
System.out.println("------------学院员工------------");
for (CollegeEmployee e : list1) {
System.out.println(e.getId());
}
}
}
//学校管理类
//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
//返回学校总部的员工
public List<Employee> getAllEmployee() {
List<Employee> list = new ArrayList<Employee>();
for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
Employee emp = new Employee();
emp.setId("学校总部员工id= " + i);
list.add(emp);
}
return list;
}
//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {
//分析问题
//1. 将输出学院的员工方法,封装到CollegeManager
sub.printEmployee();
//获取到学校总部员工
List<Employee> list2 = this.getAllEmployee();
System.out.println("------------学校总部员工------------");
for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
8.4, 迪米特法则注意事项和细节
- 迪米特法则的核心是降低类之间的耦合
- 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是
要求完全没有依赖关系
9, 合成复用原则
9.1, 基本介绍
原则是尽量使用合成/聚合的方式,而不是使用继承。
用大白话将,就是当我们发现我们的子类只是为了用父类方法而去继承,就不符合我们的合成复用原则。我们应该将这个父类注入到子类中,从而通过父类对象调用其方法。
9.2, 设计原则核心思想
- 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程。
- 为了交互对象之间的松耦合设计而努力
二, UML 类图
1, UML 基本介绍
-
UML——Unified modeling language UML (统一建模语言),是一种用于软件系统分析和设计的语言工具,它用于帮助软件开发人员进行思考和记录思路的结果
-
UML 本身是一套符号的规定,就像数学符号和化学符号一样,这些符号用于描述软件模型中的各个元素和他
们之间的关系,比如类、接口、实现、泛化、依赖、组合、聚合等,如右图: -
使用 UML 来建模,常用的工具有 Rational Rose , 也可以使用一些插件来建模
2, UML 图
画 UML 图与写文章差不多,都是把自己的思想描述给别人看,关键在于思路和条理,UML 图分类:
- 用例图(use case)
- 静态结构图:类图、对象图、包图、组件图、部署图
- 动态行为图:交互图(时序图与协作图)、状态图、活动图
说明:
- 类图是描述类与类之间的关系的,是 UML 图中最核心的
3, UML 类图
- 用于描述系统中的类(对象)本身的组成和类(对象)之间的各种静态关系。
- 类之间的关系:依赖、泛化(继承)、实现、关联、聚合与组合。
4, 类图—依赖关系
只要是在类中用到了对方,那么他们之间就存在依赖关系。如果没有对方,连编绎都通过不了。
public class PersonServiceBean {
private PersonDao personDao;//类
public void save(Person person){}
public IDCard getIDCard(Integer personid){}
public void modify(){
Department department = new Department();
}
}
public class PersonDao{}
public class IDCard{}
public class Person{}
public class Department{}
小结
- 只要类中用过别的类,那么这两个类就有依赖关系。
5, 类图—泛化关系
泛化关系实际上就是继承关系,他是依赖关系的特例
public abstract class DaoSupport{
public void save(Object entity){
}
public void delete(Object id){
}
}
public class PersonServiceBean extends Daosupport{}
小结:
- 泛化关系实际上就是继承关系
- 如果 A 类继承了 B 类,我们就说 A 和 B 存在泛化关系
6, 类图—实现关系
实现关系实际上就是 A 类实现 B 接口,他是依赖关系的特例
public interface PersonService {
public void delete(Interger id);
}
public class PersonServiceBean implements PersonService {
public void delete(Interger id){}
}
7, 类图—关联关系
小结
- 关联关系就是一种拥有关系,它使一个类知道另外一个类的属性和方法。就比如上图讲的,一个人有一个身份证这个对象,可以通过这个对象调用身份证方法。
- 关联可以时双向的也可以时单向的。
- 代码的实现就是成员变量
8, 类图—聚合关系
8.1, 基本介绍
聚合关系(Aggregation)表示的是整体和部分的关系,整体与部分可以分开。聚合关系是关联关系的特例,所
以他具有关联的导航性与多重性。
如:一台电脑由键盘(keyboard)、显示器(monitor),鼠标等组成;组成电脑的各个配件是可以从电脑上分离出来
的,使用带空心菱形的实线来表示:
8.2, 应用实例
小结
- 聚合关系是整体与部分的关系。是一种强关联关系。关联和聚合在语法上无法区分,必须考察具体的逻辑关系。
- 代码实现:成员变量。
9, 类图—组合关系
9.1, 基本介绍
组合关系:也是整体与部分的关系,但是整体与部分不可以分开。
再看一个案例:在程序中我们定义实体:Person 与 IDCard、Head, 那么 Head 和 Person 就是组合,IDCard 和
Person 就是聚合。
但是如果在程序中 Person 实体中定义了对 IDCard 进行级联删除,即删除 Person 时连同 IDCard 一起删除,那
么 IDCard 和 Person 就是组合了.
public class Person{
private IDCard card;
private Head head = new Head();
}
public class IDCard{}
public class Head{}
小结
- 组合关系也是整体与部分的关系。例如没有公司就没有部门,没有学校就没有学院。这是一种比聚合关系还要强的关系,他要求普通的聚合关系中代表整体的对象拥有部分对象的生命周期。
- 代码实现:成员变量
各种关系的强弱
泛化=实现>组合>聚合>关联>依赖
三,单例模式
1, 单例设计模式介绍
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,
并且该类只提供一个取得其对象实例的方法(静态方法)。
比如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个 SessionFactory 就够,这是就会使用到单例模式。
2, 单例设计模式八种方式
单例模式有八种方式:
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)
- 懒汉式(线程安全,同步代码块)
- 双重检查
- 静态内部类
- 枚举
3, 饿汉式(静态常量)
饿汉式(静态常量)应用实例
步骤如下:
- 构造器私有化 (防止 new )
- 类的内部创建对象
- 向外暴露一个静态的公共方法。getInstance
- 代码实现
package type1;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest01
* @Description: 单例模式之饿汉式
* @Author: ming
* @Date: 2022/4/13 20:16
* 饿汉式就是上来我就要一个对象可以用,且一个类只能右一个对象,那么我们就只能让构造方法私有化
* 从而只用我们在类中创建的这个对象。
* 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题(因为我只有一个对象)
* 缺点:在类装载的时候就创建了对象,极其消耗内存
*/
public class SingletonTest01 {
public static void main(String[] args) {
}
}
class Singleton {
/**
* 1,构造器私有化,外部不能调用
*/
private Singleton() {
}
//2,本来内部创建对象实例
private final static Singleton instance = new Singleton();
/**
* 3,提供一个公有的静态方法,返回实例对象
* @return
*/
public static Singleton getInstance() {
return instance;
}
}
优缺点说明:
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果
- 结论:这种单例模式可用,可能造成内存浪费
4, 饿汉式(静态代码块)
package type1;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest02
* @Description: 饿汉式(静态代码块)
* @Author: ming
* @Date: 2022/4/13 21:38
*/
public class SingletonTest02 {
public static void main(String[] args) {
//测试
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton02 {
private Singleton02() {
}
private static Singleton02 instance;
static {
instance = new Singleton02();
}
public static Singleton02 getInstance() {
return instance;
}
}
优缺点说明:
- 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
- 结论:这种单例模式可用,但是可能造成内存浪费
5, 懒汉式(线程不安全)
package type1;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest03
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/13 21:44
* 1,起到了Lazy Loading的效果,但是只能在单线程下使用。
* 2,如果在多线程下,一个线程进入了if语句,还未来得及往下执行,另外一个线程也通过这个判断语句,这个时候就会产生多个实例。
* 所以在多线程的时候不能使用这种方法
* 3,结论:在实际开发中,不要用这种方式
*/
public class SingletonTest03 {
public static void main(String[] args) {
System.out.println("懒汉式1 , 线程不安全~");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton03 {
private static Singleton03 instance;
private Singleton03() {
}
/**
* 饿汉式和懒汉式的区别就在于此,懒汉式是在我们需要这个对象的时候才会创建他
* @return
*/
public static Singleton03 getInstance() {
if (instance == null) {
instance = new Singleton03();
}
return instance;
}
}
优缺点:
- 起到了 Lazy Loading 的效果,但是只能在单线程下使用。
- 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式 。
- 结论:在实际开发中,不要使用这种方式 。
6, 懒汉式(线程安全,同步方法)
package type2;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest03
* @Description: 懒汉式(线程安全,同步方法)
* @Author: ming
* @Date: 2022/4/13 21:44
* 1,解决了线程安全问题。
* 2,效率太低了,每个线程在想活得类的实例时候,执行getInstance()方法都需要进行同步。而其实这个方法只执
* 行一次实例化就够了,后面想获取类的实例,直接return就行了,方法进行同步的效率太低。
* 3,结论:在实际开发中,不要用这种方式
*/
public class SingletonTest04 {
public static void main(String[] args) {
System.out.println("懒汉式2 , 线程安全~");
Singleton04 instance = Singleton04.getInstance();
Singleton04 instance2 = Singleton04.getInstance();
System.out.println(instance == instance2); // true
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton04 {
private static Singleton04 instance;
private Singleton04() {
}
/**
* 饿汉式的线程安全版就是在这个方法上加一个锁即可
* @return
*/
public static synchronized Singleton04 getInstance() {
if (instance == null) {
instance = new Singleton04();
}
return instance;
}
}
优缺点说明:
- 解决了线程安全问题
- 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低
- 结论:在实际开发中,不推荐使用这种方式
7, 懒汉式(线程安全,同步代码块)
不推荐使用
8, 双重检查
产生原因
正常的情况下,我们直接使用关键字synchronized将它加在方法getInstance()上面即可解决问题,但是加锁的操作是很影响程序性能的,由于同步一个方法会降低100倍或更高的性能,而每个线程在调用getInstance()都会获取类的锁,所以这是很消耗内存资源的;并且单例对象一旦初始化完成,获取和释放锁就显得很不必要,即一旦一个线程new 了单例对象,后面的程序再来获取锁并进行对象是否为空的判断很没有必要,所以出现了"双重检查锁定模式"
代码实现
package type3;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest06
* @Description: 双重检查
* @Author: ming
* @Date: 2022/4/13 22:08
* 1,他解决了线程安全的问题,也解决了速率的问题
*/
public class SingletonTest06 {
public static void main(String[] args) {
System.out.println("双重检查");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
/**
* 双重检查可以解决之前的所有问题。
* 加入线程a和线程b都调用了getInstance方法,这个时候线程a到了第一个if判断,线程b到了第二个if处,此时因为
* 线程b在同步块中,当线程b执行完后,线程a再次进行时就会发现已经创建好了实例,之后的线程也就都只能走到第一个if
* 同步块,而线程b卡在if处,
* @return
*/
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
注意:
第一次if判断是在避免不必要的加🔒和解🔒带来的内存占用,第二层if判断的作用在于:在单例对象被实例化之前,有1个以上的多线程通过了第一次检查,由于使用了同步块,所以它们需要排队进入同步块执行,那么第二个if判断的作用就体现在这里,即只要有一个线程通过了第二层的if判断,它就会实例化单例对象,这就让后面再进入同步块的线程不能再去实例化新的单例对象了。
第一次判断时为了防止乱加锁从而影响性能,第二次则是为了过滤都过了一层判断的线程。
除了上面的使用两层if之外,我们还需要使用关键字volatile去关闭我们的单例对象的指令重排,原因就是:new一个对象的正常步骤为
- ①为引用分配内存空间,此时对象的引用就不为空了,而是为这个内存地址
- ②执行构造,初始化对象
- ③将对象的地址传给对象的引用,就是将对象的地址存入引用执行的那片内存空间
但是在实例执行的时候,线程可能不是按照这样的顺序在执行,它可能的另一种顺序就是①③②,这就叫"指令重排";这样的顺序对于new对象的这个线程没有什么影响,在单线程中也没什么影响,但是在多线程的懒汉式单例模式下就会出现问题,即volatile的作用就是让对象在实例化的时候按照正常的方法执行
因为懒汉式单例只要判断单例对象的引用不为null,就认为这个对象已经创建好了,它就会直接返回对象的引用并在线程中调用对象的方法;如果线程A实例化单例对象采用的是①③②的顺序进行实例化,那么在进行了③但是还没进行②之前,线程B看到的单例对象的引用不为空,就会直接调用,但是实际上这片内存空间中什么都没有,就会出现空指针异常,所以我们需要关闭new单例对象的时候的指令重排,方式就是在对象的引用处使用关键字volatile即可,即使得这个引用的赋值过程变成一个原子性操作,不能变更
优缺点说明:
- Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。
- 这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步.
- 线程安全;延迟加载;效率较高
- 结论:在实际开发中,推荐使用这种单例设计模式
9, 静态内部类
package type4;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest07
* @Description: 静态内部类
* @Author: ming
* @Date: 2022/4/13 22:44
*/
public class SingletonTest07 {
public static void main(String[] args) {
System.out.println("使用静态内部类完成单例模式");
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2); // true
System.out.println("instance.hashCode=" + instance.hashCode());
System.out.println("instance2.hashCode=" + instance2.hashCode());
}
}
class Singleton {
private static volatile Singleton instance;
//构造器私有化
private Singleton() {}
//写一个静态内部类,该类中有一个静态属性 Singleton
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
**优缺点说明: **
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 结论:推荐使用.
10,枚举
10.1,出现的原因
- 由于霸道的反射技术的存在,所以即使我们的单例模式中使用了构造私有,但是反射一样可以破除该访问权限,获得到类的构造器,并且随意的new对象,这显然违反了单例模式的初衷
- 即单例模式并不安全
10.2,使用反射破坏单例模式
-
除了使用上面的3种方法实现单例模式以外,我们还可以使用反射
-
这个技术就很霸道了,不管你类里面的东西是不是私有的,它都能拿出来使用;所以反射可以破坏我们设计好的懒汉式单例模式
//反射破坏 Class<Singleton> singletonClass = Singleton.class; Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor(null); //设置构造器可以获取使用,这就破坏了单例模式的构造器私有 declaredConstructor.setAccessible(true); Singleton singleton = declaredConstructor.newInstance(); System.out.println("反射:" + singleton.hashCode());
-
那么我们如何解决这个问题呢?
答:在构造方法中搞事情。
private Singleton() { synchronized (Singleton.class) { if (singleton != null) { throw new RuntimeException("总有刁民想害朕,还想用反射搞我,做梦"); } } System.out.println(Thread.currentThread().getName()+"使用单例,并成功new 了一个对象"); }
-
此时我们做的操作就是将双重检查锁升级为了3重检查锁
-
甚至说我们都不需要调用getIntstance方法,我们可以直接通过反射掉构造器,这样就不存在判断。
-
解决办法,可以添加一个标识符,如果为false就不能用构造器了
private Singleton() { synchronized (Singleton.class) { if (flag) { System.out.println(Thread.currentThread().getName()+"使用单例,并成功new 了一个对象"); flag = true; }else { throw new RuntimeException("总有刁民想害朕,还想用反射搞我,做梦"); } } }
-
但是标志位始终还是一个成员属性,我们可以通过反射获取它,在获取一个对象之后又将它修改为false,这样不就又可以破坏单例模式了吗?所以这种我们之前写的所有的单例方式都存在被破坏的可能性。
10.3,使用枚举防止单例被破坏
Java官方给出了解决办法,反射之所以可以获取多个对象,是因为它可以调用方法constructor.newInstance(),我们可以去查看newInstance()的源码
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//如果通过反射获取的时一个枚举类就会直接报异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
所以,Java官方的办法就是直接对反射获取对象的过程下手,只要我们将代理模式设为枚举类型,那么就可以防止反射破坏我们的单例模式
那么我们可以做个尝试
package type5;
import java.lang.reflect.Constructor;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest08
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/13 23:17
*/
public class SingletonTest08 {
public static void main(String[] args) throws Exception{
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2);
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Singleton singleton = declaredConstructor.newInstance();
System.out.println(singleton.hashCode());
instance.sayOK();
}
}
//使用枚举,可以实现单例, 推荐
enum Singleton {
INSTANCE; //属性
public void sayOK() {
System.out.println("ok~");
}
}
那么我们尝试一下有参构造
package type5;
import java.lang.reflect.Constructor;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest08
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/13 23:17
*/
public class SingletonTest08 {
public static void main(String[] args) throws Exception{
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2);
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Singleton singleton = declaredConstructor.newInstance();
System.out.println(singleton.hashCode());
instance.sayOK();
}
}
//使用枚举,可以实现单例, 推荐
enum Singleton {
INSTANCE; //属性
public void sayOK() {
System.out.println("ok~");
}
}
这个错误就是我们上面看的源码说的错误。
总结
- 使用枚举类可以防止反射获取类的构造并创建对象
- 枚举类中的对象都是通过一个有参构造创建出来的,而不是一个无参构造
10.4,代码实现
package type5;
import java.lang.reflect.Constructor;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: SingletonTest08
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/13 23:17
*/
public class SingletonTest08 {
public static void main(String[] args) throws Exception{
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance == instance2);
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode());
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Singleton singleton = declaredConstructor.newInstance();
System.out.println(singleton.hashCode());
instance.sayOK();
}
}
//使用枚举,可以实现单例, 推荐
enum Singleton {
INSTANCE; //属性
public void sayOK() {
System.out.println("ok~");
}
}
11, 单例模式在 JDK 应用的源码分析
12,单例模式注意事项和细节说明
- 单例模型就是要求只能有一个对象通过的方法
- 饿汉式单例:就是一开始就去定义并实例化对象,不管后面到底用不用先拿了再说
- 缺点:浪费资源
- 原因:当new一个静态对象时,它就在内存中存在了,就换占用资源
- 缺点:浪费资源
- 所以为了解决这个问题就有了懒汉式单例
- 懒汉式单例:就是我可以先去定义好这个对象,但是我就是不去实例化,啥时候要我才会去使用
- 缺点是对于多线程支持不好,甚至不支持
- 于是乎诞生了DCL懒汉式单例
- DCL懒汉式单例:就是双重锁懒汉式单例
- 双重锁,第一把是为了避免他多加无用锁导致资源浪费,第二把锁是为了让别的线程无法再去实例化,同时还需要再对象的引用处使用关键字volatile,避免new的时候指令重排、
- 只有双重检测🔒+volatile关键字,才是真正解决懒汉式单例的完整且正确的方法
- 但是我们又想到了反射这个变态的方法,过于离谱,它可以破除访问的权限,获取到类构造器从而随意new对象,这显然是违反了我们的初衷所以就引入了,枚举类开对抗反射
- 原理在于反射方法中的源码,它规定了反射new对象的过程如果是枚举就直接报错抛出异常
- 所以我们只要用枚举类定义单例的类即可
四,工厂模式
1.什么是工厂模式
- 作用:
- 实现了创建者和调用者的分离(工厂模式为了让消费者和生产者分开)
- 详细分类:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式(不属于工厂模式,但是可以放在一起进行对比)
- 只包含简单工厂模式和工厂方法模式
- 核心本质:
- 实例化对象不使用new,用工厂方法代替
- 将选择实现类,创建对象统一管理和控制,从而将调用者跟我们的实现类解耦
- 三种模式
- 简单工厂模式:用来生产同一等级结构中的任意产品(对于增加新的产品,需要修改已有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品(支持增加任意产品)
- 抽象工厂模式:围绕一个超级工厂创建其他工厂,该超级工厂又称为其他工厂的工厂
2.简单/静态 工厂模式
我们以买车为例,首先我们创建一个Car接口,然后创建两个Car的实现类:五菱宏光、奔驰。
我们不用工厂模式的代码如下:
public interface Car { //实体类的接口
void name();
}
public class WuLing implements Car{ //五菱宏光
@Override
public void name() {
System.out.println("我是五菱宏光");
}
}
public class Benz implements Car{
@Override
public void name() {
System.out.println("我是梅赛德斯-奔驰");
}
}
创建一个用户类(测试类)
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Test
* @Description: 测试代码,也是我们消费者
* @Author: ming
* @Date: 2022/4/14 23:20
*
* 他的问题是我们直接调用的是构造方法,这样子的话我们还要去了解工厂做的事情,显然不是我们消费者想要的
*/
public class Test {
public static void main(String[] args) {
//模拟买了五菱宏光
Car car1 = new WuLing();
//模拟买了奔驰
Car car2 = new Benz();
car1.name();
car2.name();
}
}
这样子的代码是可以完成我们想要的操作的,但是却有点点越界了,凭什么我们一个用户还要去了解工厂如何去生产车的过程呢?我们想要的应该是我们只要告诉他我要什么车,你直接给我就行了,我不想参与别的事情。
所以我们的简单工厂模式就出现了。
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: CarFactory
* @Description: 工厂类
* @Author: ming
* @Date: 2022/4/14 23:29
*/
public class CarFactory {
public static Car car = null;
public static final String WuLing = "五菱";
public static final String Benz = "大奔";
public static Car getInstance(String name) {
if (WuLing.equals(name)) {
car = new WuLing();
}else if (Benz.equals(name)) {
car = new Benz();
}
return car;
}
}
测试代码如下:
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Test
* @Description: 测试代码,也是我们消费者
* @Author: ming
* @Date: 2022/4/14 23:20
*
* - 但是这种做法也存在弊端/缺陷,代码冗余,一旦实现类多起来就会出现大量的代码冗余,并且维护困难的问题,
* 并且上面这个做法也是不满足"开闭原则"的
*
*/
public class Test {
public static void main(String[] args) {
Car car1 = CarFactory.getInstance("五菱");
Car car2 = CarFactory.getInstance("大奔");
car1.name();
car2.name();
}
}
简单工厂解决上面的问题是通过我创建一个沟通的平台,类似于4s店,客户只要告诉4s店我要什么车,4s店就会给我们什么车,至于如何去生产,这个是4s店和厂商的问题,和用户没有关系。
但是我们发现一个很严肃的问题就是如果我们4s店又多了一个车的品种,不仅需要多一个厂商类,还需要加对我们这个4s店动手。这明显的违背了我们的开闭原则。
小结
- 对于像上面这样的工厂类,我们一般称为简单/静态 工厂模式,原因:因为这个类里面的所有方法都必须是静态的,并且对于类的扩展,不修改类的源代码是做不到的,所以简单/静态 工厂模式是有很大的弊端的
- 使用 简单/静态 工厂模式,屏蔽了我们实际使用具体实现类的时候的细节,主要就是new一个对象的时候需要传入哪些参数(即调用实现类的有参构造的调用),我们不需要管了,方便了调用者使用,只是它不满足开闭原则
- 获取的方法就是我们需要一个什么对象,汽车工厂可以根据我们提供的名字去创建这个对象,但是假如我们在类中多或者少一个对象的话就需要改变逻辑代码,所以我们用了静态方法
3.工厂方法模式
产生原因
工厂方法模式解决了简单/静态工厂模式每一次进行扩展的时候需要修改原工厂代码的弊端
我们可以分析原来扩展实现类的时候必须修改工厂类的原因:所有Car类的对象的创建都在一个Factory中,如果我们能够一个Car的实现类一个工厂,则当实现类扩张的时候,我们只需要重新定义一个这个实现类的工厂类就可以实现原来简单/静态工厂模式相同的效果,这样的做法就叫工厂方法模式
首先我们定义一个CarFactory接口,用于规范所有实现类的工厂类
public interface CarFactory1 {
public Car getInstance();
}
然后让所有的品牌去自己开一个4s店
public class BenzFactory implements CarFactory1{
@Override
public Car getInstance() {
return new Benz();
}
}
public class WuLingFactory implements CarFactory1{
@Override
public Car getInstance() {
return new WuLing();
}
}
进行测试
package 工厂模式;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Test
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/14 23:49
* 这样子的好处就是我们如果要新增一个品牌的汽车的话只要修改提供者,不需要修改使用者,保证了开闭原则
* 同时还满足了我们的要求。就像我们现实生活中每个品牌都有一个属于自己的4s店。
* 缺点是代码量多了
*/
public class Test {
public static void main(String[] args) {
//这里确实是用new去创建了对象,但是我们不需要传递任何属性,其实也将我们消费者和生产这给分开了。
Car car1 = new BenzFactory().getInstance();
Car car2 = new WuLingFactory().getInstance();
car1.name();
car2.name();
}
}
有没有发现这样子越来越像我们的现实生活了,每个品牌都有自己的4s店了,用户要啥车就去啥4s店去买,唯一的缺点就是4s店占地方,在代码中叫代码冗余。所以艺术源于生活。
注意
- 你可能觉得在使用对应的实现类的工厂的时候还要new,这不是返祖现象?回到了直接new Car的实现类的时代了吗?
- 注意:上面使用对应的实现类的工厂的时候确实使用了new,但是我们可以保证所有的工厂new的时候没有任何参数需要传入,但是new实现类的时候不能保证,所以使用工厂方法模式还是保留了工厂模式的好处,不用关系具体创建的细节
4.对比工厂方法模式和简单/静态 工厂模式
-
当我们使用工厂方法模式的时候我们首先只需要定义一个Car接口,一个Factory接口;然后只要有一个Car的实现类,我们就去创建一个这个类对应的Factory类,且实现Factory接口;最后的效果就是横向扩张的时候我们不会修改原来的代码,只需要跟着横向扩展一个实现类对应的工厂实现类即可
-
所以工厂方法模式的优点:在保留工厂模式特点的基础上,符合了OOP 7大原则中的开闭原则;缺点:每扩展一个实现类就要多扩展一个实现类对应的工厂类,这导致文件数量几乎达到使用简单/静态 工厂模式的时候的2倍
-
我们也可以从实际使用方面来对二者进行比较
-
可见,不管在哪一个方面,我们在实际使用方面,都应该更优先的使用简单/静态 工厂模式,而不是工厂方法模式,虽然工厂方法模式符合开闭原则,而简单/静态 工厂模式不符合,但是为了符合开闭原则就将整个项目结构变得复杂实在没有必要
-
即在实际开发中,我们不一定必须满足OOP 7大原则,而是根据实际的开发中使用哪一种方式进行开发更加便利且实用来决定(思想)
-
所以在实际的开发中我们更多的使用的是简单/静态 工厂模式
五,抽象工厂模式
1.什么是抽象工厂模式
- 注意:抽象工厂模式不属于工厂模式,抽象工厂模式和工厂模式是两中设计模式(有待商讨)
- 定义∶抽象工厂模式提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类
- 适用场景:
- 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
- 强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码
- 提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现
- 优点:
- 具体产品在应用层的代码隔离,无需关心创建的细节
- 将一个系列的产品统一到一起创建
- 缺点:
- 规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难;
- 增加了系统的抽象性和理解难度
-
产品族和产品等级
-
产品等级:就是由不同的工厂/厂商生产出的相同性值的产品;比如:上图中的小米和华为手机,都是手机,但是是从两个不同的工厂中生产的相同性值(手机)的产品
-
产品族:就是由一个工厂/厂商生产的,不同性值的产品;比如:上图中的小米手机和小米路由器,都是从一个工厂中生产出来的,但是是不同的产品性值(一个是手机,一个是路由器)
-
区分这两个概念的原因是因为抽象工厂模式是围绕产品族进行生产的,即抽象工厂模式生产的是一个可以创建一个产品族的产品的工厂/厂商,可以简单理解为创建了一家公司
2.代码实现
创建两个接口类:PhoneProduct+RouterProduct,用于规范生产某一产品(比如:手机和路由器)的时候必须实现的功能
public interface PhoneProduct {
void start();//开机
void shutdown();//关机
void call();//打电话
void message();//发短信
}
public interface RouterProduct {
void start();//开机
void shutdown();//关机
void openWifi();//启动wifi
void setting();//设置路由器
}
创建两个接口类:PhoneProduct+RouterProduct,用于规范生产某一产品(比如:手机和路由器)的时候必须实现的功能
public interface PhoneProduct {
void start();//开机
void shutdown();//关机
void call();//打电话
void message();//发短信
}
public interface RouterProduct {
void start();//开机
void shutdown();//关机
void openWifi();//启动wifi
void setting();//设置路由器
}
创建上面产品的实现类
小米系列产品
public class XiaomiPhone implements PhoneProduct{
@Override
public void start() {
System.out.println("小米手机-开机");
}
@Override
public void shutdown() {
System.out.println("小米手机-关机");
}
@Override
public void call() {
System.out.println("小米手机-打电话");
}
@Override
public void message() {
System.out.println("小米手机-发短信");
}
}
public class XiaomiRouter implements RouterProduct{
@Override
public void start() {
System.out.println("小米路由器-开机");
}
@Override
public void shutdown() {
System.out.println("小米路由器-关机");
}
@Override
public void openWifi() {
System.out.println("小米路由器-开启WiFi");
}
@Override
public void setting() {
System.out.println("小米路由器-设置WiFi参数");
}
}
=============================================================================
华为系列产品
public class HuaweiPhone implements PhoneProduct{
@Override
public void start() {
System.out.println("华为手机-开机");
}
@Override
public void shutdown() {
System.out.println("华为手机-关机");
}
@Override
public void call() {
System.out.println("华为手机-打电话");
}
@Override
public void message() {
System.out.println("华为手机-发短信");
}
}
public class HuaweiRouter implements RouterProduct{
@Override
public void start() {
System.out.println("华为路由器-开机");
}
@Override
public void shutdown() {
System.out.println("华为路由器-关机");
}
@Override
public void openWifi() {
System.out.println("华为路由器-开启WiFi");
}
@Override
public void setting() {
System.out.println("华为路由器-设置WiFi参数");
}
}
上面的接口就是实现了要生产的产品的约束,具体的产品类就按照接口规范实现自己的产品即可
接下来就是客户端获取产品和使用产品,按照工厂设计模式的使用步骤,现在我们应该定义一个简单/静态工厂类,用于返回对应产品的实例;但是按照抽象工厂模式的原则,在创建工厂之前,我们应该创建一个工厂接口,该接口用于规范所有的工厂类
比如:要实现一个工厂类,就必须要有生产手机和路由器的方法
public interface IProductFactory { //抽象的产品工厂,用于规范创建的工厂
//工厂必须能够生产手机
PhoneProduct phoneProduct();
//工厂必须能够生产路由器
RouterProduct routerProduct();
}
在上面工厂接口的基础上,我们再来根据产品族的不同,实现工厂接口
public class XiaomiFactory implements IProductFactory{
@Override
public PhoneProduct phoneProduct() {
return new XiaomiPhone();
}
@Override
public RouterProduct routerProduct() {
return new XiaomiRouter();
}
}
==============================================================
public class HuaweiFactory implements IProductFactory{
@Override
public PhoneProduct phoneProduct() {
return new HuaweiPhone();
}
@Override
public RouterProduct routerProduct() {
return new HuaweiRouter();
}
}
客户端测试
public class Client {
public static void main(String[] args) {
System.out.println("========小米系列产品========");
XiaomiFactory xiaomiFactory = new XiaomiFactory();
PhoneProduct phone1 = xiaomiFactory.phoneProduct();
RouterProduct router1 = xiaomiFactory.routerProduct();
System.out.println("========小米手机========");
phone1.start();
phone1.call();
phone1.message();
phone1.shutdown();
System.out.println("========小米路由器========");
router1.start();
router1.openWifi();
router1.setting();
router1.shutdown();
System.out.println("========华为系列产品========");
HuaweiFactory HuaweiFactory = new HuaweiFactory();
PhoneProduct phone2 = HuaweiFactory.phoneProduct();
RouterProduct router2 = HuaweiFactory.routerProduct();
System.out.println("========华为手机========");
phone2.start();
phone2.call();
phone2.message();
phone2.shutdown();
System.out.println("========华为路由器========");
router2.start();
router2.openWifi();
router2.setting();
router2.shutdown();
}
}
看完整体的实现,我们发现产品的产生都是通过建立了相关的接口从而一步步的实现的,例如说我们生产手机,必须要有一个手机的接口,最后再通过创建一个超级工厂,然后在建立不同品牌的工厂实现的。
让我们仔细的看看类图
通过上面的例子我们可以看出:抽象工厂模式,就是对于工厂本身也要进行抽象,且抽象的工厂是针对产品族的抽象,所有的工厂类都要实现这个接口,所以所有的工厂类都需要生产工厂抽象中有的产品,即需要生产这个产品族中的所有产品,如果不想生产的工厂,可以直接返回null;即一个工厂中必须要生产一系列的东西,其对应获取产品实例的方法我们都应该定义在该接口中,然后我们按照一个产品族一个工厂接口实现类的标准,在工厂类中实现本产品族中具体产品的生产(就是new对象)
比如上面例子中,我们定义了一个抽象接口IProductFactory,这个接口要求实现它的工厂类必须实现生产手机和路由器的方法,即一个工厂就必须要生产这两款产品,不想生产可以直接返回null,但是方法体还是要存在;当我们需要生产小米手机的时候,我们就创建了一个工厂类XiaomiFactory去实现接口IProductFactory,然后我们实现生产手机和路由器的时候,直接return new 小米手机/路由器的构造,这就实现了小米工厂产品族中产品的生产,就是实现了这两款产品的工厂类中获取对应实例的方法
当客户端需要使用小米系列产品的时候,只需要new一个小米产品族工厂类,调用抽线工厂接口中的方法就可以获取到对应的产品,这就保留了原来工厂模式的优点:将产品生产和使用分离
此时我们再来回顾一下抽象工厂模式的定义
- 定义∶抽象工厂模式提供了一个创建一系列相关或者相互依赖对象(即一个产品族中的产品)的接口,无需指定它们具体的类
- 适用场景:
- 客户端(应用层)不依赖于产品类实例如何被创建、实现等细节(工厂模式)
- 强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码
- 提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现
- 优点:
- 具体产品在应用层的代码隔离,无需关心创建的细节
- 将一个系列的产品统一到一起创建
- 缺点:
- 规定了所有可能被创建的产品集合,产品族中扩展新的产品困难(因为一旦新增一个工厂类需要生产的产品,那么所有的工厂实现类都要新增获取这个产品的方法)
- 增加了系统的抽象性和理解难度
3.小结
- 抽象工厂模式和工厂模式的区别
- 抽象工厂将注意力从产品转移到了工厂本身/产品族上,原来的工厂模式主要注意产品的生产
- 在我们的产品族生产的产品种类比较稳定的时候,我们应该优先使用抽象工厂模式,这样使用起来更有效率;如果产品族生产的产品变化比较频繁,我们应该优先使用简单工厂模式,总之具体选取哪一个模式还是根据自己的业务需求来定的
- 我感觉哈(不一定正确),抽象工厂模式其实和普通的工厂模式是极其相似的,大体的创建实现流程都一样,但是不同的是我们抽象工厂的关注点是一个产品族,也就是一个品牌,我们可以生产一系列该品牌的东西,而普通的工厂模式只能生产一个品牌。换言之就是抽象工厂就像是改进版的普通工厂模式。但是如果要新增产品的话是很复杂的,主要是用于创建产品固定,但是品牌不固定的模式。
- 简单小结抽象工厂模式,就是生产产品的工厂的抽象,我们可以通过抽象工厂的接口实现对于工厂要生产的产品的约束
- 简单工厂模式(静态工厂模式)
- 虽然某种程度上不符合设计原则,但实际使用最多!
- 工厂方法模式
- 不修改已有类的前提下,通过增加新的工厂类实现扩展
- 抽象工厂模式
- 不可以增加产品,可以增加产品族,是工厂的抽象
- 应用场景:
- JDK中Calendar的getInstance方法
- JDBC中的Connection对象的获取
- Spring中IOC容器创建管理bean对象
- 反射中Class对象的newInstance方法
- 自己的理解:抽象工厂就是先通过接口定义产品的具体方法,再去实现它,然后呢我们就知道了我们到底有啥产品族(产品的类型,不考虑功能),我们通过这个来创建一个工厂,工厂必须要有所有产品族的创建方法,不同的产品等级(即不同品牌的产品)可以同时实现这个接口创建俩具体的工厂,然后当我们要去调用的时候,我们需要先去new这个工厂,通过工厂去创建我们的对象从而去调用它的方法。我们经常使用的场景就是当代码大规模重复,且不增加对象时可以使用
六,建造者模式
1.什么是建造者模式
-
建造者模式也属于创建型模式,它提供了一种创建对象的最佳方式
-
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
-
主要作用:在用户不知道对象的建造过程和细节的情况下就可以直接创建复杂的对象
-
用户只需要给出指定复杂对象的类型和内容,建造者模式负责按顺序创建复杂对象(把内部的建造过程和细节隐藏起来)
-
例子:
- 工厂(建造者模式)∶负责制造汽车(组装过程和细节在工厂内)
- 汽车购买者(用户)︰你只需要说出你需要的型号(对象的类型和内容,然后直接购买就可以使用了(不需要知道汽车是怎么组装的(车轮、车门、>发动机、方向盘等等))
- 即一个复杂类的各个部分的创建由工厂模式实现,复杂对象的组装由建造者模式实现
-
角色分析
-
既然是建造者模式,那么我们还是继续造房吧,其实我也想不到更简单的例子;假设造房简化为如下步骤:(1)地基(2)钢筋工程(3)铺电线((4)粉刷;“如果”要盖一座房子,首先要找一个建筑公司或工程承包商(指挥者),承包商指挥工人(具体建造者)过来造房子(产品),最后验收
-
从上图可知:我们需要创建4个类,Director指挥类、Builder抽象的建造者类、Worker具体实现Builder抽象的建造者类的工人类、Product具体的产品类;其中逻辑为:Director–(指挥)–>Builder–(创建)–>Product,只是我们需要使用Builder抽象类的实现类Worker类来具体的实现创建产品的流程
2.代码实现
-
首先创建一个需要被创建的产品类Product,这个产品有多个需要被工人建造的属性,只有当属性设置完成之后(调用成员属性的set()),这个产品才算是创建成功
/** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Product * @Description: 建造者建造的产品 * @Author: ming * @Date: 2022/4/16 21:49 */ public class Product { //一个房子应该有的4个成员属性 private String buildA; private String buildB; private String buildC; private String buildD; public String getBuildA() { return buildA; } public void setBuildA(String buildA) { this.buildA = buildA; } public String getBuildB() { return buildB; } public void setBuildB(String buildB) { this.buildB = buildB; } public String getBuildC() { return buildC; } public void setBuildC(String buildC) { this.buildC = buildC; } public String getBuildD() { return buildD; } public void setBuildD(String buildD) { this.buildD = buildD; } @Override public String toString() { return "Product{" + "buildA='" + buildA + '\'' + ", buildB='" + buildB + '\'' + ", buildC='" + buildC + '\'' + ", buildD='" + buildD + '\'' + '}'; } }
-
创建一个抽象的建造者类Builder,建造者类主要就是定义产品类Product中属性的建造方法,即定义我们要为某一个成员属性赋什么样的值,怎么赋值等操作,最后暴露一个获取产品对象的方法
public abstract class Builder { //抽象的建造者,定义建造的4个步骤 abstract void buildA(); //步骤1:地基 abstract void buildB(); //步骤2:钢筋工程 abstract void buildC(); //步骤3:铺电线 abstract void buildD(); //步骤4:粉刷 abstract Product getProduct(); //返回一个创建好的房子,返回一个生产完成的产品 }
-
创建一个工人类Worker,这个类主要用于从0开始创建一个真正的、明确的产品,它用于继承建造者类Builder并实现其中定义好的建造一个产品需要经过的详细步骤
/** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Worker * @Description: 请描述该类的功能 * @Author: ming * @Date: 2022/4/16 21:52 */ public class Worker extends Builder{ //具体的建造者:工人 private Product product; //工人要完成的产品 public Worker() { //通过构造器实例化工人要完成的产品对象 this.product = new Product(); } @Override void buildA() { product.setBuildA("地基建造"); System.out.println("产品的buildA属性设置"); } @Override void buildB() { product.setBuildA("钢筋工程"); System.out.println("产品的buildB属性设置"); } @Override void buildC() { product.setBuildA("铺电线"); System.out.println("产品的buildC属性设置"); } @Override void buildD() { product.setBuildA("粉刷"); System.out.println("产品的buildD属性设置"); } @Override Product getProduct() { return product; } }
-
创建一个指挥者类Director,这个类在整个生产过程中非常重要,因为我们需要将整个产品的生产核心步骤放在这个类里面,即怎么合理的调用建造者类Builder中定义好的步骤,指挥工人类Worker创建出一个产品就是依靠这个类
/** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Director * @Description: 指挥类 * @Author: ming * @Date: 2022/4/16 21:53 * 指挥【核心】,负责指挥整个产品的创建步骤,主要就是按照逻辑调用Builder中定义好的创建一个产品的步骤 */ public class Director { public Product build(Builder builder){ //将实际进行这项工作的工人/worker对象传入,让工人按照这个方法中的步骤创建产品 //按照实现逻辑一步步的构建生产产品 builder.buildA(); builder.buildB(); builder.buildC(); builder.buildD(); //将生产好的产品返回给调用处 return builder.getProduct(); } }
-
创建一个实现类
/** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Test * @Description: 请描述该类的功能 * @Author: ming * @Date: 2022/4/16 21:54 */ public class Test { public static void main(String[] args) { //创建一个指挥工人干活的指挥者 Director director = new Director(); //创建一个实际干活的工人 Worker worker = new Worker(); //指挥者指挥工人生产产品 Product product = director.build(worker); System.out.println(product); } }
-
上面示例是Builder模式的常规用法,指挥者类Director在建造者模式中具有很重要的作用,它用于指导具体工人类如何正确的构建产品,通过控制调用先后次序,并向调用者返回完整的产品类实现产品的生产;但是有些情况下需要简化系统结构,可以把Director指挥者类和Builder抽象建造者进行结合
-
这种方式我们可以通过指挥者去修改创建的一个过程,这种方法不能缺少指挥者的原因是,我们的工人类在创建的时候产品类是没有默认值的。除非说你工人自己掉自己的方法,但是如果这样做,没有任何意义。
-
类图如下
-
我们可以通过静态内部类方式实现零件无序装配构造,这种方式使用更加灵活,更符合定义。内部有复杂对象的默认实现,使用时可以根据用户需求自由定义更改内容,并且无需改变具体的构造方式,就可以生产出不同复杂产品。
-
类图如下
-
比如∶麦当劳的套餐,服务员(具体建造者)可以随意搭配任意几种产品(零件)组成一款套(产品),然后出售给客户;比第一种方式少了指挥者,主要是因为第二种方式把指挥者交给用户来操作,使得产品的创建更加简单灵活,即用户可以DIY自己要获取的产品
代码实现
-
产品类Product1
public class Product1 { //建造者建造的产品 //一个套餐/产品中应该有的4个成员属性,这里我们为这个麦当劳中的套餐类中的成员属性设置了默认值 //有了默认值只要使用的时候我们不修改,获取到的产品就是这些默认值对应的产品 private String buildA = "可乐"; private String buildB = "汉堡"; private String buildC = "薯条"; private String buildD = "甜点"; public String getBuildA() { return buildA; } public void setBuildA(String buildA) { this.buildA = buildA; } public String getBuildB() { return buildB; } public void setBuildB(String buildB) { this.buildB = buildB; } public String getBuildC() { return buildC; } public void setBuildC(String buildC) { this.buildC = buildC; } public String getBuildD() { return buildD; } public void setBuildD(String buildD) { this.buildD = buildD; } @Override public String toString() { return "Product{" + "buildA='" + buildA + '\'' + ", buildB='" + buildB + '\'' + ", buildC='" + buildC + '\'' + ", buildD='" + buildD + '\'' + '}'; } }
-
生产产品的抽象类Builder1
public abstract class Builder1 { //抽象的建造者,定义建造的4个步骤 abstract Builder1 buildA(String msg);//可乐生产 abstract Builder1 buildB(String msg);//汉堡 abstract Builder1 buildC(String msg);//薯条 abstract Builder1 buildD(String msg);//甜点 abstract Product1 getProduct(); //返回一个创建好的套餐对象,即返回一个生产完成的产品 }
-
生产产品的具体工人类Worker1(工人可以修改一些属性,在这个例子中,我们工人可以根据用户的需求做一些定制化的操作)
public class Worker1 extends Builder1 { private Product1 product; public Worker1() { this.product = new Product1(); } @Override Builder1 buildA(String msg) { product.setBuildA(msg); System.out.println("buildA属性设置成功/生产完成!"); return this; } @Override Builder1 buildB(String msg) { product.setBuildB(msg); System.out.println("buildB属性设置成功/生产完成!"); return this; } @Override Builder1 buildC(String msg) { product.setBuildC(msg); System.out.println("buildC属性设置成功/生产完成!"); return this; } @Override Builder1 buildD(String msg) { product.setBuildD(msg); System.out.println("buildD属性设置成功/生产完成!"); return this; } @Override Product1 getProduct() { return this.product; } }
-
测试类Test1
package 静态内部类; /** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Test1 * @Description: 实现类 * @Author: ming * @Date: 2022/4/16 22:18 * 我们发现,指定每一步的套餐是,他会给定一个默认的值, */ public class Test1 { public static void main(String[] args) { //获取工人/麦当劳中的服务员 Worker1 worker = new Worker1(); //直接调用工人生产产品 Product1 product = worker.getProduct(); System.out.println(product); } }
-
当用户不想要可乐,想喝雪碧的时候
package 静态内部类; /** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Test1 * @Description: 实现类 * @Author: ming * @Date: 2022/4/16 22:18 * 我们发现,指定每一步的套餐是,他会给定一个默认的值, */ public class Test1 { public static void main(String[] args) { //获取工人/麦当劳中的服务员 Worker1 worker = new Worker1(); worker.buildA("雪碧"); //直接调用工人生产产品 Product1 product = worker.getProduct(); System.out.println(product); } }
-
在上面的这个例子中我们可以发现,相较于原来的方式,我们少写了一个指挥者Director,可以缺少它的原因是我们在产品类Product中已经为产品的所有属性都赋上了初值,由于工人类Worker 被new的时候会自动new一个它内部的Product对象属性,所以此时工人获取到的就是一个完整的可以直接使用的产品,只是产品的各个属性都是默认值
-
当客户端获取工人对象的时候,可以直接调用工人对象.getProduct()就能够获取到所有属性都是默认值的完整的产品对象;如果用户想要对产品的某些属性进行修改,只需要调用Product.setXXX()即可实现对于默认值的覆盖,所以此时工人类的作用从原来被指挥者Director调用,从0开始创建一个完整的产品,变为现在当客户端有自定义需求的时候,调用工人类Worker中对应属性的方法即可完成对于默认产品的属性进行修改的需求;即从创造产品–>修改产品
-
这两个方法可以理解为,一个是给了你面,肉,请一个专业人士指挥你做汉堡,然会给客户用,客户只和专业人士反应需求。静态内部类的则是我直接给了你现成的几种产品,工作人员可以依据用户需求去做。本质可以理解为一个是无中生有,一个是组装件。
3.源码说明
![\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pWSwKU4e-1650638371622)(%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.assets/1650122422331.png)\]](https://img-blog.csdnimg.cn/00b6ff5f02ea432da1c0cc24c5ffcfd5.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA44CCbWluZ-OAgg==,size_20,color_FFFFFF,t_70,g_se,x_16)
4.小结
- 建造者模式的优点
- 产品的建造和表示分离,实现了解耦,使用建造者模式可以使客户端不必知道产品内部组成的细节
- 将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰
- 具体的建造者类之间是相互独立的,这有利于系统的扩展,增加新的具体建造者无需修改原有类库的代码,符合“开闭原则“
- 建造者模式的缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制(每一个产品都有自己的属性的话,要么就要创建一个很多属性的类,要么就是要创建很多产品类,同时还有建造类,想想就头疼)
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大
- 建造者模式的应用场景
- 需要生成的产品对象有复杂的内部结构,这些产品对象具备共性
- 适合于一个具有较多的零件(属性)的产品(对象)的创建过程
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品
- 建造者与抽象工厂模式的比较:
- 与抽象工厂模式相比,建造者模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族
- 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在建造者模式中,客户端可以不直接调用建造者的相关方法,而是通过指挥者来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象
- 如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车
- 自己的理解
- 创建者模式就是我们要先知道他的属性,然后就可以通过他的属性去创建一个抽象工厂来抽象他的方法,再去创建一个工人类继承即具体实现它的方法,最后呢需要一个指挥者指挥一下按他的全过程
- 但是她的限制就是我们的属性必须要类似
- 而且与抽象工厂相比,创建者就是我可以去创建一个完整的对象,但是抽象工厂则是生产一个产品族
七,原型模式
1.什么是原型模式
- 原型(prototype)的理解需要和复制/克隆联系
- 原型的意思就是按照对象A,复制/拷贝一份,形成一个单独独立的新的对象B,我们对对象B进行操作,而原来被复制/拷贝的对象A就称为对象B的原型
- 原型模式的使用场景就是当一个对象的创建比较复杂的时候,我们耗费了一些资源好不容易创建出了一个对象A,如果此时我们需要再创建一个对象B,那么我们不需要再去经过前面繁琐的步骤创建对象B,而是直接复制粘贴对象A,得到一个完整对立的对象B
- 原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节
- 工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它
- 形象的理解:孙大圣拔出猴毛, 变出其它孙大圣
2.怎么使用原型模式
-
原型模式的克隆实现方式就是原来我们学习的对象的克隆,即实现接口Cloneable(可克隆的)并调用方法clone()
-
克隆实现原理
1.代码实现
-
定义一个类用于被克隆(需要继承Cloneable接口,实现重写clone)
package improve; /** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Sheep * @Description: 继承了克隆的接口 * @Author: ming * @Date: 2022/4/18 20:27 */ public class Sheep implements Cloneable { private String name; private int age; private String color; private String address = "蒙古羊"; public Sheep friend; //是对象, 克隆是会如何处理 public Sheep(String name, int age, String color) { super(); this.name = name; this.age = age; this.color = color; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } @Override public String toString() { return "Sheep [name=" + name + ", age=" + age + ", color=" + color + ", address=" + address + "]"; } //克隆该实例,使用默认的clone方法来完成 @Override protected Object clone() { Sheep sheep = null; try { sheep = (Sheep)super.clone(); } catch (Exception e) { // TODO: handle exception System.out.println(e.getMessage()); } // TODO Auto-generated method stub return sheep; } }
-
编写一个类进行克隆
package improve; /** * @version v1.0 * @ProjectName: 设计模式 * @ClassName: Client * @Description: 请描述该类的功能 * @Author: ming * @Date: 2022/4/18 20:28 */ public class Client { public static void main(String[] args) { System.out.println("原型模式完成对象的创建"); // TODO Auto-generated method stub Sheep sheep = new Sheep("tom", 1, "白色"); sheep.friend = new Sheep("jack", 2, "黑色"); Sheep sheep2 = (Sheep)sheep.clone(); //克隆 Sheep sheep3 = (Sheep)sheep.clone(); //克隆 Sheep sheep4 = (Sheep)sheep.clone(); //克隆 Sheep sheep5 = (Sheep)sheep.clone(); //克隆 sheep.setAge(12); System.out.println("sheep2 =" + sheep2 + "sheep2.friend=" + sheep2.friend.hashCode()); System.out.println("sheep3 =" + sheep3 + "sheep3.friend=" + sheep3.friend.hashCode()); System.out.println("sheep4 =" + sheep4 + "sheep4.friend=" + sheep4.friend.hashCode()); System.out.println("sheep5 =" + sheep5 + "sheep5.friend=" + sheep5.friend.hashCode()); } }
3. 深入讨论-浅拷贝和深拷贝
3.1, 浅拷贝的介绍
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
- 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值
- 前面我们克隆羊就是浅拷贝
- 浅拷贝是使用默认的 clone()方法来实现
sheep = (Sheep) super.clone();
3.2. 深拷贝基本介绍
- 复制对象的所有基本数据类型的成员变量值
- 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝
- 深拷贝实现方式 1:重写 clone 方法来实现深拷贝
- 深拷贝实现方式 2:通过对象序列化实现深拷贝(推荐)
代码实现
package deepclone2;
import java.io.*;
import java.util.Date;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: Prototype
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/18 20:57
*/
public class Prototype implements Cloneable, Serializable { //需要被克隆的类
private String name;
private Date date;
public Prototype(String name, Date date) {
this.name = name;
this.date = date;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
@Override
protected Object clone() throws CloneNotSupportedException {
//覆写Object的clone()
Object obj = super.clone();//obj就是返回的克隆对象
Prototype prototype = (Prototype) obj;//强转z类型设置时间属性
prototype.setDate((Date) this.date.clone());//设置克隆对象的date属性为当前进行克隆对象的原型对象的date成员属性的克隆
return obj;//返回克隆好的对象
}
@Override
public String toString() {
return "Prototype{" +
"name='" + name + '\'' +
", date=" + date +
'}';
}
public Object deepClone() throws IOException {
/**
* ByteArrayOutputStream实现了一个输出流,其中数据被写入一个字节数组。缓冲区会随着数据写入而自动增长。可以
* 使用toByteArray()和toString()检索数据。关闭ByteArrayOutputStream无效。可以在流关闭后调用此类中的方法,而不
* 会生成IOException 。
*
* ObjectOutputStream 将 Java 对象的原始数据类型和图形写入 OutputStream。可以使用 ObjectInputStream 读取(重构)
* 对象。对象的持久存储可以通过使用流的文件来实现。如果流是网络套接字流,则可以在另一个主机或另一个进程中重构对象。
*
* ByteArrayInputStream包含一个内部缓冲区,其中包含可以从流中读取的字节。内部计数器跟踪read方法提供的下一个字节。
* 关闭ByteArrayInputStream无效。可以在流关闭后调用此类中的方法,而不会生成IOException 。
*
* ObjectInputStream 反序列化以前使用 ObjectOutputStream 编写的原始数据和对象。
* 当分别与 FileOutputStream 和 FileInputStream 一起使用时,ObjectOutputStream 和 ObjectInputStream 可以为应用
* 程序提供对象图的持久存储。 ObjectInputStream 用于恢复之前序列化的那些对象。其他用途包括使用套接字流在主机之间传
* 递对象或在远程通信系统中编组和解组参数和参数。
*/
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
//序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
//反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
Prototype copyObj = (Prototype) ois.readObject();
return copyObj;
}catch (Exception e) {
e.printStackTrace();
return null;
} finally {
bos.close();
oos.close();
bis.close();
ois.close();
}
}
}
4.小结
-
原型模式
- 就是因为有些对象他的创建比较复杂,所以我们想去直接复制已经创建的对象,所以才出来原型模型
- 他是先去创建一个需要被克隆的类,继承Cloneable
- 然后写入这个类的对象并进行方法的clone方法(如果直接返回这个对象的话,就是浅克隆,如果是重写了这个方法,就是通过属性去调用clone方法就是深克隆)
- 编写一个类进行克隆,通过调用clone方法来实现克隆
- 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了 ocp 原则,这点请同学们注意.
-
到此,所有的创建型模式就讲解完了
- 单例模式:保证类的实例全局唯一
- 工厂模式:实现了创建者和调用者的分离;分为简单工厂模式和工厂方法模式
- 抽象工厂模式:就是对工厂进行抽象,创建的工厂针对于生产产品族,即工厂可以生产一系列的产品
- 建造者模式:实现了对象的创建和使用的分离
- 原型模式:通过Java提供的克隆方法提高复杂对象的生产方式
八,适配器设计模式
1. 现实生活中的适配器例子
需求:现在很多轻薄笔记本电脑上都没有为我们预留网线接口,那么我们要怎么通过网线上网呢?答案就在下图,我们需要买一个 转换器/适配器,这个适配器就是来解决网线接口和笔记本接口不一致的问题
- 我们来捋一捋关系:网线 --(连接)–> 适配器 --(连接)–> 电脑接口,电脑接口–(调用)–>适配器连接上网的功能方法–(调用)–>网线的上网功能方法,所以我们可以抽象出3个类,网线类、适配器类和电脑类
- 根据上面的关系我们可以知道:电脑类中应该接收一个适配器的对象,而适配器类中应该接收一个网线对象,这样才符合我们上面分析的关系
2.什么是适配器模式
- 将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作
- 角色分析
- 需要适配的类(正常的网线接口):需要适配的类或适配者类
- 适配器(网线转usb接口):通过包装一个需要适配的对象,把原接口转换成目标对象
- 目标接口(电脑上的usb接口):客户所期待的接口,目标可以是具体的或抽象的类,也可以是接口
- 主要分为三类:类适配器模式、对象适配器模式、接口适配器模式
3.类适配器模式
-
网线类NetWire,网线的功能就是连接上网络,让电脑能够上网,所以我们将这个功能抽象为这个类的一个方法request()
public class NetWire { //要被适配的类,比如就是网线 public void request(){ //网线的功能,比如连接上网络 System.out.println("网线已连接,可以正常连接网络"); } }
-
适配器接口NetToUsb,注意:网线连接适配器,适配器连接电脑,所以网线的上网功能应该是适配器调用,电脑调用的是适配器向上暴露的方法
public interface NetToUsb { //转换器的抽象实现 //作用:作为不能直接传递数据的Adapter类和Computer类的数据传输桥梁 public void handleRequest(); }
-
适配器接口实现类Adapter,首先适配器实现类肯定是要实现适配器接口的,在实现的接口方法中必然会调用网线的request()实现电脑的网络连接,所以在该实现类中我们需要有能够调用方法request()的条件,要么组合,要么继承,这里我们直接使用继承
public class Adapter extends NetWire implements NetToUsb{ //适配器类,需要连接电脑和网线两个类 @Override public void handleRequest() { super.request(); //调用父类网线类的连接网络方法实现电脑连接网络 } }
-
电脑类,这个类只需要有一个连接上网的方法即可,这个方法只需要传入一个适配器对象,然后调用适配器对象的handleRequest()即可连接上网络
import sun.applet.Main; public class Computer { //客户端类:电脑类 public void net(NetToUsb adapter){ //电脑提供的功能:上网 adapter.handleRequest(); //处理上网的请求 } }
-
测试
public static void main(String[] args) { //需要的对象:电脑、网线、适配器 Computer computer = new Computer(); //电脑对象 NetWire netWire = new NetWire(); //网线对象 NetToUsb adapter = new Adapter(); //适配器对象 computer.net(adapter); //调用电脑对象的连接网络的方法net() }
-
在上面的代码测试中我们发现我们并没有使用网线类就可以完成上网的功能。其原因是我们的适配器是继承了这个网线类,有了它的功能,在使用时我们只要一个电脑和适配器即可完成上网的功能,也就是我们说的集成
-
好处:在实现相同的上网功能时,我们手动创建的对象变少了;
-
坏处:适配器是通过继承获取的上网功能,到那时Java只能单继承,也就是说我这个适配器已经没有扩展的功能了。
-
但是真实的使用情况并不是这样,我们实际使用的情况上上面图片中的样子,即网线连接适配器,适配器连接电脑,电脑通过调用适配器的方法间接的调用网线的上网方法;
4.对象适配器
- 上面在实现适配器的时候,我们直接使用适配器继承了网线类,所以我们的适配器被固定只能实现网线上网的功能;如果我们使用组合的方式呢?使用组合的方式,适配器中可以存储多个具体功能的类,然后我们可以在适配器类中定义调用对应类的功能的方法
- 电脑对象只需要有一个适配器对象,并将其他功能对象传入适配器对象中,就可以调用适配器向我们暴露的方法间接的调用多个功能类的功能方法
- 好处:将多个功能类的具体调用封装到了一个适配器类中,我们只需要在使用的时候将电脑类、适配器类和要使用的功能类new出来,并将功能类传入适配器类中,就可以调用适配器类中的方法实现功能类功能的间接调用,我们自己不需要再去理会功能类具体是怎么调用的,这降低了客户端与功能类之间的耦合;并且提高了适配器类的扩展性,可以集成多个功能类;在使用的时候传入对应的功能类对象就类似于实际使用的时候将网线插在适配器上了
- 坏处:每增加一个功能类,我们就需要去修改适配器类的代码,添加对应功能类的调用方法供客户端调用
-
通过组合,我们可以将适配器做成一个与任何功能类无关的独立的类,在使用的时候只需要将对应的功能类传入适配器类中,然后将适配器对象传入实用类中,再通过使用类就可以间接调用功能类的方法,实现对应的功能
-
好处:减少了使用类与功能类之间的耦合,我们只需要有功能类的对象,对应的方法调用全部丢给适配器类;
-
而适配器类的设计就显得尤为重要,就比如上面的电脑和适配器,适配器类的设计应该完全遵循电脑接口对应的功能类进行设计,即我们将电脑上所有的接口上的功能类全部集成到适配器类中,并将电脑上对应接口的功能类的方法调用也全部实现在适配器类中,那么实现的效果就是:整个电脑只需要开一个和适配器连接的接口就够了,其他的电源接口、usb接口、网线接口、HDMI接口…这么多的接口全部都从电脑上转移到适配器上面,要使用什么东西直接插在是适配器上,电脑上始终只需要插上一个适配器即可
5.小结
-
对象适配器优点【推荐使用,适配器和功能类进行组合】
- 一个对象适配器可以把多个不同的适配者适配到同一个目标
- 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配
-
类适配器缺点【有局限性(不通用),适配器继承功能类】
- 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者
- 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性
-
适用场景
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码
- 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作
- 适配器在spring全家桶中使用的地方很多,比如springMVC中配置的处理器适配器HandleAdapter
-
我自己的理解
- 适配器模式分为对象适配器和类适配器
- 类适配器的工作流程:先去写一个要被适配器适配的功能类,然后去写一个适配器的接口类(作为主体和适配器间的数据交换的桥梁),现在就可以去写一个适配器的实现类,这个类继承与被适配功能的类,且实现了适配器接口,调用被适配的方法,在主体中则是通过调用这个适配器来实现方法。换言之就是我通过把被适配的功能集成与适配器,去给主体使用。
- 对象适配器:先去写一个要被适配器适配的功能类,然后去写一个适配器的接口类(作为主体和适配器间的数据交换的桥梁),现在就可以去写一个适配器的实现类,但是现在这个类变为了组合,即就是把被代理的功能类给倒进去了,并传递了实例,通过实例来调用。换言之就是电脑调用适配器,适配器在去调用被代理的功能类
九,桥接模式bridge
1.什么是桥接模式
-
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式
-
上图表示了一个多继承的关系图,各种类型的电脑继承了电脑类,各个厂商又根据不同类型的电脑实现类自己品牌下该电脑产品,但是上面这样的继承模式扩展的时候是很麻烦的
-
比如:现在华为进军电脑市场,我们就需要分别在上图中的台式、笔记本和平板下面写3个华为品牌子类,只要是增加一个品牌就要新增3个子类
-
从另一个角度来看,如果电脑的实现类多了一个,比如量子计算机,那么我们需要将所有厂商对于它的实现类都写出来
-
导致上面扩展如此麻烦的原因是什么?是上面的继承关系违背了OOP 7大原则中的单一职责原则;因为上面的每一个电脑子类都同时实现了两个功能,电脑品牌+电脑类型;如果我们将类的粒度进一步切分,使得品牌和电脑类型两个类平行,扩展的事情就能够变得简单起来
-
分析:上图场景有两个变化的维度 —— 电脑品牌+电脑的类型
2.代码实现
-
就通过桥接模式完成上面的改造需求,将电脑类型和电脑品牌分离成两个接口
-
首先我们应该抽象出品牌接口,具体的厂商就去首先这个接口即可
public interface Brand { //厂商品牌接口 void info();//打印厂商名称 } public class Apple implements Brand{ //苹果品牌 @Override public void info() { System.out.print("苹果"); } } public class Lenovo implements Brand{ //联想品牌 @Override public void info() { System.out.print("联想"); } }
-
编写一个电脑类型类,我们的电脑类型类应该和品牌产生关系(否则我们如何将品牌和电脑类型联系起来?这里产生联系使用推荐的组合,而不使用继承),所以我们不应该将电脑类型类设计为接口,可以将其设计为抽象类,这样这个类就不能直接使用,必须被继承之后才能使用;
-
这个类的作用除了定义电脑实现类的约束,还实现了将厂商类组合进来,并被电脑类型类继承的作用,所以在new电脑类型类的时候,我们就需要指定这个类是哪一个厂商的这个类型的电脑
public abstract class Computer { //抽象的电脑类型类 //厂商品牌 protected Brand brand; public Computer(Brand brand) { //通过构造初始化该电脑类型类的厂商 this.brand = brand; } public void info(){ //调用厂商类的信息输出厂商/品牌的名称 brand.info(); } }
-
两种电脑类型类
class Desktop extends Computer{ //台式机 public Desktop(Brand brand) { //构造,需要传入电脑厂商 super(brand); } @Override public void info() { //输出电脑厂商+电脑类型 super.info(); System.out.println("台式机"); } } class Laptop extends Computer{ //笔记本 public Laptop(Brand brand) { //构造,需要传入电脑厂商 super(brand); } @Override public void info() { super.info(); System.out.println("笔记本"); } }
-
注意:我们只写了两个电脑类型,但是它们都继承了抽象的电脑类,所以每一个电脑类型类调用super.info()就会输出厂商品牌,然后再紧接着输出该电脑类型即可实现"品牌和电脑类型"的随意组装
-
测试
public class Test { public static void main(String[] args) { //苹果笔记本 Laptop laptop = new Laptop(new Apple()); //联想台式机 Desktop desktop = new Desktop(new Lenovo()); laptop.info(); desktop.info(); } }
-
上面的例子中,我们通过组合的方式将两个不相关的东西进行了桥接/使它们产生联系,这样做的好处在于,不管你是哪一个厂商的哪一种类型的电脑对象,在客户端使用的时候,我只需要按照我自己的要求new就可以获取到对应的产品;
-
而在原来的多继承中,我们需要将所有厂商对于所有类型的电脑的实现类都写好,在客户端使用的时候直接new,但是这样做的扩展性并不好;
-
现在我们将电脑的品牌和电脑的类型都抽象出来,并且通过**"组合"的方式**使得new出来的电脑类型对象自带了电脑厂商(我们需要手动传入厂商对象),这就大大减少了冗余的代码(每个厂商的每种电脑类型都去写一个是实现类),对于无论是厂商的扩展还是电脑类型的扩展都很便利,直接扩展即可,反正组合的是厂商的接口引用
3.小结
-
好处分析
- 桥接模式偶尔类似于多继承方案,但是多继承方案违背了类的单一职责原则,复用性比较差,类的个数也非常多,桥接模式是比多继承方案更好的解决方法,极大的减少了子类的个数,从而降低管理和维护的成本
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合开闭原则,就像一座桥,可以把两个变化的维度连接起来
-
劣势分析
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
- 桥接模式要求正确识别出系统中两个独立变化的维度(如果两个东西耦合性很强,且不能拆分,就不能使用桥接模式),因此其使用范围具有一定的局限性
-
适用情况
- 如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展
- 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用
-
常见的场景
-
Java语言通过Java虚拟机实现了平台的无关性
-
AWT中的Peer架构
-
DBC驱动程序也是桥接模式的应用之一
-
-
我的理解
- 多层继承结构,因为如果说多一个对象或者多一个类就会要改好多代码,所以就有桥接模式的诞生
- 以买电脑为例子去说明
- 我们现在有一个电脑,他又分为台式机,笔记本,然后每个又分品牌苹果,联想,现在呢我们要去加一个华为,桥接模式就是我们可以将品牌和对象分离,通过组合的方式去构造
- 第一我们抽象出品牌接口,由具体的品牌厂家去实现
- 第二我们去写一个电脑类的抽象类,将品牌作为属性加进去,并通过构造初始化该电脑类型类的厂,同时调用该品牌工厂的方法
- 构造不同计算机的类,因为计算机的品牌不同,所以构造对象的时候需要品牌,最后执行品牌和电脑的方法进行组合
- 测试,当我去调用的时候,就可以通过电脑类型 传入品牌参数来创建一个电脑并执行其方法
十,代理模式
为什么会学习代理模式?因为这就是SpringAOP的底层!【SpringAOP和SpringMVC】
代理模式分类:
- 静态代理
- 动态代理
1,静态代理
代码步骤:
-
接口
//租房 public interface rent { public void rent(); }
-
真实角色
//房东 public class Host implements rent{ public void rent() { System.out.println("房东要出租房子"); } }
-
代理角色
public class Proxy implements rent{ private Host host; public Proxy(){ } public Proxy(Host host){ this.host = host; } public void rent() { host.rent(); } //看房 public void seeHome(){ System.out.println("中介带你看房"); } //签合同 public void hetong(){ System.out.println("中介签合同"); } //收中介费 public void fare(){ System.out.println("中介收钱"); } }
-
客户端访问代理角色
public class Client { public static void main(String[] args) { //房东要去组房子 Host host = new Host(); //代理,中介代理房东去租房子,但是代理一般又有自己的附属操作 Proxy proxy = new Proxy(host); //你呢不用面对房东,直接找中介租房子即可 proxy.rent(); } }
代理模式的好处:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共业务就交给了代理角色!实现了业务的分工!
- 公共业务发生扩展的时候,方便集中管理!
缺点:
一个真实角色就会产生一个代理角色代码量会翻倍~开发小路降低
2,动态代理
- 动态代理和静态代理角色一样
- 动态代理类是动态生成的,不是我们直接写好的!
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口的—JDK动态代理【使用】
- 基于类:cglib
- java字节码实现:javasist
需要了解两个类:Proxy:代理,InvocationHandler:调用处理程序
动态代理的好处:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共业务就交给了代理角色!实现了业务的分工!
- 公共业务发生扩展的时候,方便集中管理!
- 一个动态代理代理的是一个接口,一般就是对应的一类业务
- 一个动态代理可以代理多个类,只要是实现了同一个接口即可
//等会我们会用这个类,动态生成代理类!
public class ProxyInvocationHandler implements InvocationHandler {
//被代理的接口
private Object target;
public void setTarget(Object target) {
this.target = target;
}
//生成得到代理类
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
//处理代理实例,并返回结果
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log(method.getName());
//动态代理的本质,就是使用反射及时实现
Object result = method.invoke(target, args);
return result;
}
public void log(String msg){
System.out.println("执行了"+msg+"方法");
}
}
public class Client {
public static void main(String[] args) {
//真实角色
UserServiceImpl userService = new UserServiceImpl();
//代理角色
ProxyInvocationHandler pih = new ProxyInvocationHandler();
pih.setTarget(userService);//设置代理角色
//动态生成代理类
UserService proxy = (UserService) pih.getProxy();
proxy.add();
}
}
package 动态代理;
/**
* @title: ITeacherDao
* @Author Tan
* @Date: 2022/4/22 21:41
* @Version 1.0
*/
public interface ITeacherDao {
void teach(); // 授课方法
void sayHello(String name);
}
package 动态代理;
/**
* @version v1.0
* @ProjectName: 设计模式
* @ClassName: TeacherDao
* @Description: 请描述该类的功能
* @Author: ming
* @Date: 2022/4/22 21:42
*/
public class TeacherDao implements ITeacherDao{
@Override
public void teach() {
// TODO Auto-generated method stub
System.out.println(" 老师授课中.... ");
}
@Override
public void sayHello(String name) {
// TODO Auto-generated method stub
System.out.println("hello " + name);
}
}
动态代理的理解:动态代理相对于静态代理而言多了,多了两个类Proxy:代理,InvocationHandler:调用处理程序,而且他的动态代理类是自动生成的。首先我们要知道ProxyInvocationHandler类,他的作用是动态生成代理类。它继承于InvocationHandler,表示需要进行程序调用处理。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。在invoke方法中执行被代理对象target的相应方法。在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。这也是Spring中的AOP实现的主要原理,这里还涉及到一个很重要的关于java反射方面的基础知识。
-
动态代理代理的是一个接口(其实就是一个被代理的对象),所以写一个被代理的接口
//被代理的接口 private Object target;
-
通过set注入,把接口带进去
public void setTarget(Object target) { this.target = target; }
-
处理代理的实例并返回结果集
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log(method.getName()); //动态代理的本质,就是使用反射及时实现 Object result = method.invoke(target, args); return result; } public void log(String msg){ System.out.println("执行了"+msg+"方法"); }
-
生成得到代理类,是一个代码的优化将生成得到的代理类给放在生成代理类的这个类中,方便后面测试时使用
//生成得到代理类 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this); }
-
测试类
public class Client { public static void main(String[] args) { // TODO Auto-generated method stub //创建目标对象 ITeacherDao target = new TeacherDao(); //给目标对象,创建代理对象, 可以转成 ITeacherDao ITeacherDao proxyInstance = (ITeacherDao)new ProxyInvocationHandler(target).getProxyInstance(); // proxyInstance=class com.sun.proxy.$Proxy0 内存中动态生成了代理对象 System.out.println("proxyInstance=" + proxyInstance.getClass()); //通过代理对象,调用目标对象的方法 //proxyInstance.teach(); proxyInstance.sayHello(" tom "); } }
十一,小结
1,创建型模式
单例模式
- 饿汉式模型
- 就是直接去new一个对象,必然会造成资源浪费
- 懒汉式模型
- 就是为了解决饿汉式模型的缺陷,所以才诞生的
- DCL懒汉式单例
- 因为懒汉式不支持多线程,所以才诞生了这个双重锁定的懒汉式
- 双重锁,第一层锁是判断他是否被实例化,防止随意加锁导致浪费资源。第二层锁则是为了保证对象只可以实例化一次
- 同时要在定义单例对象时加上volatile关键字,防止指令重排
- 枚举的方法实现单例
- 我们可以用反射来实现单例,但是这样子就破坏了权限问题,所以我们就可以用枚举来限制他
抽象工厂模式
-
抽象工厂模式就是创建了一个超级工厂,让不同的产品去围绕这个工厂去创建工厂,并实现其功能
工厂模式
作用是实现了创建者和调用者的分离
简单/静态 工厂模式
-
根据对象的需求,我们去创建一个工厂,我们需要什么对象,去告诉工厂,让工厂给我们一个什么。
-
但是我们发现如果增加一个对象就需要改变工厂的逻辑代码,这样子显然是不符合预期的,所以有了工厂方法
工厂方法
-
分析之前需要改工厂方法是因为我们只有一个工厂,如果我们要是有一个总工厂,不同的对象拥有自己的工厂不就可以了嘛
-
但是这样的话就有一个问题是如果增加一个产品,那么就会多很多代码
-
同时就是用来生产同一等级结构中的固定产品(支持增加任意产品)
建造者模式
-
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示
-
我们可以通过静态内部类方式实现零件无序装配构造,这种方式使用更加灵活,更符合定义。内部有复杂对象的默认实现,使用时可以根据用户需求自由定义更改内容,并且无需改变具体的构造方式,就可以生产出不同复杂产品。原因就是我们有了具体的属性(即就是有了产品),我们就不需要去指挥类
-
我们在产品类中定义了的属性有默认值,生产产品的抽象类去定义的四个步骤(即是个抽象方法),再由工人类去具体的实现返回产品
-
使用的场景
- 需要生成的产品对象有复杂的内部结构,这些产品对象具备共性
- 适合于一个具有较多的零件(属性)的产品(对象)的创建过程
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品
-
建造者模式的优点
- 产品的建造和表示分离,实现了解耦,使用建造者模式可以使客户端不必知道产品内部组成的细节
- 将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰
- 具体的建造者类之间是相互独立的,这有利于系统的扩展,增加新的具体建造者无需修改原有类库的代码,符合“开闭原则“
-
建造者模式的缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似;如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制
- 如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大
原型模式
-
诞生的原因:有些对象他比较复杂,我们创建这个对象比较难,所以想直接去通过复制这个对象
-
但是这样呢,如果我们改变这个对象中的属性,被克隆对象的属性也会改变,即他们的属性指向的是同一个地址,这样的克隆叫浅克隆
-
深克隆就是将其属性也通过调用clone的方法来实现
@Override protected Object clone() throws CloneNotSupportedException { //覆写Object的clone() Object obj = super.clone();//obj就是返回的克隆对象 Prototype prototype = (Prototype) obj;//强转z类型设置时间属性 prototype.setDate((Date) this.date.clone());//设置克隆对象的date属性为当前进行克隆对象的原型对象的date成员属性的克隆 return obj;//返回克隆好的对象 }
2,结构型模式
适配器模式
-
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作
-
但是上述方法只能一个适配器实现一个类,不符合我们的想法
-
所以在适配器实现类中使用组合的方式,即把要实现的功能类通过组合的方式放入适配器实现类中,在方法体内通过调用这个对象来使用这个方法
-
对象适配器优点【推荐使用,适配器和功能类进行组合】
- 一个对象适配器可以把多个不同的适配者适配到同一个目标
- 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也可通过该适配器进行适配
-
类适配器缺点【有局限性(不通用),适配器继承功能类】
- 对于Java、C#等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者
- 在Java、C#等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性
-
适用场景
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码
- 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作
- 适配器在spring全家桶中使用的地方很多,比如springMVC中配置的处理器适配器HandleAdapter
桥接模式
-
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interfce)模式
-
好处分析
- 桥接模式偶尔类似于多继承方案,但是多继承方案违背了类的单一职责原则,复用性比较差,类的个数也非常多,桥接模式是比多继承方案更好的解决方法,极大的减少了子类的个数,从而降低管理和维护的成本
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合开闭原则,就像一座桥,可以把两个变化的维度连接起来
-
劣势分析
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
- 桥接模式要求正确识别出系统中两个独立变化的维度(如果两个东西耦合性很强,且不能拆分,就不能使用桥接模式),因此其使用范围具有一定的局限性
-
适用情况
- 如果一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合
- 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展
- 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用