前言:
掌握常用的设计模式(单例模式、工厂模式),了解其他设计模式。
一、学习设计模式的真正目的:
编程时,有意识地现象接口编程,多用封装、多态、继承、组合等OOP思想,而不仅仅是死记几种设计模式。
二、常用的设计模式分类
创建型(创建一个对象):单例模式、工厂模式、抽象工厂模式
结构型(将几个对象组织成一个结构):桥接模式、外观模式、代理模式
行为型(多个对象间的通信):观察者模式、 策略模式
三、设计模式的定义
被反复使用的,代码设计经验的总结。
四、设计模式的原则
总结起来就是,多用接口/抽象类,从而增加代码的可扩展性(减少修改代码)。降低模块间的依赖和联系。
体现了OOP的模块化、可扩展性等特征。
五、设计模式的六大原则
1、开闭原则:对扩展开放,对修改关闭。
在程序需要进行扩展时,不去修改原有的代码。概括起来就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类。
2、里氏代换原则(Liskov Substitution Principle LSP):面向对象设计的基本原则之一。
里氏代换原则中说,任何基类出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开闭原则”的补充。实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3、依赖倒转原则:开闭原则的基础
对接口编程,依赖于抽象而不依赖于具体
4、接口隔离原则:使用多个隔离的接口比使用单个接口要好
还是一个降低类之间耦合度的意思。其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便 。
5、迪米特法则(最少知道原则)
一个实体应该尽可能少的与其他实体之间发生相互作用,使得系统功能模块相互独立。
6、合成复用原则
尽量使用合成/聚合的方式,而不是使用继承。
单例模式:
一、概述
0、概念:在一个类的外部有且只有该类的一个对象,所有的请求都用这个对象来处理。
实现: 构造方法私有化
通过静态方法返回该类对象
私有化该类类型的静态变量
1、优点:
某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
省去了new操作符,降低了系统内存的使用频率,减轻GC压力
有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了,所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
2、缺点:没有接口、不能继承,与单一职责原则冲突,一个类应该之关心内部逻辑,而不关心外部怎么样来实例化
二、模式
1、饿汉模式:静态变量先创建,拿空间换时间。类加载的时候立即实例化对象,仅实例化一次,线程安全。
优点:仅实例化一次,线程是安全的,获取实例的速度快。
缺点:类加载的时候立即实例化对象,可能实例化的对象不被使用,造成内存浪费。
2、懒汉模式:使用的时候先判断,在创建变量。拿时间换空间,不能保证实例的唯一性。
优点:获取实例的时候才进行实例初始化,节省了系统资源
缺点:(1)如果获取实例时,初始化的工作量较多,加载速度会变慢,影响系统性能。
(2)每次获取实例都要进行非空检查,系统开销大
(3)非线程安全。当多个线程同时getInstance时,可能singleton实例化未完成,singleton==null 判断均为true,造成对象重复实例化。
3、Holder方式(广泛使用):声明类的时候,成员变量中不声明实例变量,而是放到内部静态类中。
优点:内部类只有在外部类被调用的时候才加载,从而实现了延迟加载。线程安全且不用加锁。
4、枚举:本质上跟饿汉模式没有任何区别,只是采用了Enumeration实现的更巧妙了。
优点:仅实例化一次,线程安全,获取实例速度快。
缺点:类加载的时候立即实例化对象,可能实例化的对象不被使用,造成内存浪费。
5、枚举和懒汉模式相结合
优点:线程安全且不用加锁,实现了懒加载
缺点:仍然需要实力非空判断,耗费一定的资源
6、双重检查锁DCL+volatile
优点:线程安全。进行双重检查,保证只在实例未初始化前进行同步,效率高
线程安全:饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以线程是天生安全的。懒汉式线程不安全,并发环境下很可能出现多个singleton实例。
工厂模式:
一、定义及使用场合
假如现在需要创建几个对象,且这几个对象有共同的特征,则不需要具体创建各个对象,而是创建对象工厂类即可。
一般常用静态工厂模式。
工厂模式主要是为创建对象提供了接口。
应用场景如下:
在编码时不能预见需要创建哪种类的实例。
系统不应依赖于产品类实例如何被创建、组合和表达的细节。
应用实例:
1、你需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
2、Hibernate换数据库只需换方言和驱动即可。
二、优缺点
优点:一个调用者想要创建一个对象,只要知道其名称就可以了。
扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以了。
屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增长,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。
注:作为一种创建类模式,在任何需要生成复杂类对象的地方,都需要使用工厂方法模式。有一点需要注意的地方就是,使用工厂模式,而简单对象,特别是只需要通过new就可以完成创建对象,无需使用工厂模式。如果使用工厂模式就需要引入一个工厂类,会增加系统的复杂度。
三、实现步骤
// 1、创建接口
public interface Shape { void draw(); }// 2、创建实现接口的实体类
public class Square implements Shape{
@Override
public void draw() {
System.out.println("Inside Square::draw() method.");
}
}public class Circle implements Shape{
@Override
public void draw() {
System.out.println("Inside Circle::draw() method.");
}
}// 3、创建一个工厂,生成基于给定信息的实体类的对象
public class ShapeFactory {
public Shape getShape(String shapeType) {
if(shapeType == null) {
return null;
}
if("CIRCLE".equalsIgnoreCase(shapeType)) {
return new Circle();
}else if("SQUARE".equalsIgnoreCase(shapeType)) {
return new Square();
}else if("RECTANGLE".equalsIgnoreCase(shapeType)) {
return new Rectangle(); } return null;
}
}// 4、使用该工厂,通过传递类型信息来获取实体类的对象
public class FactoryPatternDemo {
public static void main(String[] args) {
ShapeFactory shape = new ShapeFactory();
Shape shape2 = shape.getShape("CIRCLE");
shape2.draw();
Shape shape3 = shape.getShape("SQUARE");
shape3.draw();
}
}
适配器模式:
一、概念
将一个类的接口转换成客户期望的另一个接口。适配器模式使得原本由于接口不匹配的而不能一起工作的那些类可以一起工作。。实例:插座转换器。
适配器模式是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,他结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入的独立的或不兼容的接口功能。
1、主要解决的问题:在软件系统中,常常要将一些“现存的对象”放到新的环境中,而新环境要求的接口是现对象不能满足的。
2、何时使用:系统需要使用现有的类,而此类的接口不符合系统的要求。
想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
通过接口转换,将一个类插入另一个类系中(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
3、优缺点:
优点:可以让任何两个没有关联的类一起运行;提高了类的复用;增加了类的透明度;灵活性好。
缺点:过多的使用适配器,会让系统非常凌乱,不易整体进行把握。
由于java至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
二、类适配器
1、通过一个适配器,将用到的功能通过extends、implements使它们全在适配器中实现或被调用。适配器继承并实现目标接口和原始类。
核心思想就是:有一个Source类,拥有一个方法,待适配,目标接口是Targetable,通过Adapter类,将Source的功能扩展到Targetable里。
public class Source {
public void method1() {
System.out.println("this is original method!");
}
}
public interface Targetable {
/**
* 与原类中方法相同
*/
public void method1();
/**
* 新类的方法
*/
public void method2();
}
public class Adapter extends Source implements Targetable{
@Override
public void method2() {
System.out.println("this is the targetable method!");
}
}
//这样Targetable的实现接口就有了Source类的功能
public class AdapterTest {
public static void main(String[] args) {
Targetable tag = new Adapter();
tag.method1();
tag.method2();
}
}
三、对象适配器
1、通过组合来实现适配器的功能。在适配器中,通过组合来引用被适配者,并用构造方法将参数传进来,然后实现接口的方法,,只需实现接口,不必继承类。
2、优缺点:
优点:同一适配器可以把适配者和它的子类都适配到目标接口
缺点:与类适配器相比,想要置换适配者类的方法不容易。
3、步骤:
只改了Adapter类。
public class Wrapper implements Targetable{
public Source source;
public Wrapper(Source source) {
super();
this.source = source;
}
@Override
public void method1() {
source.method1();
}
@Override
public void method2() {
System.out.println("this is targetable method!");
}
}
四、接口适配器
1、通过抽象类来实现适配。
当一个接口有n个方法,而我们只需要其中的1,2种,这时可以定义一个抽象类来实现这个接口,这个抽象类就是适配器。然后定义一个实现类继承这个适配器,并实现所需的方法。
3、首先定义Sourceable接口,在其中写上所需要的所有抽象方法,一个Wrapper抽象类,实现Sourceable接口及其中的所有方法,默认实现就行,然后定义一个SourceSub1类,实现继承Wrapper,随便实现哪一个方法都可以。抽象类的作用就是可以让后面继承它的实现类只实现其所需要的方法,不必每一个实现类都实现所有接口的方法,节约了系统资源的消耗。