学习一下几个设计模式,做一下记录,仅供参考。
模式 | 包括 |
---|---|
创建型模式 | 单例模式(Singleton Pattern)、工厂模式(Factory Pattern)、抽象工厂模式(Abstract Factory Pattern)、建造者模式(Builder Pattern)、原型模式(Prototype Pattern) |
单例模式
- 什么是单例模式?
单例模式是Java中最简单的设计模式之一,属于创建型设计模式,它提供了一种最佳的创建对象的方式。这种模式涉及到一个单一的类,这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类。
意图:保证一个类只有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁的创建和销毁。
何时使用:想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经存在这个单例,有则返回,没有则创建。
关键代码点:构造方法是私有的
例子:
- Windows是多进程多线程的,在操作一个文件的时候,就不可避免多个进程或线程同时操作一个文件的现象,所以对文件的处理必须通过唯一一个实例来进行。
- 一些设备管理器常常设置为单例模式。比如一个电脑有2台打印机,在输出的时候就要处理不能两台打印机打印同一个文件
优点:
- 在内存中只有一个实例,减小开销,尤其是频繁的创建和销毁。
- 避免对系统资源的多重占用(比如写文件操作)
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关系外部怎么样来实例化。
使用场景
- 要求产生唯一序列号
- web中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
- 创建一个对象所消耗的资源过多,比如I/O与数据库的连接等
- 文件系统、打印机、资源管理器等,因为底层资源只能同时被一方操纵,所以这些模块暴露的接口必然是单例的
- Java 中的 dao、service 一般都是单例的,而 action 层一般都是多例。
注意点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须提供给所有对象这一实例
- getInstance()方法中需要使用同步锁synchronize防止多线程同时进入造成instance被多次实例化
实现:
1.懒汉式,线程不安全
特点:
1、懒加载
2、线程不安全
特点:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class SingleObject {
public static SingleObject singleObject;
private void SingleObject(){
}
public static SingleObject getInstance(){
if(singleObject == null){
singleObject = new SingleObject();
}
return singleObject;
}
}
2.懒汉式,线程安全
特点:
1、懒加载
2、线程安全
优点:第一次调用才初始化,避免内存浪费
缺点:需要加synchronize关键字才能保证线程安全,但是加锁会影响效率。
public class SingleObject {
public static SingleObject singleObject;
private void SingleObject(){
}
public static synchronized SingleObject getInstance(){
if(singleObject == null){
singleObject = new SingleObject();
}
return singleObject;
}
}
3.饿汉式
特点:
1、非懒加载
2、线程安全
优点:没有加锁,执行效率提高
缺点:类加载时就初始化,浪费内存
基于类加载器的机制避免了多线程同步的问题,但是无法实现懒加载了
public class SingleObject {
public static SingleObject singleObject = new SingleObject();
private void SingleObject(){
}
public static SingleObject getInstance(){
return singleObject;
}
}
4.双检锁/双重校验锁(DCL, double-checked locking)
要求:JDK1.5之后
特点:
1、懒加载
2、线程安全
采取双锁机制,不仅线程安全而且还高效
public class SingleObject {
//利用volatile,防止指令重排导致的问题
public static volatile SingleObject singleObject;
private void SingleObject(){
}
public static synchronized SingleObject getInstance(){
if(singleObject == null){
synchronized (SingleObject.class){
if(singleObject == null){
singleObject = new SingleObject();
}
}
}
return singleObject;
}
}
上面代码用了volatile,为的是防止多线程下指令重排导致的问题
5.静态内部类/登记式
特点:
1、懒加载
2、线程安全
这种方式能达到双检锁的效果,而且实现方式更简单。对静态域使用延迟初始化但是又不是使用双检锁的方式。这种方式只适用于静态域的情况,双检锁方式可以在实例域延迟初始化时使用。
也是利用了类加载器的加载机制保证初始化时只有一个线程。
跟第三种方式不同的是:第三种方式只要SingleObject 类被加载,instance就会被实例化,没有懒加载的效果。而这种方式,就算SingleObject 类被加载,instance不一定被初始化了,**因为SingleObjectHolder类没有被主动使用,**只有显式调用getInstance()方法时,才会显式装载SingleObjectHolder类,从而实例化instance。
public class SingleObject {
public static class SingleObjectHolder{
private static final SingleObject INSTANCE = new SingleObject();
public static final SingleObject getInstance(){
return SingleObjectHolder.INSTANCE;
}
}
}
6.枚举
特点:
1、非懒加载
2、线程安全
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
工厂模式
工厂模式是什么:顾名思义,一个模型,用来大规模的生产同类产品。该模式将对象的具体实例过程抽象化,并不关心具体的创建过程。
为什么存在:可快速规模化实例化对象
简单工厂的实现:一个工厂类根据传入的参数,动态决定创造哪一个产品类实例(这些产品类继承同一个接口或父类)
特点:将对象的具体实例过程抽象化,并不关系具体的创建过程。
意图:定义一个创建对象的接口,让其子类自己决定实例化哪个类
何时使用:明确地计划不同条件时创建不同的实例
如何解决:让其子类实现工厂接口,返回的也是一个抽象的实例
关键代码:子类创建实例
-
优点:
1.一个调用者想创建一个实例,知道它的名字就行了
2.扩展性提高,如果想增加一个产品,就增加一个工厂类
3.屏蔽具体的实现,调用者只需要关心接口 -
缺点:
1、每次增加一个产品,都要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事 -
例子:
我们经常使用的数据库中间件,我们无需关心具体的底层实现类,只需将用户名密码等连接信息传过去,就会直接获取到相应的数据库连接实例,这个角度,就可以将数据库中间件看作一个大的工厂 -
使用场景:
日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方
数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时 -
注意事项:
作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度 -
分类:简单普通工厂、多方法简单工厂、静态方法简单工厂
-
实现:
普通简单工厂:
UML图
先新建一个共有的接口:
public interface Sender {
public void send();
}
再新建三个实现类:
public class EmailSender implements Sender{
@Override
public void send() {
System.out.println("send email");
}
}
public class SmsSender implements Sender{
@Override
public void send() {
System.out.println("send sms");
}
}
public class ExpressSender implements Sender{
@Override
public void send() {
System.out.println("send express");
}
}
然后,新建一个工厂类来生成“产品”:
public class SenderFactory {
public Sender produce(String type){
if(type == "email"){
return new EmailSender();
}else if(type == "express"){
return new ExpressSender();
}else if(type.equals( "sms")){
return new SmsSender();
}else{
return null;
}
}
}
然后开始测试:
public class Demo {
public static void main(String[] args) {
SenderFactory senderFactory = new SenderFactory();
Sender sms = senderFactory.produce("sms");
sms.send();
Sender email = senderFactory.produce("email");
email.send();
Sender express = senderFactory.produce("express");
express.send();
}
}
多方法简单工厂:
多方法简单工厂是根据普通工厂的基础改进的,简单工厂方法如果传递的参数错误,那就无法正确创建对象。多方法直接将produce的逻辑展开到具体方法中,从而避免该问题。
uml图:
工厂类:
public class SenderFactory {
public Sender produceEmail(){
return new EmailSender();
}
public Sender produceSms(){
return new SmsSender();
}
public Sender produceExpress(){
return new ExpressSender();
}
}
测试:
public class Demo {
public static void main(String[] args) {
SenderFactory senderFactory = new SenderFactory();
Sender sms = senderFactory.produceSms();
sms.send();
Sender email = senderFactory.produceEmail();
email.send();
Sender express = senderFactory.produceExpress();
express.send();
}
}
静态方法简单工厂
普通工厂模式和多方法工厂模式有一个弊端,就是需要频繁的实例化工厂类,一般我们会将 “多方法” 设置为静态的,从而避免类的频繁实例化,拿来即用
public class Main {
public static void main(String[] args) {
//SendFactory sendFactory = new SendFactory();
Sender senderEmail = SenderFactory.produceEmail();
senderEmail.Send();
Sender senderSms = SenderFactory.produceSms();
senderSms.Send();
Sender senderExpress = SenderFactory.produceExpress();
senderExpress.Send();
}
}
简单工厂的延申 — 工厂方法模式
简单工厂模式有个比较明显的弊端:工厂类集中了所有实例的创建逻辑,明显违背高内聚的责任分配原则,违背了闭包规则。
而工厂方法模式则是对该问题的进一步延伸解决,差异就是将原先存在于一个工厂类中的逻辑抽调出来,创建一个接口和多个工厂类。这样,一旦功能有新增,比如说我们要加一个 “发送导弹” 的功能,只需要加一个 “导弹发送工厂类”,该类实现 produce 接口返回实例化的 “导弹发送类”,再在 “导弹发送类” 中,实现具体的发送逻辑即可,无需修改之前的业务代码,拓展性较好。
首先,我们还是创建一个 Sender 接口:
public interface Sender {
public void Send();
}
然后我们创建几个具体的实现类:
public class SmsSender implements Sender{
@Override
public void Send() {
System.out.println("发送短信");
}
}
public class ExpressSender implements Sender {
@Override
public void Send() {
System.out.println("发送快递");
}
}
public class EmailSender implements Sender{
@Override
public void Send() {
System.out.println("发送邮件");
}
}
然后,我们统一一下工厂类的接口行为:
public interface Provider {
public Sender produce();
}
继续,定义几个工厂实现上面这种 “行为约束”:
public class ExpressSendFactory implements Provider {
@Override
public Sender produce() {
return new ExpressSender();
}
}
public class EmailSendFactory implements Provider{
@Override
public Sender produce() {
return new EmailSender();
}
}
public class SmsSendFactory implements Provider {
@Override
public Sender produce() {
return new SmsSender();
}
}
测试:
public class Main {
public static void main(String[] args) {
Provider providerSms = new SmsSendFactory();
Sender senderSms = providerSms.produce();
senderSms.Send(); // 发送短信
Provider providerEmail = new EmailSendFactory();
Sender senderEmail = providerEmail.produce();
senderEmail.Send(); // 发送邮件
Provider providerExpress = new ExpressSendFactory();
Sender senderExpress = providerExpress.produce();
senderExpress.Send(); // 发送快递
}
}
工厂方法模式中,核心的工厂类(这里为 Provider 接口)不再负责所有产品的创建,而是将具体创建的工作交给子类去做,该核心类仅扮演抽象工厂的角色,负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应该被实例化的细节,拓展性较简单工厂模式提升明显。
这里真正的好处是,多方法简单工厂的最大弊端是:如果我需要增加一个新的实现,就不得不去修改工厂类,工厂类作为一个基础类尽量不要做改动,试想,如果每当要加一个实现类,大家就改动工厂,那这个类要多么膨胀,后期怎么维护,要符合开闭原则。如果用工厂方法就不一样了,我们对工厂进行接口化,并提供各个实现工厂类,如果你要新增新的实现类,那你就再扩展一个新的工厂,不会对接口进行改变,想怎么扩展就怎么扩展,别看代码量上去了,但是在实际业务中会极大的方便后期维护。这样是松耦合的,并且符合开闭原则。
抽象工厂模式
是什么:为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们具体类。
通俗一点理解就是:对一组具有相同主题的工厂进行的封装。比如要生产一台电脑,如果使用的工厂模式,那么就会有显卡工厂,主板工厂等等……但是使用抽象工厂模式,只会有一个电脑工厂,而电脑工厂里面又涵盖了显卡工厂、主板工厂、cpu工厂等等……。所以工厂模式针对的是同一类或者同等级的产品,而抽象工厂则是针对多种的产品设计。
解决的问题:接口选择的问题
怎么解决:在一个产品族里面,定义多个产品
什么时候会用到:系统的产品里面有多余一个的产品族,而系统只消费某一族的产品。
优点:
- 易于交换产品的系列
- 让具体的创建实例过程和客户端分离
关键代码:在一个工厂里聚合多个同类产品
什么是产品族:比如上面电脑的例子:电脑中的主板和显卡就是两个相互依赖的产品族。
例子:
加入项目中使用的是sql server数据库,结果有一天开会,需求更改,项目经理要求数据库改为Access。假设数据库操作的是用户,那么:
最原始的项目代码是:
用户类User:只有id和name字段
public class User {
private Integer id;
private String name;
}
/**
* getter setter
*/
SqlServerUser类:用于操作User,现在只有新增和修改
public class SqlserverUser {
public void insert(User user){
System.out.println("sql server 新增一个用户");
}
public User select(Integer id){
System.out.println("sql server 获得一个用户");
return null;
}
}
客户端代码:
public class Demo {
public static void main(String[] args) {
//与sql server耦合
SqlserverUser sqlserverUser = new SqlserverUser();
User user = new User();
//新增一个用户
sqlserverUser.insert(user);
//获得一个用户
sqlserverUser.select(1);
}
}
最开始这种写法非常简单,但是问题在于:
SqlserverUser sqlserverUser = new SqlserverUser() 使得sqlserverUser这个对象被写死在了Sql server数据库上,如果这里灵活一点,可以是多态的话,那么调用insert()和select()方法时,就不需要知道底层是用的Sql server 还是Access了。接着进行下一步的优化:
工厂方法模式:
新创建一个User接口,用于客户端访问,解除与具体数据库访问的耦合
public interface IUser {
public User select(int id);
public void insert(User user);
}
SqlServerUser类:用于访问sql server的user
public class SqlserverUser implements IUser{
@Override
public User select(int id) {
System.out.println("sql server 获得一个用户");
return null;
}
public void insert(User user){
System.out.println("sql server 新增一个用户");
}
}
AccessUser类:用于访问Access的user
public class AccessUser implements IUser{
@Override
public User select(int id) {
System.out.println("Access获得一个用户");
return null;
}
public void insert(User user){
System.out.println("Access新增一个用户");
}
}
DBFactory接口:定义一个创建User表对象的抽象工厂接口
public interface DBFactory {
public IUser createUser();
}
SqlServerFactory类:实例化SqlServerUser
public class SqlServerFactory implements DBFactory {
@Override
public IUser createUser() {
return new SqlserverUser();
}
}
AccessFactory类:实例化AccessUser
public class AccessFactory implements DBFactory {
@Override
public IUser createUser() {
return new AccessUser();
}
}
客户端:
public class Demo {
public static void main(String[] args) {
User user = new User();
DBFactory factory = new SqlServerFactory();
//DBFactory factory = new AccessFactory();
IUser iu = factory.createUser();
iu.insert(user);
iu.select(1);
}
}
若要更换成Access数据库,那么简化为上面客户端代码:
DBFactory factory = new SqlServerFactory();→DBFactory factory = new AccessFactory();即可。
此时由于多态的关系,使得对象iu一开始根本不知道访问的是哪个数据库,这就是所谓的业务逻辑和数据访问的解耦。
那么如果数据库里不止一个User表,还有其他表,比如说部门表Dept,那怎么办?
新建一个部门接口:
public interface IDept {
public void create(Dept dept);
public Dept select(int id);
}
SqlServerDept类:用于访问Sql server的Dept:
public class SqlServerDept implements IDept {
@Override
public void create(Dept dept) {
System.out.println("sql server 新建了一个部门");
}
@Override
public Dept select(int id) {
System.out.println("sql server 查询一个部门");
return null;
}
}
AccessDept类:用于访问Access的Dept:
public class AccessDept implements IDept {
@Override
public void create(Dept dept) {
System.out.println("access新建了一个部门");
}
@Override
public Dept select(int id) {
System.out.println("access查询一个部门");
return null;
}
}
DBFactory接口新增一个新增部门的方法:
SqlServerFactory类和AccessFactory需要做对应的实现
客户端代码要更改为:
public class Demo {
public static void main(String[] args) {
User user = new User();
DBFactory factory = new SqlServerFactory();
//DBFactory factory = new AccessFactory();
IUser iu = factory.createUser();
iu.insert(user);
iu.select(1);
Dept dept = new Dept();
IDept idept = factory.createDept();
idept.create(dept);
idept.select(1);
}
}
此时部门和用户一样,若要更换成Access数据库,
DBFactory factory = new SqlServerFactory();
转化为:
DBFactory factory = new AccessFactory();
如果只有User类和User操作类,那么只需要工厂模式就可以了,但是现在数据库下有多张表,而Sql server和Access又是两个不同的分类,所以涉及到这种多个产品系列的问题,有一个专门的工厂模式叫抽象工厂模式
抽象工厂模式
下图源自《大话设计模式》
DBFactory是一个抽象工厂接口,里面包含所有产品创建的抽象方法。比如上面的:
public interface DBFactory {
public IUser createUser();
public IDept createDept();
}
而ConCreteFactory就相当于上面例子的SqlServerFactory和AccessFactory。这个具体的工厂再创建具有特定实现的产品对象,比如SqlServerFactory创建的就是SqlserverUser和SqlServerDept:
public class SqlServerFactory implements DBFactory {
@Override
public IUser createUser() {
return new SqlserverUser();
}
@Override
public IDept createDept() {
return new SqlServerDept();
}
}
也就是说,为了创建不同的产品对象,客户端就得使用不同的具体工厂。
- 重回上面的优点:
- 易于交换产品的系列
由于具体工厂类,DBFactory factory = new SqlServerFactory();只需要初始化一次,就可以很轻易地改变具体工厂,只需要改变具体工厂就可以使用不同的产品配置。
- 让具体的创建实例过程和客户端分离
它让具体的创建实例过程和客户端分离, 客户端是通过它们的抽象接口操纵实例的,产品的具体类名也被具体工厂的实现分离,不会出现在客户代码中。比如上面的例子,客户端所认识的只有IUser和IDept,至于是SqlServer还是Access实现的就不知道了。
- 缺点:
如果要新加一个Project产品,那么得加IProject、SqlServerProject、AccessProject,还要更改DBFactory、SqlServerFactory、AccessFactory。
如果现在不止一个客户端在使用IUser和IDept,那么在每一个客户端类都得加上 DBFactory factory = new SqlServerFactory();,如果此时要改成Access,那么就得改多个地方,这是十分麻烦的。那么我们引进:用简单工厂改进抽象工厂
用简单工厂改进抽象工厂
去除DBFactory、SqlServerFactory、AccessFactory,取而代之的是DataFactory
由于db实现设置好,所以可以根据选择来实例化不同的对象
public class DataFactory {
public static String db = "Sqlserver";
//通过切换数据库名称,更换为Access
//public static String db = "Access";
public static IUser createUser(){
IUser user = null;
switch (db){
case "Sqlserver":
user = new SqlserverUser();
break;
case "Access":
user = new AccessUser();
break;
}
return user;
}
public static IDept createDept(){
IDept dept = null;
switch (db){
case "Sqlserver":
dept = new SqlServerDept();
break;
case "Access":
dept = new AccessDept();
break;
}
return dept;
}
}
客户端代码:
public class Demo {
public static void main(String[] args) {
User user = new User();
//直接得到实际的数据库访问实例,不存在任何依赖
DataFactory factory = new DataFactory();
IUser iu = factory.createUser();
iu.insert(user);
iu.select(1);
Dept dept = new Dept();
IDept idept = factory.createDept();
idept.create(dept);
idept.select(1);
}
}
由于提前设置了db的值(Sqlserver和Access),所以简单工厂的方法不需要设置参数,这样客户端就只需要 factory.createUser()和factory.createDept()来生成具体的数据库访问类实例,客户端没有出现任何SqlServer和Access字样,达到了解耦的目的
但是这样做的缺点是,如果我要加一个oracle数据库,那么我得去DataFactory 的switch中多加一个case。接下来我们用反射+抽象工厂来设置
反射+抽象工厂