01
—
前言
有这么一个很形象的比喻,把写代码比作是建房子,代码比作是砖瓦、一个完整的系统就好比是一栋高楼大厦、程序员无疑就好比是建(ban)筑(zhuan)工,这些很表面的东西我们都可以很形象深刻的理解,其实要设计和开发一个系统远远不只这些东西,深挖表象之下隐藏着的细节往往才是灵魂所在,诸如:算法和数据结构、框架、设计模式等,设计模式是一个虚幻的抽象的概念,好比建造房子时的设计理念方案一样,一个软件系统扩展性、可维护性以及稳定健壮性如何,很大程度上取决于设计模式。
如下,介绍我们在日常开发中常用的11种设计模式,这些设计模式都是非常经典,翻开源代码和一些开源框架经常会发现它们的踪影。
02
—
单例模式
2.1概念
单例模式(SingleTon):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类的自身负责保存它的唯一实例,这个类可以保证没有其他任何实例可以被创建。并且它可以提供一个访问该实例的方法。
单例模式有两种模式,一个是饿汉模式,一个是懒汉模式。
在对象被加载时,定义的静态全局变量的就new实例化对象。
public class SingleTon{
private static readonly SingleTon instance = new SingleTon();//静态初始化时就把对象实例化
private SingleTon(){
}
private static getInstance(){
return instance ;
}
}
只有在调用了该对象的时候,才调用构造方法去实例化该对象实例。 多线程场景需要在构造方法里增加Lock,双重锁判断,其中有2个null判断。
public class Singleton {
/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
private static Singleton instance;
/* 私有构造方法,防止被实例化 */
private Singleton() {}
/* 1:懒汉式,静态工程方法,创建实例 */
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:延迟加载(需要的时候才去加载),适合单线程操作 缺点:线程不安全,在多线程中很容易出现不同步的情况,如在数据库对象进行的频繁读写操作时。
public class Singleton {
/* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */
private static Singleton instance ;
private static readonly object syncRoot = new object();
/* 私有构造方法,防止被实例化 */
private Singleton() {}
/* 1:懒汉式,静态工程方法,创建实例 */
public static Singleton getInstance() {
if(null == instance ){
lock(syncRoot){
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
03
—
简单工厂模式
定义一个用于创建对象的接口,一个工厂类处于对产品类实例化调用的中心位置上,它决定哪一个产品类应当被实例化。
组成如下:
工厂类角色:含有一定的业务逻辑和判断逻辑,Java里往往是一个具体类来实现。
抽象产品角色:具体产品继承的父类或者实现的接口,Java里由接口或者抽象类来实现。
具体产品角色:工厂类所创建的对象就是此类角色的实例,由一个具体类实现。
示例代码:
public class Factory{ //getClass 产生Sample 一般可使用动态类装载装入类。
public static Sample creator(int which){
if (which==1)
return new SampleA();
else if (which==2)
return new SampleB();
}
}
04
—
工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类。
工厂方法模式实现时,客户端需要决定实例化哪一个工厂来实现运算类,选择判断的问题还是存在的,工厂方法把简单工厂的内部逻辑判断
转移到了客户端代码来进行。如果想要加功能,直接在客户端修改即可实现。
//抽象产品角色
public interface Moveable {
void run();
}
//具体产品角色
public class Plane implements Moveable {
@Override
public void run() {
System.out.println("plane....");
}
}
//具体产品角色
public class Broom implements Moveable {
@Override
public void run() {
System.out.println("broom.....");
}
}
//抽象工厂
public abstract class VehicleFactory {
abstract Moveable create();
}
//具体工厂
public class PlaneFactory extends VehicleFactory{
public Moveable create() {
return new Plane();
}
}
//具体工厂
public class BroomFactory extends VehicleFactory{
public Moveable create() {
return new Broom();
}
}
//测试类
public class Test {
public static void main(String[] args) {
VehicleFactory factory = new BroomFactory();
Moveable m = factory.create();
m.run();
}
}
组成:
抽象工厂角色:这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在java中它由抽象类或者接口来实现。
具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象
抽象产品角色:它是具体产品继承的父类或者是实现的接口。在java中一般有抽象类或者接口来实现。
具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在java中由具体的类来实现。
工厂方法模式使用继承自抽象工厂角色的多个子类来代替简单工厂模式中的“上帝类”。正如上面所说,这样便分担了对象承受的压力;而且这样使得结构变得灵活 起来——当有新的产品(即暴发户的汽车)产生时,只要按照抽象产品角色、抽象工厂角色提供的合同来生成,那么就可以被客户使用,而不必去修改任何已有的代 码。可以看出工厂角色的结构也是符合开闭原则的。
05
—
抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,无需指定它们具体的类。
一个系统有多于一个的产品族,而系统只消费其中某一产品族时。
一个系统不依赖于产品类实例如何被创建、组合和表达的细节时。
工厂模式是为一类对象提供创建接口,抽象工厂模式是为创建一组(多类)相关或互相依赖的对象提供创建接口。
示例代码:
//抽象工厂类
public abstract class AbstractFactory {
public abstract Vehicle createVehicle();
public abstract Weapon createWeapon();
public abstract Food createFood();
}
//具体工厂类,其中Food,Vehicle,Weapon是抽象类,
public class DefaultFactory extends AbstractFactory{
@Override
public Food createFood() {
return new Apple();
}
@Override
public Vehicle createVehicle() {
return new Car();
}
@Override
public Weapon createWeapon() {
return new AK47();
}
}
//测试类
public class Test {
public static void main(String[] args) {
AbstractFactory f = new DefaultFactory();
Vehicle v = f.createVehicle();
v.run();
Weapon w = f.createWeapon();
w.shoot();
Food a = f.createFood();
a.printName();
}
}
在抽象工厂模式中,抽象产品 (AbstractProduct) 可能是一个或多个,从而构成一个或多个产品族(Product Family)。在只有一个产品族的情况下,抽象工厂模式实际上退化到工厂方法模式。
06
—
策略模式
策略模式是一种定义一系列算法的方法,从概念上看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少各种算法类与使用算法类之间的耦合。策略模式模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为,继承有助于析取这些算法中的公共功能。策略模式的优点是简化了单元测试。
07
—
装饰模式
7.1 定义
装饰模式是动态地给一个对象添加一些额外额职责,就增加功能来说,装饰模式比生成子类更为灵活。
装饰模式是为已有功能动态地添加更多功能的一种方式。当系统需要新功能的时候,是向旧的类中添加新的代码。这些新加的代码通常装饰了原有的类的核心职责或主要行为。
新加入的东西仅仅是为了满足一些只在某种特定情况下才会执行的特殊行为的需要。装饰模式提供了一个非常好的解决方案,把每个要装饰的功能放在单独的类中,让这个类包装它所要装饰的对象。当需要执行特殊行为时,客户代码就可以在运行时根据需要有选择地按顺序地使用装饰功能包装对象了。
优点:
把类中的装饰功能从类中搬移去除,这样可以简化原有的类。
有效地把类的核心职责和装饰功能区分开,去除相关类中重复的装饰逻辑。
上图是Decorator 模式的结构图,让我们可以进行更方便的描述:
Component是定义一个对象接口,可以给这些对象动态地添加职责。
ConcreteComponent是定义了一个具体的对象,也可以给这个对象添加一些职责。
Decorator是装饰抽象类,继承了Component,从外类来扩展Component类的功能,但对于Component来说,是无需知道Decorator存在的。ConcreteDecorator就是具体的装饰对象,起到给Component添加职责的功能。
假设情景:某人装扮自己形象,穿衣服,裤子,鞋子,戴帽子等来把自己给包装起来,需要把所需的功能按正确的顺序串联起来进行控制,我们应该如何设计才能做到呢?如下,先看下代码结构图:
先创建一个接口类:Component.java
public interface Component {
void show();
}
创建一个具体的 ConcreteComponent 来实现 Component 接口:Person.java
public class Person implements Component{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person(String name){
this.name = name;
}
@Override
public void show() {
System.out.println("装扮的" + name);
}
}
创建装饰类 Decorator 实现 Component 接口
public class Decorator implements Component{
private Component mComponent;
public void decoratorObj(Component component){
mComponent = component;
}
@Override
public void show() {
if(mComponent != null){
mComponent.show();
}
}
}
分别创建具体的装饰类:Jeans.java , Pelisse.java, Sandal.java ...等等,分别继承 Decorator.java 类
/** 牛仔裤 */
public class Jeans extends Decorator {
@Override
public void show(){
System.out.println("穿牛仔裤");
super.show();
}
}
08
—
代理模式
8.1 定义
基本概念:为其他对象提供一种代理以控制对这个对象的访问。也可以说,在出发点到目的地之间有一道中间层,意为代理。
8.2 使用的优势
授权机制不同级别的用户对同一对象拥有不同的访问权利,如在论坛系统中,就使用Proxy进行授权机制控制,访问论坛有两种人:注册用户和游客(未注册用户),论坛就通过类似ForumProxy这样的代理来控制这两种用户对论坛的访问权限。
某个客户端不能直接操作到某个对象,但又必须和那个对象有所互动。
举例两个具体情况:
如果那个对象是一个是很大的图片,需要花费很长时间才能显示出来,那么当这个图片包含在文档中时,使用编辑器或浏览器打开这个文档,打开文档必须很迅速,不能等待大图片处理完成,这时需要做个图片Proxy来代替真正的图片。
如果那个对象在Internet的某个远端服务器上,直接操作这个对象因为网络速度原因可能比较慢,那我们可以先用Proxy来代替那个对象。
总之原则是,对于开销很大的对象,只有在使用它时才创建,这个原则可以为我们节省很多宝贵的Java内存。所以,有些人认为Java耗费资源内存,我以为这和程序编制思路也有一定的关系。
8.3 如何使用
以论坛系统为例,访问论坛系统的用户有多种类型:注册普通用户、论坛管理者、系统管理者、游客。注册普通用户才能发言,论坛管理者可以管理他被授权的论坛,系统管理者可以管理所有事务等,这些权限划分和管理是使用Proxy完成的。
在Forum中陈列了有关论坛操作的主要行为,如论坛名称,论坛描述的获取和修改,帖子发表删除编辑等,在ForumPermissions中定义了各种级别权限的用户:
public class ForumPermissions implements Cacheable {
/**
* Permission to read object.
*/
public static final int READ = 0;
/**
* Permission to administer the entire sytem.
*/
public static final int SYSTEM_ADMIN = 1;
/**
* Permission to administer a particular forum.
*/
public static final int FORUM_ADMIN = 2;
/**
* Permission to administer a particular user.
*/
public static final int USER_ADMIN = 3;
/**
* Permission to administer a particular group.
*/
public static final int GROUP_ADMIN = 4;
/**
* Permission to moderate threads.
*/
public static final int MODERATE_THREADS = 5;
/**
* Permission to create a new thread.
*/
public static final int CREATE_THREAD = 6;
/**
* Permission to create a new message.
*/
public static final int CREATE_MESSAGE = 7;
/**
* Permission to moderate messages.
*/
public static final int MODERATE_MESSAGES = 8;
public boolean isSystemOrForumAdmin() {
return (values[FORUM_ADMIN] || values[SYSTEM_ADMIN]);
}
//相关操作代码
}
因此,Forum中各种操作权限是和ForumPermissions定义的用户级别有关系的,作为接口Forum的实现:ForumProxy正是将这种对应关系联系起来。比如,修改Forum的名称,只有论坛管理者或系统管理者可以修改,代码如下:
public class ForumProxy implements Forum {
private ForumPermissions permissions;
private Forum forum;
this.authorization = authorization;
public ForumProxy(Forum forum, Authorization authorization,ForumPermissions permissions){
this.forum = forum;
this.authorization = authorization;
this.permissions = permissions;
}
.....
public void setName(String name) throws UnauthorizedException,
ForumAlreadyExistsException{
//只有是系统或论坛管理者才可以修改名称
if (permissions.isSystemOrForumAdmin()) {
forum.setName(name);
}
else {
throw new UnauthorizedException();
}
}
...
}
而DbForum才是接口Forum的真正实现,以修改论坛名称为例:
public class DbForum implements Forum, Cacheable {
...
public void setName(String name) throws ForumAlreadyExistsException {
....
this.name = name;
//这里真正将新名称保存到数据库中
saveToDb();
....
}
...
}
凡是涉及到对论坛名称修改这一事件,其他程序都首先得和ForumProxy打交道,由ForumProxy决定是否有权限做某一样事情,ForumProxy是个名副其实的"网关","安全代理系统"。
在平时应用中,无可避免总要涉及到系统的授权或安全体系,不管你有无意识的使用Proxy,实际你已经在使用Proxy了。
示意流程图:
09
—
模板方法模式
概念:定义一个操作中的算法的骨架,将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。
模板方法模式是通过把不变行为搬移到超类,去除子类中的重复代码来体现它的优势。
模板方法模式提供了一个代码复用的平台。
模板方法模式在很多组件和框架中经常看到,如:Spring中的各种Template,有JdbcTemplate、RedisTemplate、RestTemplate等。
10
—
建造者模式
10.1 定义
基本概念:是一种对象构建的设计模式,它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
Builder模式是一步一步创建一个复杂的对象,它允许用户可以只通过指定复杂对象的类型和内容就可以构建它们。用户不知道内部的具体构建细节。Builder模式是非常类似抽象工厂模式,细微的区别大概只有在反复使用中才能体会到。
UML结构图:
上图是Strategy 模式的结构图,让我们可以进行更方便的描述:
Builder:为创建一个Product对象的各个部件指定抽象接口。
ConcreteBuilder:实现Builder的接口以构造和装配该产品的各个部件,定义并明确它所创建的表示,提供一个检索产品的接口
Director:构造一个使用Builder接口的对象。
Product:表示被构造的复杂对象。ConcreateBuilder创建该产品的内部表示并定义它的装配过程。
10.2 优势
是为了将构建复杂对象的过程和它的部件解耦。注意:是解耦过程和部件。
因为一个复杂的对象,不但有很多大量组成部分,如汽车,有很多部件:车轮、方向盘、发动机,还有各种小零件等等,部件很多,但远不止这些,如何将这些部件装配成一辆汽车,这个装配过程也很复杂(需要很好的组装技术),Builder模式就是为了将部件和组装过程分开。
首先假设一个复杂对象是由多个部件组成的,Builder模式是把复杂对象的创建和部件的创建分别开来,分别用Builder类和Director类来表示。
首先,需要一个接口,它定义如何创建复杂对象的各个部件:
public interface Builder {
//创建部件A 比如创建汽车车轮void buildPartA();
//创建部件B 比如创建汽车方向盘void buildPartB();
//创建部件C 比如创建汽车发动机void buildPartC();
//返回最后组装成品结果 (返回最后装配好的汽车)
//成品的组装过程不在这里进行,而是转移到下面的Director类中进行.
//从而实现了解耦过程和部件
Product getResult();
}
用Director构建最后的复杂对象,而在上面Builder接口中封装的是如何创建一个个部件(复杂对象是由这些部件组成的),也就是说Director的内容是如何将部件最后组装成成品:
public class Director {
private Builder builder;
public Director( Builder builder ) {
this.builder = builder;
}
// 将部件partA partB partC最后组成复杂对象
//这里是将车轮 方向盘和发动机组装成汽车的过程
public void construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
}
}
Builder的具体实现ConcreteBuilder:
public class ConcreteBuilder implements Builder {
Part partA, partB, partC;
public void buildPartA() {
//这里是具体如何构建
}
public void buildPartB() {
//这里是具体如何构建
}
public void buildPartC() {
//这里是具体如何构建
}
public Product getResult() {
//返回最后组装成品结果
}
}
复杂对象:产品Product
public interface Product { }
复杂对象的部件:
public interface Part { }
调用Builder模式:
ConcreteBuilder builder = new ConcreteBuilder();
Director director = new Director( builder );
director.construct();
Product product = builder.getResult();
10.4 Builder模式的应用
在Java实际使用中,我们经常用到"池"(Pool)的概念,当资源提供者无法提供足够的资源,并且这些资源需要被很多用户反复共享时,就需要使用池。"池"实际是一段内存,当池中有一些复杂的资源的"断肢"(比如数据库的连接池,也许有时一个连接会中断),如果循环再利用这些"断肢",将提高内存使用效率,提高池的性能。修改Builder模式中Director类使之能诊断"断肢"断在哪个部件上,再修复这个部件。
11
—
观察者模式
基本概念:定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。将一个系统分割成一系列相互协作的类有一个很不好的副作用,需要维护相关对象间的一致性。否则为了维持一致性使各类紧密耦合,给维护、扩展和重用都带来不便。
使用场景:
观察者模式所做的工作其实就是在解除耦合,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响另一边的变化。
12
—
适配器模式
12.1 定义
基本概念:系统的数据和行为都正确,但接口不符时,应该考虑使用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。
12.2 角色职责
目标角色(Target):该角色定义把其他类转换为何种接口,也就是我们期望的接口。
源角色(Adaptee):希望转换的源角色,已经存在的,运行良好的只是需要适配的角色。
适配器角色(Adaptor):适配器模式的核心角色,其他两个角色都是已经存在的角色,适配器角色是需要新建立的,职责简单:通过继承或是类关联的方式把源角色转换成目标角色。
当遇到两个类所做的事情相同或相似,但是具有不同的接口时要使用它。
13
—
总结
以上介绍的11种设计模式在日常的开发工作中会经常使用到,也在很多开源框架如:Spring中就会看到很多的案例,灵活运用这些设计模式可以使我们的系统做到:易扩展、可维护性高、更稳健等。
参考文献:《大话设计模式》