设计模式一:设计原则与创建型设计模式
最近学习《Java设计模式》(刘伟版),对设计模式有了新的认识,现在将自己的学习笔记整理一下,加深印象,第一篇记录了设计原则与六种设计模式。
如有错误,欢迎指正!
设计原则
一、单一职责原则
单一职责原则是指一个类只负责一个功能领域的相应职责,是实现高内聚低耦合的指导方针。
如上图所示的CustomerDataChart类中,getConnection用于获取数据库连接,findCustomers用于获取用户列表,createChart跟displayChart用于创建、展示图标。这样一个CustomerDataChart类中包含了多个类型的功能,不符合单一职责原则。
按照单一职责原则重构后,可以实现三个类:DBUtil负责数据库的连接,包含getConnection方法;CustomerDAO负责用户的增删改查,包含findCustomers方法;CustomerDataChart负责图表的生成和展示,包含createChart与displayChart方法。
二、开闭原则
开闭原则是指一个软件实体应该对扩展开放,对修改关闭。即软件实体应该在不改变源代码的前提下进行扩展。ps:软件中xml或properties等配置文件是纯文本文件,无需编译,所以关于配置文件的修改是符合开闭原则的。
开闭原则的关键是抽象化,即通过接口或者抽象类定义好函数,通过子类实现具体的功能,这样在后续的维护扩展时,只需要添加扩展的子类并实现,不需要修改源代码,从而符合开闭原则。
上图是符合开闭原则的一种设计思路,定义一个抽象的AbstractChart类,并定义display方法;在ChartDisplay中通过setChart方法确定具体的子类是PieChart还是BarChart等,在display中调用chart.display来调用子类的display方法;如果需要添加扩展,只需要新建新的Chart类,并实现具体的display方法即可,不需要修改源代码。
三、里氏替换原则
里氏替换原则是指所有引用基类的地方必须能够透明地引用其子类的对象。即将父类替换为子类,程序不会引起异常,反之将子类替换为父类则不行。
- 子类的所有方法必须在父类中声明,或子类必须实现父类的所有方法。为了方便扩展,如果子类实现了父类中没有的方法,那么以父类定义的方法无法调用子类的特定方法。
- 在运用里氏替换原则时,尽量将父类设计成接口或者抽象类,让子类继承父类或者实现父类的接口,并实现父类中声明的方法。
ps:里氏替换原则是开闭原则的具体实现方法。
四、依赖倒置原则
依赖倒置原则指抽象不应依赖于细节,细节应该依赖于抽象。要针对接口编程,而不是针对实现编程。
大多数情况下,开闭原则、里氏替换原则、依赖倒置原则会同时出现,三者相辅相成、目标一致,只是对同一问题的不同角度的思考而已。
五、接口隔离原则
接口隔离原则:使用多个专门的接口,而不是使用一个总接口,即客户端不应该依赖那些不需要的接口。
六、最少知识原则
一个软件应该尽可能少的与其他实体发生关系。
七、合成复用原则
尽量使用对象组合,而不是继承来实现复用的目的。
六个创建型设计模式
一、简单工厂模式
简单工厂模式:定义一个工厂类,它可以根据参数的不同返回不同的事例,被定义的事例大多都有共同的父类。
简单工厂模式包含以下角色:
- Product(抽象产品角色):工厂所创建的角色抽象出来的抽象对象,是创建角色的父类,封装了角色的各种方法。
- ConcreateProduct(具体产品角色):简单工厂模式创建的具体目标。
- Factory(工厂角色):工厂类,负责创建所有产品角色的逻辑。在工厂类中实现了静态工厂方法,返回类型为Product,所以简单工厂模式又被称为 静态工厂模式 。
具体使用范例:
客户要求展示柱状图、饼状图、折线图的一种,根据传入的参数进行选择,如果没有使用设计模式,则实现过程如下。
class Chart {
private String type; //图表类型
public Chart(Object[][] data, String type) {
this.type = type;
if (type.equalsIgnoreCase("histogram")) {
//初始化柱状图
} else if (type.equalsIgnoreCase("pie")) {
//初始化饼状图
} else if (type.equalsIgnoreCase("line")) {
//初始化折线图
}
}
public void display() {
if (this.type.equalsIgnoreCase("histogram")) {
//显示柱状图
} else if (this.type.equalsIgnoreCase("pie")) {
//显示饼状图
} else if (this.type.equalsIgnoreCase("line")) {
//显示折线图
}
}
}
代码中的问题有:
- 包含太多的if...else...,代码冗长,阅读难度大;
- Chart类职责过重,将对象的判断、初始化、展示全部放在一个类中,不符合单一职责原则;
- 如果需要增加新表,需要修改Chart类源代码,不符合开闭原则;
- 对象通过new实例化,类与客户端的耦合性太高,创建与使用无法分离;
按照简单工厂模式进行重构: - 将对象类抽象出一个抽象产品类,可以是抽象类,也可以是接口
interface Chart {
public void display();
}
- 具体产品类实现抽象接口类,并实现具体的初始化以及展示方法
//柱状图类:具体产品类
class HistogramChart implements Chart {
public HistogramChart() {
System.out.println("创建柱状图!");
}
public void display() {
System.out.println("显示柱状图!");
}
}
- 创建工厂类,根据需求初始化对象,其中包含了 静态 的工厂方法,用于创建对象
//图表工厂类:工厂类
class ChartFactory {
//静态工厂方法
public static Chart getChart(String type) {
Chart chart = null;
if (type.equalsIgnoreCase("histogram")) {
chart = new HistogramChart();
System.out.println("初始化设置柱状图!");
}
else if (type.equalsIgnoreCase("pie")) {
chart = new PieChart();
System.out.println("初始化设置饼状图!");
}
else if (type.equalsIgnoreCase("line")) {
chart = new LineChart();
System.out.println("初始化设置折线图!");
}
return chart;
}
}
- 客户端直接使用工厂类进行对象的初始化
class Client {
public static void main(String args[]) {
Chart chart;
chart = ChartFactory.getChart("histogram"); //通过静态工厂方法创建产品
chart.display();
}
}
- 第三步中判断客户端的参数来进行判断初始化,如果需要增加新的对象,那么需要修改工厂类的源代码,不符合开闭原则,可以通过配置文件进行优化(根据开闭原则的定义,配置文件的修改不算是违背开闭原则)。
在xml配置文件中定义键值对,通过DOM读取文件中的对象名,通过Java反射获取制定的对象事例。
public static Object getBean() {
try {
//创建DOM文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));
//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();
/* 其他工厂模式中使用,使其完全符合开闭原则
//通过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;*/
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
缺点:
- 只有一个工厂类,职责过重;
- 引入新的类(工厂类、抽象对象类),增加了系统复杂度;
- 仍不符合开闭原则;
- 工厂方法使用了静态方法,工厂类无法形成继承结构。
二、工厂方法模式
工厂方法模式:定义一个用于创建对象的接口,让子类决定具体创建哪个对象。
工厂方法模式包含的角色:
- Product(抽象产品):定义产品的接口,所有产品对象的超类型;
- ConcreateProduct(具体产品):需要创建的产品对象;
- Factory(抽象工厂):定义工厂的接口,所有具体工厂的超类型;
- ConcreateFactory(具体工厂):抽象工厂的子类,实现了抽象工厂中的工厂方法,与ConcreateProduct是一对一关系,由客户端调用。
具体使用范例:
以前面的简单工厂模式为例,需要对简单工厂模式重构的代码进行再次重构。
- 将对象类抽象出一个抽象产品类,可以是抽象类,也可以是接口
interface Chart {
public void display();
}
- 具体产品类实现抽象接口类,并实现具体的初始化以及展示方法
//柱状图类:具体产品类
class HistogramChart implements Chart {
public HistogramChart() {
System.out.println("创建柱状图!");
}
public void display() {
System.out.println("显示柱状图!");
}
}
- 创建抽象工厂类
//抽象工厂类
interface ChartFactory{
public Chart createChart();
}
- 创建具体工厂类
//图表工厂类:具体工厂类
class HistogramChartFactory implements ChartFactory{
@Override
public Chart createChart() {
Chart chart = new HistogramChart();
return chart;
}
}
- 客户端直接使用工厂类进行对象的初始化
class Client {
public static void main(String args[]) {
ChartFactory factory;
Chart chart;
factory = new HistogramChartFactory(); //可引入配置文件实现
chart = factory.createChart();
chart.display();
}
}
- 第五步中判断客户端的参数来进行判断初始化,如果需要增加新的对象,那么需要修改工厂类的源代码,不符合开闭原则,可以通过配置文件进行优化(根据开闭原则的定义,配置文件的修改不算是违背开闭原则)。
在xml配置文件中定义键值对,通过DOM读取文件中的对象名,通过Java反射获取制定的对象事例。
public static Object getBean() {
try {
//创建DOM文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));
//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();
/* 其他工厂模式中使用,使其完全符合开闭原则
//通过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;*/
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
优点:
- 完全符合开闭原则;
缺点: - 添加新的对象时,需要创建对应的具体对象类与具体工厂类,系统中的类成对增加;
- 使用了DOM、Java反射,增加了系统实现难度。
三、抽象工厂模式
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
抽象工厂模式中包含的角色:
- Factory(抽象工厂):声明了一组用于创建一族对象的方法,一个方法对应一类对象;
- ConcreateFactory(具体工厂):实现了抽象工厂中具体创建对象的方法,生成一些具体产品对象;
- Product(抽象对象):为每种产品声明接口,所有产品对象的超类型;
- ConcreateProduct(具体对象):具体的产品对象。
具体使用范例:
以上述的工厂方法模式为例,现有柱状图、圆饼图、折线图,如果要分别增加红色、白色、黑色三种颜色,按照工厂方法模式,需要建立9种具体工厂类,如果按照抽象工厂模式,只需要三种,代码需要重构。
- 将对象类抽象出一个抽象产品类,可以是抽象类,也可以是接口
interface HistogramChart {
public void display();
}
interface PieChart {
public void display();
}
interface LineChart {
public void display();
}
- 具体产品类实现抽象接口类,并实现具体的初始化以及展示方法
//柱状图类:具体产品类
class RedHistogramChart implements HistogramChart {
public RedHistogramChart() {
System.out.println("创建红色柱状图!");
}
public void display() {
System.out.println("显示红色柱状图!");
}
}
...
- 创建抽象工厂类
//抽象工厂类
interface ChartFactory{
public HistogramChart createHistogramChart();
public PieChart createPieChart();
public LineChart createLineChart();
}
...
- 创建具体工厂类
//图表工厂类:具体工厂类
class RedChartFactory implements ChartFactory{
@Override
public HistogramChart createHistogramChart() {
HistogramChart chart = new RedHistogramChart();
return chart;
}
@Override
public PieChart createPieChart() {
PieChart chart = new RedPieChart();
return chart;
}
@Override
public LineChart createLineChart() {
LineChart chart = new RedLineChart();
return chart;
}
}
...
- 客户端直接使用工厂类进行对象的初始化
class Client {
public static void main(String args[]) {
//使用抽象层定义
ChartFactory factory;
HistogramChart hc;
PieChart pc;
LineChart lc;
factory = (ChartFactory)XMLUtil.getBean();
hc = factory.createHistogramChart();
pc = factory.createPieChart();
lc = factory.createLineChart();
hc.display();
pc.display();
lc.display();
}
}
缺点:
结构复杂,重构工作量大。
四、单例模式
单例模式:确保某一个类只有一个实例,而且自行初始化并向整个系统提供这个实例。
单例模式中的角色:
- singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
单例模式的实现步骤:
- 单例模式只允许存在一个实例,由于new方法每次都能初始化一个实例,所以应将对象的构造方法设为private;
- 由该私有构造方法构造的唯一实例需要向整个系统公开,所以为了保证唯一性,需要定义一个静态私有变量来存放这个实例;
- 为了向系统公开,定义一个公开的getInstance方法,并返回第二步的静态私有变量。
在getInstance方法中,为了向系统提供唯一的实例,需要先判断实例是否初始化了,如果有则返回,如果没有则初始化并返回。当不同线程同时调用该方法时,两个判断语句都会返回false,并且同时初始化实例,导致系统不止有一个实例,引起异常。因此需要对单例模式进行线程安全操作,常见的解决办法有饿汉式、懒汉式、IoDH。
饿汉式
在私有静态变量定义的时候进行初始化,这样在类加载的时候就已经创建了单例,只需要在getInstance中返回该单例就可以了。
private static final EagerSingleton instance = new EagerSingleton();
优点:在类加载时初始化,无需考虑多线程,可保证只有一个实例,从调用速度跟反应时间来说是最优解决方案。
缺点:无论是否使用,在类加载时都会初始化对象,从资源利用率上来说不如懒汉式,且软件加载时间会变长。
懒汉式
在私有静态变量定义的时候不进行初始化,而是在getInstance方法中进行线程保护,可以为etInstance方法添加 synchronized 修饰符,这样在不同线程同时调用getInstance时,就会依次进行访问。
为getInstance方法添加synchronized修饰符后,每次调用方法都会进行同步锁的判定,会消耗大量的系统资源,因此可以对懒汉式进行优化,去掉getInstance方法的synchronized修饰符,只对方法内的初始化方法进行锁定,这样只有在第一次调用时进行同步锁判定,之后都不会影响性能。
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
使用以上优化后,表面上是对线程同步做了保护,但实际上仍会出现多个单例对象出现的现象。假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-CheckLocking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:
private volatile static LazySingleton instance = null;
public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //创建单例实例
}
}
}
return instance;
}
需要注意的是,如果使用双重检查锁定,需要在静态变量前面添加volatile修饰符,以便成员变量可以在多个线程中被正确处理。
优点:实例在第一次使用时创建,无需一直占用系统资源,实现了延迟加载。
缺点:多线程问题需要进行处理,在第一次初始化时可能占用大量资源进行判断。
IoDH
在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用。
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
public static void main(String args[]) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
在第一次调用时进行初始化,通过静态内部类HolderClass进行线程保护,因为HolderClass中的instance是静态成员变量,所以该变量的线程保护由Java虚拟机来保证其安全性,因此性能不会受到影响。
优点:从调用速度、资源利用率来说都是最优选择。
缺点:有些语言不支持。
五、原型模式
原型模式:使用原型实例创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式包含的角色:
- Prototype(抽象原型类):它是声明克隆方法的接口,是所有原型类的公共父类,可以是抽象类也可以是接口,甚至可以是具体实现类。
- ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回一个自己的克隆对象。
- Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。
实现方法一:通用实现方法
在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新创建的对象中。
public Prototype clone() //克隆方法
{
Prototype prototype = new ConcretePrototype(); //创建新对象
prototype.setAttr(this.attr);
return prototype;
}
实现方法二:Java语言提供的clone方法
所有Java类都继承自java.lang.Object,Object类提供了一个clone方法,可以将Java对象复制一份。需要注意的是能够实现克隆的Java类必须实现Cloneable接口,否则会报异常。
class ConcretePrototype implements Cloneable
{
……
}
克隆方法分为深克隆跟浅克隆,主要区别在于是否支持引用类型的成员变量的复制。
浅克隆
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象,如果是引用类型,则将引用对象的地址复制一份给克隆对象。原型模型的第二种方式通过覆盖Java中Object类中的clone方法实现的克隆就是浅克隆。
深克隆
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都会复制一份给克隆对象。如果要实现深克隆,需要使用原型模式的第一种实现方式,通过序列化进行实现。所以需要深克隆的对象必须实现Serialiazable接口。
public WeeklyLog deepClone() throws IOException, ClassNotFoundException, OptionalDataException
{
//将对象写入流中
ByteArrayOutputStream bao=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bao);
oos.writeObject(this);
//将对象从流中取出
ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
return (WeeklyLog)ois.readObject();
}
六、建造者模式
建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式中的角色:
Builder(抽象建造者):它为创建一个产品对象的各个部件制定抽象接口,该接口中一般有两类方法,一类是buildPartX,用于创建对象的各个部件;另一类是getResult,用于返回复杂对象;
ConcreteBuilder(具体创建者):实现了Builder接口,实现具体的内部构造和装配方法,并返回目标对象;
Product(产品角色):被构建的复杂对象;
Director(指挥者):又称为导演类,复杂安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,指挥者通过抽象建造者进行对象的创建。
class Director {
private Builder builder;
public Director(Builder builder) {
this.builder=builder;
}
public void setBuilder(Builder builder) {
this.builder=builer;
}
//产品构建与组装方法
public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}
建造者模式跟抽象工厂模式有点类似,但建造者模式返回一个完整的复杂对象,抽象工程模式返回一系列对象。