创建型设计模式
GoF 是 "Gang of Four"(四人帮)的简称,它们是指 4 位著名的计算机科学家:
Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides。他们合作编写
了一本非常著名的关于设计模式的书籍《Design Patterns: Elements of Reusable
Object-Oriented Software》(设计模式:可复用的面向对象软件元素)。这本书在
软件开发领域具有里程碑式的地位,对面向对象设计产生了深远影响。
GoF 提出了 23 种设计模式,将它们分为三大类:
GoF 提出了 23 种设计模式,将它们分为三大类:
1. 创建型模式(Creational Patterns):这类模式主要关注对象的创建过程。它们
分别是:
单例模式(Singleton)
工厂方法模式(Factory Method)
抽象工厂模式(Abstract Factory)
建造者模式(Builder)
原型模式(Prototype)
2. 结构型模式(Structural Patterns):这类模式主要关注类和对象之间的组合。
它们分别是:
适配器模式(Adapter)
桥接模式(Bridge)
组合模式(Composite)
装饰模式(Decorator)
外观模式(Facade)
享元模式(Flyweight)
代理模式(Proxy)
3. 行为型模式(Behavioral Patterns):这类模式主要关注对象之间的通信。它们
分别是:
职责链模式(Chain of Responsibility)
命令模式(Command)
解释器模式(Interpreter)
迭代器模式(Iterator)
中介者模式(Mediator)
备忘录模式(Memento)
观察者模式(Observer)
状态模式(State)
策略模式(Strategy)
模板方法模式(Template Method)
访问者模式(Visitor)
这些设计模式为面向对象软件设计提供了一套可复用的解决方案。掌握和理解这些模
式有助于提高软件开发人员的编程技巧和设计能力。
单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建
一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模
式,简称单例模式。
一、为什么要使用单例
1、表示全局唯一
如果有些数据在系统中应该且只能保存一份,那就应该设计为单例类。如:
配置类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,应
该被映射为一个唯一的【配置实例】,此时就可以使用单例,当然也可以不用。
全局计数器:我们使用一个全局的计数器进行数据统计、生成全局递增ID等功
能。若计数器不唯一,很有可能产生统计无效,ID重复等。
public class GlobalCounter {
private AtomicLong atomicLong = new AtomicLong(0);
private static final GlobalCounter instance = new GlobalCounter();
// 私有化无参构造器
private GlobalCounter() {}
public static GlobalCounter getInstance() {
return instance;
}
public long getId() {
return atomicLong.incrementAndGet();
}
}
// 查看当前的统计数量
long courrentNumber = GlobalCounter.getInstance().getId();
2、处理资源访问冲突
如果让我们设计一个日志输出的功能,你不要跟我杠,即使市面存在很多的日志框
架,我们也要自己设计。
如下,我们写了简单的小例子
public class Logger {
private String basePath = "D://info.log";
private FileWriter writer;
public Logger() {
File file = new File(basePath);
try {
writer = new FileWriter(file, true); //true表示追加写入
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}
当然,任何的设计都不是拍脑门,这是我们写的v1版本,他很可能会存在很多的
bug,设计结束之后,我们可能是这样使用的:
@RestController("user")
public class UserController {
public Result login(){
// 登录成功
Logger logger = new Logger();
logger.log("tom logged in successfully.");
// ...
return new Result();
}
}
当然,其他千千万的代码,我们都是这样写的。这样就会产生如下的问题:多个
logger实例,在多个线程中,同时操作同一个文件,就可能产生相互覆盖的问题。
因为tomcat处理每一个请求都会使用一个新的线程(暂且不考虑多路复用)。此时
日志文件就成了一个共享资源,但凡是多线程访问共享资源,我们都要考虑并发修改
产生的问题。
有的同学可能想到如下的解决方案,加锁呀,代码如下:
事实上这样加锁毫无卵用,方法级别的锁可以保证new出来的同一个实例多线程下可
以同步执行log方法,然而你却new了很多:
public synchronized void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
事实上这样加锁毫无卵用,方法级别的锁可以保证new出来的同一个实例多线程下可
以同步执行log方法,然而你却new了很多:
其实,writer方法本身也是加了锁的,我们这样加锁就没有了意义
public void write(String str, int off, int len) throws IOException {
synchronized (lock) {
char cbuf[];
if (len <= WRITE_BUFFER_SIZE) {
if (writeBuffer == null) {
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
cbuf = writeBuffer;
} else { // Don't permanently allocate very large buffers.
cbuf = new char[len];
}
str.getChars(off, (off + len), cbuf, 0);
write(cbuf, 0, len);
}
}
当然,加锁是一定能解决共享资源冲突问题的,我们只要放大锁的范围从【this】到
【class】,这个问题也是能解决的,代码如下:
public void log(String message) {
synchronized (Logger.class) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
从以上的内容我们也发现了:
如果使用单个实例输出日志,锁【this】即可。
如果要保证JVM级别防止日志文件访问冲突,锁【class】即可。
如果要保证集群服务级别的防止日志文件访问冲突,加分布式锁即可。
如果我们是一个简单工程,对日志输入要求不高。单例模式的解决思路就十分合适,
既然同一个Logger无法并行输出到一个文件中,那么针对这个日志文件创建多个
Logger实例也就失去了意义,如果工程要求我们所有的日志输出到同一个日志文件
中,这样其实并不需要创建大量的Logger实例,这样的好处有:
一方面节省内存空间。
另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能
随便浪费)。
按照这个设计思路,我们实现了 Logger 单例类。具体代码如下所示
如果我们是一个简单工程,对日志输入要求不高。单例模式的解决思路就十分合适,
既然同一个Logger无法并行输出到一个文件中,那么针对这个日志文件创建多个
Logger实例也就失去了意义,如果工程要求我们所有的日志输出到同一个日志文件
中,这样其实并不需要创建大量的Logger实例,这样的好处有:
一方面节省内存空间。
另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能
随便浪费)。
按照这个设计思路,我们实现了 Logger 单例类。具体代码如下所示:
ublic class Logger {
private String basePath = "D://log/";
除此之外,并发队列(比如 Java 中的 BlockingQueue)也可以解决这个问题:多个
线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据写入到日志
文件。这种方式实现起来也稍微有点复杂。当然,我们还可将其延伸至消息队列处理
分布式系统的日志。
二、如何实现一个单例
常见的单例设计模式,有如下五种写法,在编写单例代码的时候要注意以下几点:
1、构造器需要私有化
private static Logger instance = new Logger();
private FileWriter writer;
private Logger() {
File file = new File(basePath);
try {
writer = new FileWriter(file, true); //true表示追加写入
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Logger getInstance(){
return instance;
}
public void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}