GOF23--23种设计模式(一)
一.什么是设计模式
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决一系列特定问题的套路。
它不是语法规定,而是一套用来提高代码复用性,可读性,可维护性,稳健性,安全性的解决方案
设计模式的雏形:
1995年,GOF(Gang of Four,四人/四人帮)合作出版了《设计模式:可复用的面向对象软件的基础》一书,共收集了23种设计模式,从此有了设计模式的历程碑,人称【GoF设计模式】
设计模式的本质是面向对象设计的实际应用,是对类的封装,继承,多态,以及类的关联,和组合关系的充分理解
使用设计模式有以下优点:
- 可以提高程序员的思维能力,编程和设计能力
- 使得程序设计更加标准化,代码编制更容易加工,从而缩短软件开发周期
- 使设计代码重用性高,可读性高,可靠性高,灵活性,可维护性强
OOP的七大原则:
开闭原则:对扩展开放,对修改关闭
- 指的是在面向对象的设计中,当有新的需求时,不会优先改变源码,而是通过其它方式(继承,多态等)在源码的基础上拓展新功能
里氏替换原则:继承必须确保父类的所拥有的性质在子类依旧成立
- 指的是在程序设计中,对于子类继承父类,子类中父类的属性和方法都能正常使用,子类需要新的需求就自己写,不要直接重写父类的方法,如果为了重写父类已有的方法而继承,对于程序的复用性会大打折扣
依赖倒置原则:面向接口编程,不要面向实现编程
- 指的是在程序设计中,不应该力求于怎么实现这个功能,应该先思考有那些方法,各自负责什么,实现的细节交由实现类,抽象功能交给接口,更深层次就是面向抽象编程,不要面向实现编程
单一职责原则:控制类的粒度,将对象解耦,提高其内聚性
- 指的是一个类就专注于实现好一个功能就行了,就像一个方法就实现一个细节一样,如用户登录,想要一个方法负责密码校对又负责检测用户名是否存在,就是一个方法干了多件事,可以把检测用户是否存在抽象为另一个方法,然后调用它,这样类的粒度就低了,粒度越高,代码越可能出现问题
接口隔离原则:为各个类建立它们专需的接口
- 指的是在程序设计的时候,需要对一个接口对应一个或多个实现类,它们负责的模块可以很小,但是需要专一,不要多个功能都冗余在一个接口内部,应该实现专一功能,然后可以多个实现类来实现更小的细节
迪米特法则:只与你的朋友交谈,不和陌生人说话
- 指的是两个类需要交流时应该通过一个中间类,不要让它们两个类直接交流,如用户登录时需要密码校对(A类的功能),校对前需要进行用户名检测是否村在(B类的功能),它们之间有耦合关系,但是程序设计中不能将B类塞到A类中,而是需要一个C类,将B类和A类组合,然后实现此功能,好处是,A类B类保持纯粹,坏处是多了一个开销C类
合成复用原则:尽量优先使用组合和聚合的方式实现类之间的关系,其次才考虑继承来实现
- 指的是在类的关系中多用组合和聚合的方式设计类,组合优于继承,如果你只想使用父类的方法,而很少或根本不再设计新的方法属性,就肯定要使用组合,如果是需要大面积更改父类方法,或者重构父类,则使用继承
注意:OOP的七大原则,多用于设计阶段,需要分清设计和实现的区别
二.工厂模式
实现了创建者和调度者的分离
原来的调度者即是创建者,类就在自己的项目中,且可看源码,所以要使用的时候可以直接new出来,这种方式创建对象需要自己十分的了解这个类,如需要哪些参数,清楚内部的实现细节
在大型项目的设计中,都是面向接口编程,对于调度者,它只知道此接口的内容,和有一些实现类,并不知道实现类的具体细节,如果自己创建对象,很大概率会被抽象接口给整蒙,所以工厂模式出现了,它用于实现对象的创建,创建对象的细节都由工厂模式解决(也就是架构师),普通开发者只用知道自己使用的实现类是那个工厂提供的,然后在工厂内拿取对象,不必自己创建,而只是利用工厂调度
详细工厂的分类:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
理论上,工厂模式满足:开闭原则,依赖倒转原则,迪米特法则
但是,实际工作中以效率和业务开发为主,不一定完全满足,这取决于效率和理论的冲突
工厂模式的核心本质:
实例化对象不在使用new关键字,用工厂代替
将选择实现类,创建实现类对象统一管理和控制,从而将调用者和实现类解耦
简单工厂模式
简单工厂模式也叫静态工厂模式,指的是工厂中的代码块都是写死的,动态的拓展类需要在工厂中新增代码块来完成对象的创建
接口,Animal:
public interface Animal { void getName(); }
通过此接口拓展出的实现类:
- cat类
public class Cat implements Animal{ @Override public void getName() { System.out.println("猫类,实现Animal接口"); } }
- dog类
public class Dog implements Animal{ @Override public void getName() { System.out.println("狗类,实现Animal类"); } }
普通的创建对象的方式,通过new关键字实现:
//普通的创建对象方式 Cat cat = new Cat(); Dog dog = new Dog();
这种方式使用的前提是创建者对类的内部结构要熟悉,清楚需要什么参数才能创建对象,我们例子的实现类简单,肯定用new关键字很适用,但是这一期主要讲工厂模式,所以我们看看工厂模式怎么创建对象
简单构造一个简单工厂来创建对象(重理解):
public class AnimalFactory { public static Animal getAnimal(String name){ if (name.equals("cat")){ return new Cat(); } else if (name.equals("dog")) { return new Dog(); }else { return null; } } }
如上就是普通工厂的写法,它是讲已有的类先写入工厂中,这就导致工厂的实现类被写死了,如果新增一个拓展类,就需要改变普通工厂的源码,这很显然不符合开闭原则
工厂模式拿取对象:
//工厂模式创建对象 Animal cat1 = AnimalFactory.getAnimal("cat"); Animal dog1 = AnimalFactory.getAnimal("dog");
新建一个Mouse实现类:
public class Mouse implements Animal { @Override public void getName() { System.out.println("老鼠类,实现于Animal类"); } }
需要改变普通工厂模式的代码:
public class AnimalFactory { public static Animal getAnimal(String name){ if (name.equals("cat")){ return new Cat(); } else if (name.equals("dog")) { return new Dog(); }else if (name.equals("mouse")) { //新增的拓展实现类 return new Mouse(); }else { return null; } } }
每次新增拓展类都需要改变普通工厂类的原因:普通工厂是拿取对象的必经之路,是和其它实现类的唯一联系
普通工厂模式生产对象略图:
工厂方法模式
工厂方法模式支持实现类的横向拓展,它在普通工厂模式的基础上,增加工厂模式接口,对于每个实现类有专门的接口,
也就是说实现类,实现接口的具体细节,而工厂实现类实现的是工厂模式的创建对象
- 优点是可以横向拓展业务,不需要改变已经有的工厂模式来融入
- 缺点是代码量直接翻倍,冗余比较大
接口Animal:
public interface Animal { void getName(); }
Animal工厂接口:
public interface AnimalFactory { Animal getAnimal(); }
接口实现类:
public class Cat implements Animal { @Override public void getName() { System.out.println("猫类,实现Animal接口"); } }
工厂接口实现类:
public class CatFactory implements AnimalFactory{ @Override public Animal getAnimal() { return new Cat(); } }
如上,每个实现类都有它专有的工厂实现类,使得每个实现类都是专门的工厂来加工的,它们各个工厂实现类都是独立存在的互相解耦,所以要创建对象现在就需要去找它们对应的工厂
这样构建工厂的好处是,横向的新增业务,如果现在新增一个业务只需要实现类实现Animal接口,它对应的工厂实现工厂接口,和其它工厂是独立存在的,不需要改变已有的工厂
能实现横向拓展的关键在于,接口和工厂接口都不是关键路径了,而是约束实现类的组成
工厂方法模式创建对象:
//方法工厂模式拿取对象 Animal cat = new CatFactory().getAnimal(); Animal dog = new DogFactory().getAnimal();
工厂方法模式生产对象略图:
三.抽象工厂模式
抽象工厂模式也是工厂模式的一种,但是它的特点和普通工厂模式,工厂方法模式的机制都是不同的
抽象工厂模式围绕一个超级工厂,其它工厂的创建都是由这个超级工厂约束的
定义:抽象工厂模式提供了一个创建一系列相关或相互依赖对象的接口,无需指定它们具体的类
优点:
- 具体产品在应用层隔离,无需关心创建细节
- 将一个系列的产品统一到一起实现
缺点:
- 产品簇新增产品困难
- 增加了系统抽象性和理解难度
产品接口:
phone
//产品接口,具体的实现细节交给厂商 public interface PhoneProduct { void getPhoneName(); void getNumber(); void getProduct(); }
router
//产品接口,具体的实现细节交给厂商 public interface RouterProduct { void getRouterName(); void getRouterNumber(); void getRouterProduct(); }
抽象工厂接口,工厂都需要实现此接口:
//抽象工厂,所有的工厂都需要实现这个超级工厂 public interface AbstractFactory { PhoneProduct phone(); RouterProduct router(); }
普通工厂:
XiaoMi:
public class MiFactory implements AbstractFactory{ @Override public PhoneProduct phone() { return new MiPhone(); } @Override public RouterProduct router() { return new MiRouter(); } }
HuaWei:
public class HWFactory implements AbstractFactory{ @Override public PhoneProduct phone() { return new HuaWeiPhone(); } @Override public RouterProduct router() { return new HuaWeiRouter(); } }
抽象工厂模式生产对象略图:
三种工厂模式总结
简单工厂模式:虽然某种程度上不符合设计模式,但是实际应用最多
工厂方法模式:不修改已有类的情况下,通过新增工厂实现类的拓展
抽象工厂模式:不可以新增产品,但是可以新增产品簇或者说,不建议修改已经写好的抽象工厂接口,但是实现抽象工厂接口的普通工厂可以横向拓展
四.单例模式
单例模式指的是在创建对象的时候,只允许全局存在一个对象,从而达到资源共享的目的
实现单例模式的方式一共有两种:
- 饿汉式单例
- 懒汉式单例
饿汉式单例
饿汉式单例的特点是将一个类的构造器私有化,不让外部的程序手动的创建对象
而这个类的对象则使用静态方法获取,由程序加载初始化的时候就开始创建,然后伴随程序的结束为止
//饿汉式单例模式 public class HungryInstance { //私有化构造器,不允许外部类任意创建对象 private HungryInstance(){ } //创建静态对象,在类初始化时就被创建对象 private static HungryInstance hungry=new HungryInstance(); //外部类利用方法拿取对象,不由外部类自主创建对象 public static HungryInstance getHungry(){ return hungry; } }
饿汉式单例模式有一个缺点,也就是此类的对象是静态的,它和程序加载顺序有关系,静态的代码块会和程序初始化一起加载,所以有可能此类如果所需空间很大但是使用不平凡,会白占很多空间
如我们此类需要申请一片内存空间:
private String[] s1=new String[1000]; private String[] s2=new String[1000]; private String[] s3=new String[1000]; private String[] s4=new String[1000];
如上,这片空间会在程序初始化就被占用,且一直存在到程序结束,如果这个单例本身使用很少,内存开销就很不合算
懒汉式单例
懒汉式单例也需要将构造器私有,避免外部类创建对象
懒汉式不是再使用静态属性来创建对象,而是通过方法调用,由方法创建
如果没使用此方法就并不会存在此对象,如果使用了此方法就创建一个对象
然后加一个检测机制,调用此方法时,如果对象存在就直接返回对象,避免创建,如果不存在,则当场创建一个
//懒汉式单例 public class LazyInstance { //私有化构造器,避免外部类创建对象 private LazyInstance(){ } private LazyInstance lazy; //y由调用方法创建对象,被调用才会被创建,没被调用对象就不存在 public LazyInstance getLazy(){ if (lazy==null){ lazy = new LazyInstance(); return lazy; }else { return lazy; } } }
懒汉式单例也有自己的一个问题,那就是多线程的情况下,检测机制太简单,单例会被破坏
原因是上面方法创建对象的操作不是原子性,创建对象的过程:1.分配内存空间,2.执行构造方法,初始化对象,3.把对象指向这个空间
创建对象的顺序是123,132都可能,如果多个线程同时来拿对象只有还没进行到第3步,都会默认没有对象,但实际情况是已经有线程正在创建了,所以就会导致多个线程创建了多个对象
解决方式,加锁(synchronized)
//由调用方法创建对象,被调用才会被创建,没被调用对象就不存在 public static LazyInstance getLazy() { if (lazy == null) { //加上线程同步机制,当对象不存在时将此类资源锁住 synchronized (LazyInstance.class) { if (lazy == null) { lazy = new LazyInstance(); return lazy; } } } return lazy; }
加上同步机制后,在创建对象时,会将类资源锁住,先获得锁的线程就就去创建对象,其它线程只能等待此线程释放锁
当对象创建完成后,其它线程先后获得锁,但是对象此时已经被最先拿到锁的线程创建了,所以其它线程都不能创建对象而是直接返回已经创建好的对象
静态内部类单例
这是使用了Java静态内部类的特点,它可以直接拿到外部类的静态资源,然后又不会直接被初始化加载,它和饿汉式有异曲同工之妙
饿汉式是在程序加载时就初始化一个对象出来,而它需要在被调用时才能拿到对象,由于创建对象的类中,又是final修饰,所以在调用方法的时候不会多创建对象
//静态内部类 public class StaticClass { //私有化构造器 private StaticClass(){ } //返回静态内部类的属性 public static StaticClass getInstance(){ return InnerClass.sc; } //静态内部类负责创建外部类的对象 public static class InnerClass{ private static final StaticClass sc = new StaticClass(); } }
上面三种方式的缺点
只要有反射机制存在,以上三种方式创建对象都是不安全的
反射机制使得私有的构造器依旧可以被拿到,反射机制面前就没有私有的属性了,我们可以使用反射机制来创建对象
//通过反射拿取类的构造器 Constructor<LazyInstance> lazy = LazyInstance.class.getDeclaredConstructor(null); //设置构造器的熟悉为可访问 lazy.setAccessible(true); //通过反射拿取构造器创建对象 LazyInstance lazy1 = lazy.newInstance(); LazyInstance lazy2 = lazy.newInstance(); //展示hashcode System.out.println(lazy1);//LazyInstance@4554617c System.out.println(lazy2);//LazyInstance@74a14482
如上,通过反射机制将构造器再次变为公有属性以后,已经可以通过外部类继续创建对象
所以这种基于类的单例模式大多都是不安全的,关键在于Java的反射机制使得构造器无法真正的私有化
但是如果有能拒绝反射机制的方式,阁下又如何应对呢?接下来的枚举类值得一看
枚举类单例
枚举类的特点:
枚举类的构造器都是私有的(无论是否显式表达,都是私有的),因此枚举类不能对外创建对象
public enum EnumInstance { //实例对象 Instance; //私有构造器,不管是否显示私有化都是私有的,改为公有编译错误 private EnumInstance(){ } //拿取对象实例方法 public EnumInstance getInstance(){ return Instance; } }
试试用反射取改变构造器属性为公有
//枚举的构造器不是无参构造,Idea和JavaP命令都反编译为无参构造,而真正的构造器为参数为String和int Constructor<EnumInstance> ei = EnumInstance.class.getDeclaredConstructor(String.class, int.class); //设置构造器为公共属性 ei.setAccessible(true); //通过构造器创建对象 EnumInstance e1 = ei.newInstance(); EnumInstance e2 = ei.newInstance(); //展示hashcode System.out.println(e1); System.out.println(e2);
指向如上代码报错:
意思是不能使用反射创建枚举对象