设计模式学习笔记
开放封闭原则
软件实体(类,模块,函数等等)应该可以扩展,但是不可以修改
不要指望系统一开始确定需求之后就再也不会变化,要使得设计的软件要容易维护又不容易出问题就得多扩展,少修改
但模块没有绝对的封闭,所以就需要对设计的模块有哪些变化作出选择:猜测那些类最有可能发生变化,然后构造抽象来隔离那些变化。
对发生改变的类;立即作出反应,抽象出其功能然后利用继承、多态、复合等方法来隔离具体的实现细节和高层模块,使得之后对模块的修改都是基于抽
可维护,可扩展,可复用,灵活性好,拒绝不成熟的抽象和抽象本身一样重要。
其实就是对经常变化的类(需要经常更改的类)功能抽象出来,从对这个
依赖倒转原则
依赖倒转原则(Dependency Inversion Principle,简称DIP)是指将两个模块之间的依赖关系倒置为依赖抽象类或接口。
- 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
- 抽象不应该依赖于细节,细节应该依赖于抽象。
依赖:在程序设计中,如果一个模块a使用/调用了另一个模块b,我们称模块a依赖模块b。
高层模块与低层模块:往往在一个应用程序中,我们有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外有一些高层次的类,这些类封装了某些复杂的逻辑,并且依赖于低层次的类,这些类我们称之为高层模块。
依赖倒置(Dependency Inversion):面向对象程序设计相对于面向过程(结构化)程序设计而言,依赖关系被倒置了。因为传统的结构化程序设计中,高层模块总是依赖于低层模块。
一个良好的设计应该是系统的每一部分都是可替换的。如果“高层模块”过分依赖“低层模块”,一方面一旦“低层模块”需要替换或者修改,“高层模块”将受到影响;另一方面,高层模块很难可以重用。
DIP解决方案:在高层模块与低层模块之间,引入一个抽象接口层。
High Level Classes(高层模块) --> Abstraction Layer(抽象接口层) --> Low Level Classes(低层模块)
抽象接口是对低层模块的抽象,低层模块继承或实现该抽象接口。
这样,高层模块不直接依赖低层模块,而是依赖抽象接口层。抽象接口也不依赖低层模块的实现细节,而是低层模块依赖(继承或实现)抽象接口。
类与类之间都通过抽象接口层来建立关系。
怎么使用依赖倒置原则
1. 依赖于抽象
- 任何变量都不应该持有一个指向具体类的指针或引用。
- 任何类都不应该从具体类派生。
2. 设计接口而非设计实现
-
使用继承避免对类的直接绑定
-
抽象类/接口: 倾向于较少的变化;抽象是关键点,它易于修改和扩展;不要强制修改那些抽象接口/类
3. 避免传递依赖
- 避免高层依赖于低层
- 使用继承和抽象类来有效地消除传递依赖
依赖倒转减少了类间的耦合性、提高了系统稳定性,提高了代码可读性和可维护性,可降低修改程序所造成的风险。
单一职责原则
-
就一个类而言,应该仅有一个引起它变化的原因
-
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致错若的设计,当变化发生时,设计会遭受到意想不到的破坏。
-
在软甲设计时,就需要发现职责并把那些还在这互相分离。就是你能想到多余一个动机去改变一个类,那么这个类就具有多于一个的职责。
-
在编写代码的过程中,尽可能的让接口和方法保持单一职责,对项目后期的维护是由很大帮助的。
接口隔离原则
接口隔离原则(Interface Segregation Pinciple,ISP)是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。
所以设计接口时应该注意一下几点:
- 一个类对应另一个类的依赖应该建立在最小的接口之上。
- 建立单一接口,不要建立庞大臃肿的接口。
- 尽量细化接口,接口中的方法尽量少(适度,不要什么都塞进去),对于该分理处其他接口的方法要进行分离。
接口隔离原则符合我们常说的高内聚,低耦合的设计思想(不同模块依赖度低,同一模块内联系紧密),使类具有良好的可读性、可扩展性和可维护性。我们在设计接口时要多花时间去思考,要考虑业务模型,包括以后有可能发生变更的地方做一些预判。所以抽象、对业务模型的理解是非常重要的。
迪米特原则
-
迪米特原则(Law of Demeter LoD)是指一个对象应该对其他对象保存最少的了解,即最少知道原则(Least Knowledge Principle,LKP),尽量降低类与类之间的耦合度。
-
其强调之和朋友类交流,不和陌生人说话。
- 迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
- 但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
它强调以下两点:
-
从依赖者的角度来说,只依赖应该依赖的对象。
-
从被依赖者的角度说,只暴露应该暴露的方法。
里氏替换原则
如果每个类型T1的对象O1,都有类型成为T2的对象O2,使得T1定义的所有程序P在所有的对象O1都替换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类。(其实就是一个软件实体适用于一个父类,那么一定适用于其子类,所有引用父类的地方必须能够透明的使用其子类的对象,子类对象能够替换其父类对象并保持程序逻辑不变:子类可以扩展父类功能,但不能改变父类原有的功能)
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更加宽松。
- 当子类实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更加严格或者与父类一样。
里氏替换原则优点:
- 约束继承泛滥,是开闭原则的一种体现
- 加强程序的健壮性,同时变更时也可以做到更好的兼容性,提高程序的可维护性可扩展性,降低需求变更时引入的风险。
合成复用原则
- 合成复用原则是指尽量使用对象组合/继承而不是继承关系达到软件复用的目的、可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
- 继承叫做白箱复用,相当于吧所有的实现细节暴露给子类。组合/聚合称为黑线复用,我们是无法获取到类以外的对象的实现细节的。
- 典型的有依赖注入(不会为依赖创建一个单独的容器来管理,而是将其纳入依赖者中共同管理这个依赖)、装饰者模式(通过继承同一个接口,依赖实现了同一个接口的对象并对需要的方法直接调用依赖的这个对象的方法就行了,而不是通过继承的方式调用父类的方法,完成了对象之间的解耦)
简单工厂模式
SimpleDesgin
简单工厂类的工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说去除了与具体产品的依赖。
但是若想要添加功能,则需要在原有的类上进行修改。这样就违背了“开放-封闭原则”,于是有了工厂方法。
- 代码规范(命名规范, 容错)
- 面向对象编程(容易维护, 容易扩展, 容易复用)
- 分层(将业务逻辑和界面逻辑分开)
- 松耦合(将不同逻辑分离)
- 建立简单工厂(使用一个单独的类来创造实例)
用计算器作为实例:
Operation
public abstract class Operation {
private double lastResult = 0;
public abstract double doOperation(double numberA, double numberB);
public double getLastResult() {
return lastResult;
}
public void setLastResult(double lastResult) {
this.lastResult = lastResult;
}
}
OperationAdd
public class OperationAdd extends Operation {
@Override
public double doOperation(double numberA, double numberB) {
setLastResult(numberA + numberB);
return getLastResult();
}
}
OperationSub
public class OperationSub extends Operation {
@Override
public double doOperation(double numberA, double numberB) {
setLastResult(numberA - numberB);
return getLastResult();
}
}
OperationMul
public class OperationMul extends Operation {
@Override
public double doOperation(double numberA, double numberB) {
setLastResult(numberA * numberB);
return getLastResult();
}
}
OperationDiv
public class OperationDiv extends Operation{
@Override
public double doOperation(double numberA, double numberB) throws Exception {
if (numberB == 0){
throw new Exception("除数不能为0");
}
setLastResult(numberA / numberB);
return getLastResult();
}
}
工厂方法模式
工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类(依赖倒转原则)。
如果一个工厂类和分支耦合(上层工厂类型依赖于下层产品工厂的具体实现),那么我们就根据依赖倒转原则,将工厂类抽象出一个接口。这个接口只有一个方法,就是创建抽象产品的工厂方法。然后所有的要生产具体类的工厂去实现这个抽象出来的工厂接口,这样我们如果需要添加新的功能的话就只需要新增加产品和创建产品的工厂,这两个分别依赖于产品抽象接口和工厂抽象接口(这里就在遵循了开闭原则的公式将高层对下层的依赖倒转成了下层实现上层方法,上层依赖于上层抽象接口,降低了耦合的同时维持了封装创建对象的过程的优点)。
例子:
水果产品类:
public interface Fruit {
void eatFruit();
}
public class Apple implements Fruit{
@Override
public void eatFruit() {
System.out.println("吃了一个苹果");
}
}
public class Cherry implements Fruit{
@Override
public void eatFruit() {
System.out.println("吃了一个樱桃");
}
}
水果工厂类:
public interface FruitFactory {
Fruit creatFruit();
}
public class AppleFactory implements FruitFactory{
@Override
public Fruit creatFruit() {
return new Apple();
}
}
public class CherryFactory implements FruitFactory{
@Override
public Fruit creatFruit() {
return new Cherry();
}
}
抽象工厂模式
抽象工厂(Abstract Factory):提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类。(当同一个产品存在不同系列的操作时,我们在抽象工厂中添加不同系列的创建方法)
使用反射实现抽象工厂:
Mapper是操作数据的抽象接口,EmpMapper和DeptMapper是对两个表的数据操作抽象类,EmpMapperImpl是操作数据库中表的实现类,但数据库有两种所以存在两种实现。
Mapper:
public interface Mapper<T> {
void insert(T t);
T select();
}
DeptMapper:
public abstract class DeptMapper implements Mapper<Dept>{
}
public class DeptMapperMysqlImpl extends DeptMapper {
@Override
public void insert(Dept dept) {
System.out.println("insert into dept [Mysql]");
}
@Override
public Dept select() {
System.out.println("select from dept [Mysql]");
return new Dept();
}
}
public class DeptMapperOracleImpl extends DeptMapper {
@Override
public void insert(Dept dept) {
System.out.println("insert into mysql [oracle]");
}
@Override
public Dept select() {
System.out.println("select from dept [oracle]");
return new Dept();
}
}
EmpMapper:
public abstract class EmpMapper implements Mapper<Emp>{
}
public class EmpMapperMysqlImpl extends EmpMapper {
@Override
public void insert(Emp emp) {
System.out.println("insert into emp [Mysql]");
}
@Override
public Emp select() {
System.out.println("select from emp [Mysql]");
return new Emp();
}
}
public class EmpMapperOracleImpl extends EmpMapper {
@Override
public void insert(Emp emp) {
System.out.println("insert into emp [Oracle]");
}
@Override
public Emp select() {
System.out.println("select from emp [Oracle]");
return new Emp();
}
}
DataAccess:
public class DataAccess {
private static final String nameSpace = "com.design.abstractFactoryDesign.pro.";
private static String database = "Mysql";
public static EmpMapper getEmpMapper() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
String className = nameSpace + "EmpMapper"+ database + "Impl";
Class<?> clazz = Class.forName(className);
return (EmpMapper)clazz.newInstance();
}
public static DeptMapper getDeptMapper() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
String className = nameSpace + "DeptMapper"+ database + "Impl";
Class<?> clazz = Class.forName(className);
return (DeptMapper)clazz.newInstance();
}
public static String getDatabase() {
return database;
}
public static void setDatabase(String database) {
DataAccess.database = database;
}
}
创建一个主线程进行测试:
public class AbstractFactoryDesign {
public static void main(String[] args) {
try {
DeptMapper deptMapper = DataAccess.getDeptMapper();
deptMapper.insert(null);
deptMapper.select();
DataAccess.setDatabase("Oracle");
EmpMapper empMapper = DataAccess.getEmpMapper();
empMapper.insert(null);
empMapper.select();
}catch (Exception e){
e.printStackTrace();
}
}
}
输出:
insert into dept [Mysql]
select from dept [Mysql]
insert into emp [Oracle]
select from emp [Oracle]
单例模式
单例模式是对唯一实例的受控访问:让类自身保存类的唯一实例(私有化构造,变量保存唯一实例(存在两种保存方式:饿汉式,懒汉式))
懒汉式单例:在自己第一次被引用的时候才会将自己实例化(存在多线程的线程安全问题,需要加锁和判断是否为空)。
采用了双锁检查机制(DCL),并使用了volatile关键字禁止重排序
public class SlobSingleton {
volatile private static SlobSingleton instance;
private static final Object lock = new Object();
private SlobSingleton(){}
public static SlobSingleton getInstance(){
if (instance == null){
synchronized(lock){
if (instance == null){
return new SlobSingleton();
}
}
}
return instance;
}
}
使用静态内部类实现懒汉模式
class Demo{
static {
System.out.println("Demo static part");
}
private Demo(){
System.out.println("Demo");
}
public static void print(){
System.out.println("Demo static print");
}
public static Demo getInstance(){
return InnerDemo.demo; //InnerDemo只有在被调用的时候才会加载
}
private static class InnerDemo{
private static Demo demo = new Demo(); //先加载静态字段
static { //后加载静态代码块
System.out.println("InnerDemo static part");
}
}
}
因为类加载一次后会在方法区存放类信息,堆中存放对应Class对象,并且在初始化阶段对非final类变量进行初始赋值,这里就是内部类在加载时在堆中生成了外部类的静态变量。
外部类只有在调用内部类并且是第一次调用的时候内部类才会加载,在内部类加载时实例化了唯一的外部类引用。这样利用类加载机制实现了懒汉模式。
饿汉式单例:在自己被加载时就会将自己实例化(静态变量,在类初始化中的准备阶段进行空间分配,在初始化阶段进行赋值)。
public class HungerSingleton {
private static HungerSingleton INSTANCE = new HungerSingleton();
private HungerSingleton(){}
public static HungerSingleton getInstance(){
return INSTANCE;
}
}
注册式单例模式
注册式单例模式又称为登记式单例模式,就是将每个单例都登记到某一个地方,使用唯一的标识获取实例。分为两种:枚举式单例模式、容器式单例模式。
枚举式单例模式
- 避免了被类加载器加载多次,并且无法通过反射破坏枚举类型单例,因为发现是枚举类型的时候就会抛出异常。
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData(){
return data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
容器式单例
- 容器式单例适用于实例非常多的情况,便于管理。但它是非线程安全的!
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className){
synchronized (ioc){
if (!ioc.containsKey(className)){
Object o = null;
try {
o = Class.forName(className).newInstance();
ioc.put(className, o);
}catch (Exception e){
e.printStackTrace();
}
return o;
} else {
return ioc.get(className);
}
}
}
}
代理模式
- 代理模式: 为其他对象为提供一种代理以控制对这个对象的访问.
Super类定义了SubObject和Proxy的公用接口, 这样在需要使用SubObject的任何地方都可以使用Proxy.
应用场景:
- 远程代理: 远程对象的本地代表, 本地方法调用这个对象会被转发到远程中.
- 虚拟代理: 虚拟化开销大的对象
- 安全代理: 控制访问权限
- 智能指引: 调用真实的对象时, 处理另外一些事
例子:
Interface.class
public interface Interface {
void doSomething();
void doSomethingElse(String arg);
}
InterfaceImplFirst.class
public class InterfaceImplFirst implements Interface{
public void doSomething() {
System.out.println("InterfaceImplFirst: doSomething");
}
public void doSomethingElse(String arg) {
System.out.println("InterfaceImplFirst: doSomethingElse " + arg);
}
}
InterfaceImplTwo.class
public class InterfaceImplTwo implements Interface {
private final Interface interfaceImplFirst;
public InterfaceImplTwo(Interface interfaceImplFirst){
this.interfaceImplFirst = interfaceImplFirst;
}
public void doSomething() {
interfaceImplFirst.doSomething();
System.out.println("InterfaceImplTwo: doSomething");
}
public void doSomethingElse(String arg) {
interfaceImplFirst.doSomethingElse(arg);
System.out.println("doSomethingElse: doSomethingElse " + arg);
}
}
CGLib和JDK动态代理对比
-
JDK动态代理实现了被代理对象的接口,CGLib代理继承了被代理对象。
-
JDK动态代理和CGLib代理都在运行期生成字节码,JDK动态代理直接写Class字节码,CGLib代理使用ASM框架写Class字节码,CGLib代理实现更加复杂,生成代理类比JDK动态代理效率低。
-
JDK动态代理调用代理类方法通过反射机制,CGLib代理是通过FastClass机制直接调用方法的,CGLib代理的执行效率更高。
动态代理和静态代理的本质区别:
- 静态代理只能通过手动完成代理操作,如果被代理类增加了新的方法,代理类需要同步增加,违背了开闭原则。
- 动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循了开闭原则。
- 若动态代理需要对目标类增强逻辑进行扩展,结合策略模式,只需要新增策略便可以完成。
代理模式的优缺点:
- 优点:
- 代理模式能将代理对象与真实被调用对象分离。
- 在一定程度上降低了系统的耦合性,扩展性号好。
- 可以起到保护目标对象的作用。
- 可以增强目标对象的功能。
- 缺点:
- 代理模式会造成系统设计中类的数量增加。
- 在客户端和目标对象中增加一个代理对象,会导致请求处理速度变慢。
- 增加了系统的复杂度。
装饰器模式
装饰模式:“为已有的功能动态添加更多的的功能的一种方式”。
- 当系统需要新功能的时候可以选择向旧的类中添加新的代码,这些代码通常装饰了原有类的核心职责或主要行为。但这种方式在类中加入了新的字段和新的逻辑,从而增加了主类的复杂度。并且这些新加入的代码只有特定情况下才会执行。
- 装饰者模式将要添加(装饰如缓冲功能)的功能放在了一个类中,并让这个类包装要装饰的对象(如BufferInputStream包装InputStream,BufferReader包装了Reader)。这样客户端就可以有选择的,按顺序的自定义的使用包装类对象。
- 包装者模式将类中的装饰功能和核心功能区分开,去除了相关类中重复的装饰逻辑。
Component: 需要装饰的对象接口
ConcertComponent:装饰对象,可以给这个对象扩展功能
Decorator:继承Component,用于扩展Component的功能
ConcertDecoratorA:具体的装饰对象A,扩展了字段
ConcertDecoratorB:具体的装饰对象B,扩展了方法
例如:
DoClass作为父类抽象类, Courses课程类和Teacher老师类都继承DoClass类(或实现同一接口),且Courses含有DoClass对象的成员变量, 构造方法传入DoClass对象重写DoClass对象行为时调用DoClass方法(调用的实际上是传入类这里是Teacher的实现方法)。English英语类和Math数学类继承课程对象分别扩展一个方法,并对上课行为扩展了一些动作。
DoClass
public abstract class DoClass {
public abstract void doClass();
}
Teacher
public class Teacher extends DoClass{
private String name;
public Teacher(String name){
this.name = name;
}
@Override
public void doClass() {
System.out.print(name + " 今天上课 ");
}
//getter...setter...
}
Courses
public class Courses extends DoClass{
DoClass doClass;
public Courses(DoClass doClass){
this.doClass = doClass;
}
@Override
public void doClass() {
if (doClass != null){
doClass.doClass();
}
}
}
English
编写一个英语类,装饰老师对象扩展英语课的动作
public class English extends Courses{
public English(DoClass doClass) {
super(doClass);
}
@Override
public void doClass() {
doClass.doClass();
makeEnglish();
}
public void makeEnglish() {
System.out.print(" 上英语课 教英语 ");
}
}
Math
编写一个数学类,装饰老师对象扩展数学课的动作
public class Math extends Courses{
public Math(DoClass doClass) {
super(doClass);
}
@Override
public void doClass() {
doClass.doClass();
makeMath();
}
public void makeMath(){
System.out.println(" 上数学课 教数学 ");
}
}
写一个主方法Main
public class Main {
public static void main(String[] args) {
Teacher teacher = new Teacher("axianibiru"); //实例化装饰对象
English english = new English(teacher); //实例化英语包装类对装饰对象教师类进行包装
Math math = new Math(english); //实例化数学包装类对装饰对象教师类进行包装
math.doClass(); //调用包装后的方法
}
}
输出:
axianibiru 今天上课 上英语课 教英语 上数学课 教数学
生产者消费者模式
生产者消费者模式:一种并发同步模式(producer-consumer),由生产者进程/线程产生数据放入缓冲区,由消费者进程/线程取出数据进行计算。
在多线程开发中,如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完了才能产生数据。
如果消费者大于生产者那么消费者就会经常处于等待状态。
所以引入了缓冲区,供生产者存放数据,供消费者提取数据,起到一个数据缓存的作用用于平衡生产者和消费者之间的速度。同时也达到了解耦的作用(缓冲区作为中间层次分离了生产者和消费者的直接依赖)
原型模式
(Prototype)使用原型实例指定创建对象的种类, 并通过拷贝这些原型创建新对象。
-
原型模式就是从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节。
-
使用克隆Clone实现,分为浅拷贝和深拷贝
例子:
package com.design.prototype;
public class Jingubang {
public float h = 100;
public float d = 10;
public void big(){
h *= 2;
d *= 2;
}
public void small(){
h /= 2;
d /= 2;
}
}
package com.design.prototype;
import java.util.Date;
public class Monkey {
public int height;
public int wight;
public Date brithday;
}
package com.design.prototype;
import java.io.*;
import java.util.Date;
public class QiTianDaSheng extends Monkey implements Cloneable, Serializable {
public Jingubang jingubang;
public QiTianDaSheng(){
this.jingubang = new Jingubang();
this.brithday = new Date();
}
@Override
public Object clone(){
return this.deepClone();
}
public Object deepClone(){ //深克隆
try {
QiTianDaSheng qiTianDaSheng;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)){
objectOutputStream.writeObject(this);
}
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)){
qiTianDaSheng = (QiTianDaSheng) objectInputStream.readObject();
qiTianDaSheng.brithday = new Date();
}
return qiTianDaSheng;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
}
委派模式
委派模式的基本作用就是负责任务的调用和分配,跟代理模式很像,可以看做一种特殊情况下的静态的全权代理,但是代理模式注重过程而委派模式注重结果。
DispatcherServlet使用到了委派模式
例子:
public interface IEmployee {
void doing(String command);
}
public class EmployeeA implements IEmployee{
@Override
public void doing(String command) {
System.out.println("我是A,现在开始干" + command +"活");
}
}
public class EmployeeB implements IEmployee{
@Override
public void doing(String command) {
System.out.println("我是B,现在开始干" + command +"活");
}
}
public class EmployeeC implements IEmployee{
@Override
public void doing(String command) {
System.out.println("我是C,现在开始干" + command +"活");
}
}
public class Leader implements IEmployee{
private Map<String, IEmployee> targets = new HashMap<>();
public Leader(){
targets.put("加密", new EmployeeA()); //A负责加密
targets.put("登录", new EmployeeB()); //B负责登录
}
@Override
public void doing(String command) {
targets.get(command).doing(command); //接收command,并派发给负责人执行
}
}
public class Boss {
public void command(String command, Leader leader) {
leader.doing(command);
}
}
public class DelegateApp {
public static void main(String[] args) {
new Boss().command("登录", new Leader()); //发送登录指令,交由Leader进行处理,Leader根据传入的指令进行调度,将指令派发给目标类执行
}
}
输出:
我是B,现在开始干登录活
策略模式
策略模式是指定义了算法家族并分别封装起来,让他们之间可以互相替换,此模式使得算法的变化不会影响使用算法的用户。
应用场景:
- 系统中有很多类,他们的区别仅存在于行为不同。
- 一个系统需要动态地在几种算法中选择一种。
//工厂模式月策略模式
public class PayStrategy {
public static final String ALI_PAY = "AliPay";
public static final String DEFAULT_PAY = "AliPay";
private static Map<String, Payment> payStraregy = new HashMap<String, Payment>();
static {
payStraregy.put(ALI_PAY, new AliPay());
}
public static Payment get(String payKey) {
if (!payStraregy.containsKey(payKey)) {
return payStraregy.get(DEFAULT_PAY);
}
return payStraregy.get(ALI_PAY);
}
}
策略模式优点:
- 符合开闭原则。
- 策略模式可以避免多重条件语句,如if...else、switch语句。
- 使用策略模式可以提高算法的保密性和安全性。
缺点:
- 客户端必须知道所有策略,并自行决定使用哪一种策略类。
- 代码中会产生非常多的策略类,增加了代码的维护难度。
策略模式和委派模式Demo:
package com.axianibiru.springstudy.dispatcher;
import com.axianibiru.springstudy.controller.MemberController;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class DispatcherServlet extends HttpServlet {
private List<Handler> handlerMapping = new ArrayList<>();
@Override
public void init() throws ServletException {
try {
Class<?> memberController = MemberController.class;
handlerMapping.add(new Handler()
.setController(memberController.newInstance())
.setMethod(memberController.getMethod("getMemberById", new Class[]{String.class}))
.setUrl("/web/getMemberById.json"));
}catch (Exception e){}
}
public void doDispatcher(HttpServletRequest request, HttpServletResponse response) {
//获取用户请求的URL
//按照J2EE的标准一个URL对应一个Servlet
String uri = request.getRequestURI();
//通过URL获取handlerMapping(URL是策略常量)
Handler handle = null;
for (Handler h: handlerMapping) {
if (uri.equals(h.getUrl())) {
handle = h;
break;
}
}
//将具体的任务分发给Method(通过反射调用对应的方法)
Object object = null;
try {
object = handle.getMethod().invoke(handle.getController(), request.getParameter("mid"));
} catch (InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatcher(req, resp);
}catch (Exception e){
e.printStackTrace();
}
}
class Handler {
private Object controller;
private Method method;
private String url;
public Object getController(){
return controller;
}
public Handler setController(Object controller){
this.controller = controller;
return this;
}
public Method getMethod() {
return method;
}
public Handler setMethod(Method method) {
this.method = method;
return this;
}
public String getUrl() {
return url;
}
public Handler setUrl(String url) {
this.url = url;
return this;
}
}
}
模板方法模式
-
当使用继承的时候,并且肯定这个继承有意义,就应该要称为子类的模板,所有重复的代码都应该要上升到父类去,而不是让每个子类都去重复。
-
当我们要在某一细节层次一致的一个过程或一系列步骤,但其个别步骤在更详细的层次上的实现可能不同时,我们通常考虑使用模板方法模式来处理。
-
通过在细节上让子类重写,父类建立所有模板的方式完成对重复内容的复用。
-
所以定义是:顶一个一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤(在父类中给出逻辑框架,而逻辑的组成步骤在响应的抽象操作中,延迟到子类实现)。
- 模板方法通过吧不变行为搬到超类,去除子类中重复的代码类体现对它的优势。
- 当可变和不可变的行为在方法的子类中混合在一起的时候,不可变的行为就会在子类从重复出现。我们同股哟模板方法模式将这些行为搬到单一的地方,这样就帮助子类拜托重复的不变行为的纠缠。
- 一次性实现一个算法的不可变部分,并将可变部分留给子类来实现(例如IO中的一些方法)。
- 各子类中公共的行为被提取出来并集中到一个公共的父类中,从而避免代码重复。
利用模板模式重构JDBC操作业务场景:
这样DAO层只需要继承JdbcTemplate并实现RowMapper就行了
package com.design.template;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class JdbcTemplate {
private DataSource dataSource;
public JdbcTemplate(DataSource dataSource) {
this.dataSource = dataSource;
}
public List<?> executeQuery(String sql, RowMapper<?> rowMapper, Object[] values) {
try {
//获取连接
Connection conn = this.getConnection();
//创建语句集
PreparedStatement pstm = this.createPreparedStatement(conn, sql);
//执行语句集
ResultSet rs = this.executeQuery(pstm, values);
//处理结果集
List<?> result = this.paresResultSet(rs, rowMapper);
//关闭结果集
this.closeResultSet(rs);
//关闭语句集
this.closeStatement(pstm);
//关闭连接
this.closeConnection(conn);
return result;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
protected void closeResultSet(ResultSet rs) throws SQLException {
if (rs != null){
rs.close();
}
}
protected void closeConnection(Connection conn) throws SQLException {
if (conn != null){
conn.close();
}
}
protected void closeStatement(PreparedStatement pstm) throws SQLException {
if (pstm != null){
pstm.close();
}
}
protected ResultSet executeQuery(PreparedStatement pstm, Object[] values) throws SQLException {
for (int i = 0; i < values.length; i++) {
pstm.setObject(i, values[i]);
}
return pstm.executeQuery();
}
protected List<?> paresResultSet(ResultSet rs, RowMapper<?> rowMapper) throws Exception {
List<Object> result = new ArrayList<Object>();
int rowNum = 1;
while (rs.next()){
result.add(rowMapper.mapRow(rs, rowNum++));
}
return result;
}
protected PreparedStatement createPreparedStatement(Connection conn, String sql) throws SQLException {
return conn.prepareStatement(sql);
}
protected Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
RowMapper接口:
package com.design.template;
import java.sql.ResultSet;
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws Exception;
}
优点:
- 利用模板模式将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
- 将不同的代码放到不同的子类中,通过对子类的扩展,增加新的行为,可以提高代码的扩展性。
- 把不变的行为写在父类中,去除了子类重复的代码,提供了一个很好地代码复用平台,符合开闭原则。
缺点:
- 每个抽象类都需要一个子类来实现,导致了类的数量增加。
- 类数量的增加间接地增加了系统的复杂性。
- 因为继承关系自身的缺点,如果父类添加新的抽象方法,所有子类都需要修改一遍。
适配器模式
将一个类的接口转换成客户端希望的另一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。
- 当系统的数据和行为都正确,但接口不符合时,我们应该考虑使用适配器,目的是使控制范围之外的一个原有对象与某个接口匹配。适配器模式主要应用于希望复用一些显存的类,但是接口又与复用环境要求不一致的情况。
- 想使用一个类,但她的接口,也就是她的方法和要求的不相同时,就应该考虑使用适配器模式(已经存在的类方法和需求不匹配,但是方法结果相同或者相似)。
- 两个类所做的事情相同或者相似,但是具有不同的接口时使用它。
- 在双方都不太容易修改的时候再使用适配器模式(适配器模式不是软件初始阶段考虑的设计模式,而是随着软件的发展,由于不同产品,不同厂家造成功能类似而接口不同的问题的解决方案)。
- 只有预防接口不同的问题,防止不匹配的问题发生:在只有小的接口不统一的问题发生时,及时重构,问题不至于扩大;只有碰到无法改变原有设计和代码的情况时,才考虑适配(事后控制不如事中控制,事中控制不如事前控制)。
- 策略模式主要用于将一系列家族封装(依赖于接口),适配器模式讲究对于不同接口/类之间的兼容(适配器的实现不依赖于接口)。
Spring DispatcherServlet中的适配器模式:
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) { //使用简单工厂模式进行适配器的选择
if (adapter.supports(handler)) { //查看适配器是否匹配
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
优点:
- 能提高类的透明性和复用性,现有的类会被复用当不需要改变。
- 目标类和配适器类解耦(使用Object传入,真实的处理在目标类中(强转为目标类就可以复用目标类的方法),传入高层抽象,不依赖于具体实现遵循了依赖倒转原则,完成了对于原有类方法的扩展/复用)。
- 目标类和适配器解耦,提高程序的扩展性。
- 在很多业务场景中符合开闭原则。
缺点:
- 在适配器代码编写过程中需要进行全面考虑,可能会增加系统的复杂性。
- 增加了代码的阅读难度,降低了代码的可读性,过多使用适配器会使得系统的代码变得混乱。
装饰器模式和适配器模式的对比:
- 装饰者模式和配适器模式都属于包装器模式(Wrapper Pattern)
- 装饰者模式主要用于解决父类同一的问题,配置器主要用于解决兼容问题。
装饰者模式 | 适配器模式 | |
---|---|---|
形式 | 属于一种特别的配适器模式 | 没有承继关系,装饰者模式有层级关系 |
定义 | 装饰者和被装饰者实现同一个接口,主要目的是扩展之后依然保留OOP关系。 | 适配器进和被适配者没有必然的联系,通常采用继承或者代理的形式进行包装。 |
关系 | 满足is-a的关系(a是特殊的b,所以a继承b) | 满足has-a(a包括b,但不是b) |
功能 | 注重覆盖、扩展 | 注重兼容、转换 |
设计 | 前置考虑 | 后置考虑 |
观察者模式
观察者模式又叫做发布-订阅模式(Publish/Subscribe)模式
观察者模式定一个一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象再状态发生改变时,会通知所有观察者对象,使他们能够自动更新自己。
- 观察者模式将相互依赖的对象抽象成两方面,一方面依赖于另一方面,这时使用观察这模式可以将这两者独立封装在独立的对象中各自独立地改变和复用。
- 观察者模式所做的工作就是解除耦合。让耦合的双方都依赖于抽象,而不是依赖于具体。使得各自的变化都不会影响到另一边的变化。
- 观察者和模式让多个对象同时监听一个对象主题,当主体对象发生变化时,她的所有依赖者(观察者)都会收到通知并更新,属于行为型模式。
- 观察者模式有时又会被叫做发布订阅模式。观察者模式主要用于在关联行为之间建立一套触发机制的场景。
Spring中的监听机制:
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
优点:
- 在观察者和被观察者之间建立了一个抽象的耦合
- 观察者模式支持广播通信
缺点:
- 观察者之间有过多的细节依赖、时间消耗多、程序的复杂性更高
- 使用不当会出现循环调用。