文章目录
1 六项设计原则
-
1、开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 -
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 -
3、依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。 -
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 -
5、迪米特法则(最少知道原则)(Demeter Principle)
为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 -
6、合成复用原则(Composite Reuse Principle)
原则是尽量使用合成/聚合的方式,而不是使用继承
2 设计模式
1. 创建型模式
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。
- 工厂模式(Factory Pattern):一个对象接口(汽车工厂提车)
- 抽象工厂模式(Abstract Factory Pattern):一系列对象接口(产品族中包含产品族)
- 单例模式(Singleton Pattern):仅有一个实例(多线程操作同一个文件、singleton实现)
- 建造者模式(Builder Pattern):一步一步按顺序构造最终的对象(“肯德基套餐”、StringBuilder)
- 原型模式(Prototype Pattern):通过拷贝一个现有对象生成新对象(细胞分裂、Object clone()方法)
2. 结构型模式
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。
- 适配器模式(Adapter Pattern):两个不兼容的接口之间的桥梁(电压转换、 LINUX 运行 WINDOWS 程序、jdbc)
- 桥接模式(Bridge Pattern):抽象化与实现化解耦(灵魂与肉体、开关与功能)
- 过滤器模式(Filter、Criteria Pattern):过滤特定对象(字符集过滤器)
- 组合模式(Composite Pattern):一组相似的对象当作一个单一的对象(算术表达式=操作数+操作符+另一个操作数(操作数+操作符+另一个操作数))
- 装饰器模式(Decorator Pattern):现有的对象添加新的功能,不改变其结构(画+玻璃+画框)
- 外观模式(Facade Pattern):隐藏系统的复杂性,提供可访问系统的接口(JAVA 的三层开发模式)
- 享元模式(Flyweight Pattern):共享技术减少创建对象的数量(常量池、数据库池)
- 代理模式(Proxy Pattern):一个类代表另一个类的功能(Spring的AOP、快捷方式)
3. 行为型模式
这些设计模式特别关注对象之间的通信。
- 责任链模式(Chain of Responsibility Pattern):为请求创建了一个接收者对象的链(WEB中encoding处理、拦截器、过滤器)
- 命令模式(Command Pattern):以命令的形式包裹在对象中(GUI 中每一个按钮都是一条命令、模拟 CMD、封装sql)
- 解释器模式(Interpreter Pattern):评估语言的语法或表达式的方式(编译器、运算表达式计算)
- 迭代器模式(Iterator Pattern):顺序访问一个聚合对象中各个元素(iterator)
- 中介者模式(Mediator Pattern):用一个中介对象来封装一系列的对象交互(MVC中的C)
- 备忘录模式(Memento Pattern):不破坏对象封装的前提下,捕获内部状态,并在该对象之外保存(存档、ctri + z、后退、事务管理)
- 观察者模式(Observer Pattern):一种一对多的依赖关系中一个对象的状态发生改变时,所有依赖于它的对象被通知和自动更新(拍卖师观察最高标价,然后通知给其他竞价者竞价)
- 状态模式(State Pattern):类的行为是基于它的状态改变的(运动员状态与行为、不同身份不同行为)
- 空对象模式(Null Object Pattern):一个空对象取代 NULL 对象实例的检查
- 策略模式(Strategy Pattern):一个类的行为或其算法可以在运行时更改(不同出行方式、锦囊妙计)
- 模板模式(Template Pattern):一个抽象类公开定义了执行它的方法的方式/模板(抽象子类的公用方法、Spring中事务Session中的方法)
- 访问者模式(Visitor Pattern):对一个对象结构中的对象进行很多不同的并且不相关的操作,避免这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类(UI、拦截器与过滤器)
4. J2EE 模式
这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。
-
MVC 模式(MVC Pattern): Model-View-Controller(模型-视图-控制器) 模式。用于应用程序的分层开发。
-
业务代表模式(Business Delegate Pattern):减少通信或对表示层代码中的业务层代码的远程查询功能。(客户端(Client)、业务代表(Business Delegate)、查询服务(LookUp Service)、业务服务(Business Service))
-
组合实体模式(Composite Entity Pattern):一个组合实体是一个 EJB 实体 bean,代表了对象的图解(组合实体(Composite Entity)、粗粒度对象(Coarse-Grained Object)、依赖对象(Dependent Object)、策略(Strategies))
-
数据访问对象模式(Data Access Object Pattern):数据访问对象模式(Data Access Object Pattern)或 DAO 模式用于把低级的数据访问 API 从高级的业务服务中分离出来(数据访问对象接口(Data Access Object Interface)、数据访问对象实体类(Data Access Object concrete class)、模型对象/数值对象(Model Object/Value Object))
-
前端控制器模式(Front Controller Pattern):集中的请求处理机制,所有的请求都将由单一的处理程序处理。(前端控制器(Front Controller)、调度器(Dispatcher)、视图(View) )
-
拦截过滤器模式(Intercepting Filter Pattern):对应用程序的请求或响应做一些预处理/后处理(过滤器(Filter)、过滤器链(Filter Chain)、Target、过滤管理器(Filter Manager)、客户端(Client))
-
服务定位器模式(Service Locator Pattern):JNDI 查询定位各种服务(服务(Service)、Context / 初始的 Context 、服务定位器(Service Locator)、缓存(Cache) 、客户端(Client))
-
传输对象模式(Transfer Object Pattern):从客户端向服务器一次性传递带有多个属性的数据(业务对象(Business Object)、传输对象(Transfer Object)、客户端(Client))
3 Spring中涉及的设计模式:
- 简单工厂(非23种设计模式中的一种):Spring中的BeanFactory
- 工厂方法:实现了FactoryBean接口的bean是一类叫做factory的bean
- 单例模式:Spring中依赖注入的Bean实例默认是单例的。
- 适配器模式:SpringMVC中的适配器HandlerAdatper。
- 装饰器模式:Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
- 代理模式:AOP底层,就是动态代理模式的实现。
- 观察者模式:listener的实现。具体实现:事件源、事件、事件监听器。
- 策略模式:Spring框架的资源访问Resource接口。
- 模版方法模式:是模板方法模式和回调模式的结合,父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现。
4 JDK涉及的设计模式
-
Singleton(单例):保证类只有一个实例;提供一个全局访问点
(1)Runtime
(2)NumberFormat -
Factory(静态工厂):(1)代替构造函数创建对象(2)方法名比构造函数清晰
(1)Integer.valueOf
(2)Class.forName -
Factory Method(工厂方法):子类决定哪一个类实例化
Collection.iterator方法 -
Abstract Factory(抽象工厂):创建某一种类的对象
(1)java.sql包
(2)UIManager(swing外观) -
Builder(构造者):(1)将构造逻辑提到单独的类中(2)分离类的构造逻辑和表现
DocumentBuilder(org.w3c.dom)
类图: -
Prototype(原型):(1)复制对象(2)浅复制、深复制
Object.clone;Cloneable -
Adapter(适配器):使不兼容的接口相容
(1)java.io.InputStreamReader(InputStream)
(2)java.io.OutputStreamWriter(OutputStream) -
Bridge(桥接):将抽象部分与其实现部分分离,使它们都可以独立地变化
java.util.logging中的Handler和Formatter -
Composite(组合):一致地对待组合对象和独立对象
(1)org.w3c.dom
(2)javax.swing.JComponent#add(Component) -
Builder(装饰器):为类添加新的功能;防止类继承带来的爆炸式增长
(1)java.io包
(2)java.util.Collections#synchronizedList(List) -
Façade(外观):(1)封装一组交互类,一致地对外提供接口(2)封装子系统,简化子系统调用
java.util.logging包 -
Flyweight(享元):共享对象,节省内存
(1)Integer.valueOf(int i);Character.valueOf(char c)
(2)String常量池 -
Proxy(代理):(1)透明调用被代理对象,无须知道复杂实现细节(2)增加被代理类的功能
动态代理;RMI -
Iterator(迭代器):将集合的迭代和集合本身分离
Iterator、Enumeration接口 -
Observer(观察者):通知对象状态改变
(1)java.util.Observer,Observable
(2)Swing中的Listener -
Mediator(协调者):用于协调多个类的操作
Swing的ButtonGroup -
Template method(模板方法):定义算法的结构,子类只实现不同的部分
ThreadPoolExecutor.Worker -
Strategy(策略):提供不同的算法
ThreadPoolExecutor中的四种拒绝策略 -
Chain of Responsibility(责任链):请求会被链上的对象处理,但是客户端不知道请求会被哪些对象处理
(1)java.util.logging.Logger会将log委托给parent logger
(2)ClassLoader的委托模型 -
Command(命令):(1)封装操作,使接口一致(2)将调用者和接收者在空间和时间上解耦合
Runnable;Callable;ThreadPoolExecutor -
Null Object(空对象):不需每次判空,对待空值,如同对待一个相同接口的对象
Collections.EMPTY_LIST -
State(状态):将主对象和其状态分离,状态对象负责主对象的状态转换,使主对象类功能减轻
未发现 -
Visitor(访问者):异构的类间添加聚合操作;搜集聚合数据
未发现 -
Interpreter(解释器):用一组类代表某一规则
java.util.regex.Pattern -
Memento(备忘录):保持对象状态,需要时可恢复
未发现
5 各种工厂模式
-
简单工厂模式
- 虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户端程序从具体类解耦。
- 工厂类中实现方法。
- 每次要新增创建的对象时必须去修改工厂类本身,这意味着这个工厂类很容易膨胀,同时这违反了开放封闭原则。
-
静态工厂模式
- 静态方法定义一个“简单工厂”。
-
工厂方法模式
- 即工厂模式,把对象的创建委托给子类,子类实现工厂方法来创建对象。
- 实例化的过程下沉到具体子工厂,需要新增创建的对象时,就新增一个对应的工厂。
-
抽象工厂模式
- 抽象工厂模式提供一个接口(Interface), 用于创建具体或依赖对象的家族, 而不需要明确指定具体类。
- 从抽象工厂中派生出具体工厂, 这些工厂生产相同的产品, 但是产品的实现不同。
- 抽象工厂的方法经常以工厂方法的方式实现。
6 JAVA单例实现的方式
一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。上述所说都是线程安全的实现,文章的第一种方法不算正确的写法。
1懒汉式,线程不安全(static )
当被问到要实现一个单例模式时。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒加载模式,问题:当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。
2懒汉式,线程安全(static +synchronized方法)
多个实例问题
——》》将整个 getInstance() 方法设为同步(synchronized)。
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
问题:它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时(其他调用用第一次创建的实例就行)。
——》》这就引出了双重检验锁。
3双重检验锁(两次检查null中间同步块加锁)
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
public static Singleton getSingleton() {
if (instance == null) { //Single Checked
synchronized (Singleton.class) {
if (instance == null) { //Double Checked
instance = new Singleton();
}
}
}
return instance ;
}
问题:在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(有实例,但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
——》》将 instance 变量声明成 volatile 就可以了。
public class Singleton {
private volatile static Singleton instance; //声明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
不是因为 volatile 的可见性。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java
内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用volatile。
——》》更好的实现线程安全的单例模式的办法。
4饿汉式 static final field
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
public class Singleton{
//类加载时就初始化
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
问题:它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
——》》更好的写法
5静态内部类(private static class)⭐
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
——》》更好写法
6枚举 Enum⭐
用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。
public enum EasySingleton{
INSTANCE;
}
我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。