Java之设计模式
看了设计模式,感觉自己很多不理解什么意思,通过看博客别人写的理解,总结了一下,方便查阅。
一、设计模式六大原则
1、单一职责原则:
定义 : 应该有且只有一个原因引起类的变化。
注意 : 这里的类不光指类,也适用于方法和接口,比如我们常说的一个方法实现一个功能。
2、开放封闭原则:
定义:类、模块、函数等应该是可以扩展的,但不可修改。
意思就是不要改你以前写的代码,你应该加一些代码去扩展原来的功能,来实现新的需求。
3、里氏替换原则:
定义:所有引用基类的地方必须透明的使用其子类的对象。
意思可以理解为:子类可以扩展父类的功能,但不能改变父类原有的功能。
使用规范 :
- 子类必须完全实现父类的方法,如果子类无法完全实现父类的方法,则建议断开父子继承关系,采用依赖 | 聚集 | 组合 等关系来代替。
- 子类可以有自己的个性。
- 覆盖或实现父类的方法时,输入参数可以被放大,比如父类中有一个方法的输入参数是 HashMap,子类的参数可以是 Map 类型,这样父类就可以被子类替换,如果反过来,则违背了里氏替换原则,所以子类中方法的 前置条件必须与父类的。
- 被覆写的方法的前置条件相同或者更宽松。
- 覆写或实现父类的方法时,输出结果可以被缩小,也就是说如果父类方法返回的类型 T,子类的相同方法(重载或覆写)的返回值类型 S,S 和 T 要么同类型,要么 S 是 T 的子类;跟上面的道理一样。
注意 : 采用里氏替换原则时,尽量避免子类的"个性",一旦子类有了"个性",子类和父类的关系就会变得不好调和。
4、依赖倒置原则:
定义:高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象 。
高层模块和低层模块比较好理解,每一个逻辑都是由原子逻辑组成的,不可分割的原子逻辑是低层模块,原子逻辑再组装就是高层模块;
抽象指的是接口或者抽象类,两者都不能直接实例化;
细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点是可以被实例化;
这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
5、迪米特原则:
定义:一个软件实体应当尽可能少地与其他实体发生相互作用。这个应该很好理解,就是降低各模块之间的耦合。
6、接口隔离原则:
定义:一个类对另一个类的依赖应该建立在最小的接口上。
这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
二、设计模式分类
总体来说设计模式分为三大类:
创建型模式
共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式
共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式
共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:
三、设计模式解析
1、创建型模式
工厂模式:
在工厂模式中一般存在以下三个类型的角色,分别是产品类,工厂类和客户类。产品类就是最终产出的产品;工厂类就是生产产品的工厂;客户类就是最终产品的需求者。
简单工厂模式(Simple Factory)(不是23种设计模式里面的):
简单工厂模式对上面提到的工厂模式中三个类型角色中的产品类进行了修改,把产品类进行了抽象,分成了抽象产品角色和具体产品角色,其他角色类不变。抽象产品角色一般是具体产品类需要继承的父类或者需要实现的接口,而具体产品角色就是工厂类中需要创建的产品实例。
产品类
abstract class Car { public Car() { } } public class BMW extends Car { public BMW(){ System.out.println("生产制造-->BMW"); } } public class Audi extends Car{ public Audi(){ System.out.println("生产制造-->Audi"); } }
工厂类
public class Factory { public static Car create(int type) { switch (type) { case 111: return new BMW(); case 222: return new Audi(); default: break; } return null; } }
客户类
public class CustomerTest { public static void main(String[] args) { Factory factory = new Factory(); Car bmw = factory.create(111); Car audi = factory.create(222); } }
最后显示结果:
生产制造-->BMW
生产制造-->Audi
注:简单工厂模式不能算是真正意义上的设计模式,但可以将客户程序从具体类解耦。
工厂方法模式(Factory Method)
工厂方法模式是在简单工厂模式又进行了一部分修改,在简单工厂模式的基础上把工厂类进行了抽象,分成了抽象工厂角色和具体工厂角色。抽象工厂角色是工厂方法模式中的核心部分,是必须由具体工厂角色进行继承或者实现的父类或者接口。具体工厂角色在继承或者实现抽象工厂角色后在自己的内部做具体的业务逻辑。
产品类
abstract class Car { public Car() { } } public class BMW extends Car { public BMW(){ System.out.println("生产制造-->BMW"); } } public class Audi extends Car{ public Audi(){ System.out.println("生产制造-->Audi"); } }
工厂方法模式中的抽象产品角色和具体产品角色
工厂类
public interface IFactory { Car createCar(); } public class FactoryAudi implements IFactory { @Override public Audi createCar() { return new Audi(); } } public class FactoryBMW implements IFactory { @Override public BMW createCar() { return new BMW(); } }
工厂方法模式中的抽象工厂角色和具体工厂角色
客户类
public class CustomerTest { public static void main(String[] args) { FactoryBMW factoryBMW=new FactoryBMW(); BMW bmw=factoryBMW.createCar(); FactoryAudi factoryAudi=new FactoryAudi(); Audi audi=factoryAudi.createCar(); } }
最后显示结果:
生产制造-->BMW
生产制造-->Audi
抽象工厂模式(Abstract Factory)
使对象的创建被实现在工厂接口所暴露出来的方法中。
产品类
public interface IItem1 { void show(); } public interface IItem2 { void show(); } public class Item1 implements IItem1 { @Override public void show() { System.out.println("打印第一项"); } } public class Item2 implements IItem2 { @Override public void show() { System.out.println("打印第二项"); } }
工厂类
public interface IFactory { IItem1 createItem1(); IItem2 createItem2();//接口作为类型返回,返回的是实现了接口的对象 } public class Factory implements IFactory { @Override public Item1 createItem1() { return new Item1(); } @Override public Item2 createItem2() { return new Item2(); } }
接口作为类型返回,返回的是实现了接口的对象,这个我不是很熟悉,重点学习。
客户类
public class CustomerTest { public static void main(String[] args) { Factory factory =new Factory(); factory.createItem1().show(); factory.createItem2().show(); } }
最后显示结果:
打印第一项
打印第二项
工厂模式可以帮助我们针对抽象/接口编程,而不是针对具体类编程,在不同的场景下按具体情况来使用。
单例模式
单例模式有多种,有些不适合多线程的时候使用,有些效率可能相对不是很好,在这里只记录推荐使用的3种单例模式。
1、双重检查
public class Singleton { private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
优点:线程安全;延迟加载;效率较高。
2、静态内部类
public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
3、枚举
public enum Singleton { INSTANCE; public void whateverMethod() { } }
借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。
优点:系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。
适用场合
- 需要频繁的进行创建和销毁的对象;
- 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
- 工具类对象;
- 频繁访问数据库或文件的对象
但是Android官网不建议使用enums,占用内存多。
建造者模式
建造者模式:是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到的。
建造者模式通常包括下面几个角色:
1、Builder:给出一个抽象接口,以规范产品对象的各个组成成分的建造。这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。
2、ConcreteBuilder:实现Builder接口,针对不同的商业逻辑,具体化复杂对象的各部分的创建。 在建造过程完成后,提供产品的实例。
3、Director:调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
4、Product:要创建的复杂对象。
下面通过看别人的一个例子来理解建造者模式,先创建一个人的类。
public class Human { private String head; private String body; private String hand; private String foot; public String getHead() { return head; } public void setHead(String head) { this.head = head; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public String getHand() { return hand; } public void setHand(String hand) { this.hand = hand; } public String getFoot() { return foot; } public void setFoot(String foot) { this.foot = foot; } }
这个就是人的类了,取名为human。只要是人都是这个类出来的。
那我们想要造人的话,头,身体,手,脚,那都得有吧。
所以我们先写一个接口。一个定义了造人标准的接口。
public interface IBuildHuman { public void buildHead(); public void buildBody(); public void buildHand(); public void buildFoot(); public Human createHuman(); }
接口定义好了。有各种造部位的方法,最后给个createHuman方法给你返回一个我们想要造的人。
现在,人的类和造人的标准我们都定义好了。那我们可以先造一个人出来,先造一个高智商的人吧。
public class SmartManBuilder implements IBuildHuman { Human human; public SmartManBuilder(){ human = new Human(); } @Override public void buildHead() { human.setHead("智商180的头脑"); } @Override public void buildBody() { human.setBody("新的身体"); } @Override public void buildHand() { human.setHand("新的手"); } @Override public void buildFoot() { human.setFoot("新的脚"); } @Override public Human createHuman() { return human; } }
这个高智商的造人过程自然是实现了IBuildHuman这个造人标准的接口了。然后就是set方法,最后返回造好的human。
我们已经完成了建造的过程。那就这么简单的建造过程,还搞了一个建造模式吗?非也。接下来,就是介绍建造者模式的精髓,那就是director。这个director呢,就是来执行我们刚才的造人动作的。没错,精髓就是我们刚才的造人动作。我们先看代码:
public class Director { public Human createHumanByDirecotr(IBuildHuman bh){ bh.buildBody(); bh.buildFoot(); bh.buildHand(); bh.buildHead(); return bh.createHuman(); } }
最后,来看看director是如何发挥作用的
public class BuilderTest { public static void main(String[] args){ Director director = new Director(); Human human = director.createHumanByDirecotr(new SmartManBuilder()); System.out.println(human.getHead()); System.out.println(human.getBody()); System.out.println(human.getHand()); System.out.println(human.getFoot()); } }
输出结果为:
智商180的头脑
新的身体
新的手
新的脚
createHumanByDirecotr这个方法带的参数就是我们高智商人的那个类。那我们想造一个运动员,就可以像高智商人那样建好类,然后传进来就可以了!
建造者模式就是这样。那到最后,我们必须要思考一下为什么这么做?我们纵观全部代码,其实我们不难发现,在最后的BuilderTest类中,我们其实根本就不会知道具体是怎么造人的,因为这个过程让director给代劳了。然后,我们的SmartManBuilder类中才是真正的造人方法。director其实只是执行了这个过程。这样子,达到了分离模块的功能。造人的过程,启动造人的过程,和最后选择哪种人来造。都分的清清楚楚。就有了一些模块化的感觉,这样维护和扩展都是很方便的。所以设计模式的精髓还是那几个原则。感觉原则才是法则,而这么多模式就是具体的招式。
Builder模式
Builder模式介绍
Builder模式是一种一步一步创建一个复杂对象的设计模式,我认为这种设计模式的精髓就主要有两点:其一,用户使用简单,并且可以在不需要知道内部构建细节的情况下,就可以构建出复杂的对象模型;其二,对于设计者来说,这是一个解耦的过程,这种设计模式可以将构建的过程和具体的表示分离开来。
Builder模式的使用场景
1.相同的方法,不同的执行顺序,产生不同的时间结果时。
2.多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不同时。
3.产品类非常复杂,或者产品类中的调用顺序不同产生了不同的作用,这个时候用建造者模式非常适合。
4.当初始化一个对象特别复杂,如参数多,切很多参数都具有默认值时。
Builder模式分析
我们为什么要把代码的构建和表示分离开来?1.增加代码的可读性;2.方便维护。举一个例子,安卓中非常常用的AlertDialog就是Builder模式,我们知道,一个Dialog可以有非常多的属性和参数,例如title,icon,message, positiveButton,negativeButton等十几个参数,如果这些参数都放在构造函数里,那么这个方法将会有很多参数,为了完成这个函数,需要一次性准备好所有的参数,很容易写着写着就乱了。同时为了适配,还需要写很多重载的构造函数,类会很乱。而如果使用builder模式,不需要一次性准备好所有的参数,完全可以一个一个准备,代码易懂,方便非常多。
举例说明
public class Product { private int id; private String name; private int type; private float price; public Product(Builder builder) { this.id=builder.id; this.name=builder.name; this.type=builder.type; this.price=builder.price; } public static class Builder{ private int id; private String name; private int type; private float price; public Builder id(int id) { this.id = id; return this; } public Builder name(String name){ this.name = name; return this; } public Builder type(int type) { this.type = type; return this; } public Builder price(float price){ this.price = price; return this; } public Product build(){ return new Product(this); } } @Override public String toString() { return "Product{" + "id=" + id + ", name='" + name + '\'' + ", type=" + type + ", price=" + price + '}'; } }
创建对象并测试
Product p3 = new Product.Builder() .id(10) .name("phone") .price(100) .type(1) .build(); System.out.println(p3.toString());
打印的结果为:
Product{id=10, name='phone', type=1, price=100.0}
原型模式
所谓原型模式就是用原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象。
讲到原型模式了,我们就不得不区分两个概念:深拷贝、浅拷贝。
浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。
深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。
对于深拷贝和浅拷贝的深入了解,可以参考:渐析java的浅拷贝和深拷贝、深拷贝与浅拷贝详解
原型模式主要包含如下三个角色:
Prototype:抽象原型类。声明克隆自身的接口。
ConcretePrototype:具体原型类。实现克隆的具体操作。
Client:客户类。让一个原型克隆自身,从而获得一个新的对象。
java中可以直接使用clone()方法来复制一个对象。但是需要实现clone的Java类必须要实现一个接口:Cloneable.该接口表示该类能够复制且具体复制的能力,如果不实现该接口而直接调用clone()方法会抛出CloneNotSupportedException异常。
public class PrototypeDemo implements Cloneable{ public Object clone(){ Object object = null; try { object = super.clone(); } catch (CloneNotSupportedException exception) { System.err.println("Not support cloneable"); } return object; } …… }
Java中任何实现了Cloneable接口的类都可以通过调用clone()方法来复制一份自身然后传给调用者。一般而言,clone()方法满足:
(1) 对任何的对象x,都有x.clone() !=x,即克隆对象与原对象不是同一个对象。
(2) 对任何的对象x,都有x.clone().getClass()==x.getClass(),即克隆对象与原对象的类型一样。
(3) 如果对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。
下面我们来模拟复印简历,建一个Resume类实现Cloneable接口
public class Resume implements Cloneable { private String name; private String birthday; private String sex; private String school; private String timeArea; private String company; /** * 构造函数:初始化简历赋值姓名 */ public Resume(String name){ this.name = name; } /** * @desc 设定个人基本信息 * @param birthday 生日 * @param sex 性别 * @param school 毕业学校 * @return void */ public void setPersonInfo(String birthday,String sex,String school){ this.birthday = birthday; this.sex = sex; this.school = school; } /** * @desc 设定工作经历 * @param timeArea 工作年限 * @param company 所在公司 * @return void */ public void setWorkExperience(String timeArea,String company){ this.timeArea = timeArea; this.company = company; } /** * 克隆该实例 */ public Object clone(){ Resume resume = null; try { resume = (Resume) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return resume; } public void display(){ System.out.println("姓名:" + name); System.out.println("生日:" + birthday + ",性别:" + sex + ",毕业学校:" + school); System.out.println("工作年限:" + timeArea + ",公司:" + company); } }
测试类:Test.java
public class Test { public static void main(String[] args) { //原型A对象 Resume a = new Resume("小思南"); a.setPersonInfo("3.22", "男", "XX大学"); a.setWorkExperience("2018.11.08", "XX科技有限公司"); //克隆B对象 Resume b = (Resume) a.clone(); //输出A和B对象 System.out.println("----------------A--------------"); a.display(); System.out.println("----------------B--------------"); b.display(); /* * 测试A==B? * 对任何的对象x,都有x.clone() !=x,即克隆对象与原对象不是同一个对象 */ System.out.print("A==B?"); System.out.println( a == b); /* * 对任何的对象x,都有x.clone().getClass()==x.getClass(),即克隆对象与原对象的类型一样。 */ System.out.print("A.getClass()==B.getClass()?"); System.out.println(a.getClass() == b.getClass()); } }
运行结果:
----------------A-------------- 姓名:小思南 生日:3.22,性别:男,毕业学校:XX大学 工作年限:2018.11.08,公司:XX科技有限公司 ----------------B-------------- 姓名:小思南 生日:3.22,性别:男,毕业学校:XX大学 工作年限:2018.11.08,公司:XX科技有限公司 A==B?false A.getClass()==B.getClass()?true
优点:
1、如果创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率。
2、可以使用深克隆保持对象的状态。
3、原型模式提供了简化的创建结构。
缺点:
1、在实现深克隆的时候可能需要比较复杂的代码。
2、需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
使用场景:
1、如果创建新对象成本较大,我们可以利用已有的对象进行复制来获得。
2、如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用原型模式配合备忘录模式来应用。相反,如果对象的状态变化很大,或者对象占用的内存很大,那么采用状态模式会比原型模式更好。
3、需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
2、结构型模式
适配器模式
原创作品,可以转载,但是请标注出处地址http://www.cnblogs.com/V1haoge/p/6479118.html
适配器就是一种适配中间件,它存在于不匹配的二者之间,用于连接二者,将不匹配变得匹配,简单点理解就是平常所见的转接头,转换器之类的存在。
适配器模式有三种:类适配器、对象适配器、接口适配器
前二者在实现上有些许区别,作用一样,第三个接口适配器差别较大。
1、类适配器模式:
原理:通过继承来实现适配器功能。
我们以ps2与usb的转接为例
ps2接口:Ps2
public interface Ps2 { void isPs2(); }
USB接口:Usb
public interface Usb { void isUsb(); }
USB接口实现类:Usber
public class Usber implements Usb { @Override public void isUsb() { System.out.println("USB口"); } }
适配器:Adapter
public class Adapter extends Usber implements Ps2{ @Override public void isPs2() { isUsb(); } }
测试方法:Clienter
public class Clienter { public static void main(String[] args) { Ps2 p = new Adapter(); p.isPs2(); } }
显示结果:
USB口
2、对象适配器模式
原理:通过组合来实现适配器功能。
我们仍然以ps2与usb的转接为例: 其中ps2接口、USB接口、USB接口实现类都和类适配器模式一样,就不在贴出。
适配器:Adapter
public class Adapter implements Ps2 { private Usb usb; public Adapter(Usb usb) { this.usb = usb; } @Override public void isPs2() { usb.isUsb(); } }
测试方法:Clienter
public class Clienter { public static void main(String[] args) { Ps2 p = new Adapter(new Usber()); p.isPs2(); } }
显示结果:
USB口
3、接口适配器模式
原理:通过抽象类来实现适配,这种适配稍别于上面所述的适配。
目标接口:Obj
public interface Obj { void a(); void b(); void c(); void d(); void e(); void f(); }
适配器:Adapter
public abstract class Adapter implements Obj { public void a() { } public void b() { } public void c() { } public void d() { } public void e() { } public void f() { } }
实现类:Ashili
public class Ashili extends Adapter { public void a() { System.out.println("实现A方法被调用"); } public void d() { System.out.println("实现d方法被调用"); } }
测试类:Clienter
public class Clienter { public static void main(String[] args) { Obj a = new Ashili(); a.a(); a.d(); } }
显示结果:
实现a方法被调用
实现d方法被调用
4、适配器模式应用场景
类适配器与对象适配器的使用场景一致,仅仅是实现手段稍有区别,二者主要用于如下场景:
(1)想要使用一个已经存在的类,但是它却不符合现有的接口规范,导致无法直接去访问,这时创建一个适配器就能间接去访问这个类中的方法。
(2)我们有一个类,想将其设计为可重用的类(可被多处访问),我们可以创建适配器来将这个类来适配其他没有提供合适接口的类。
以上两个场景其实就是从两个角度来描述一类问题,那就是要访问的方法不在合适的接口里,一个从接口出发(被访问),一个从访问出发(主动访问)。
接口适配器使用场景:
(1)想要使用接口中的某个或某些方法,但是接口中有太多方法,我们要使用时必须实现接口并实现其中的所有方法,可以使用抽象类来实现接口,并不对方法进行实现(仅置空),然后我们再继承这个抽象类来通过重写想用的方法的方式来实现。这个抽象类就是适配器。