java设计模式
今天是2024.2.4,快要过年了,最近没有新的开发需求,所以想总结一下java设计模式,记得第一次找工作,面试官问我设计模式的东西,我是一脸懵的,从那开始就有关注设计模式,买了书(Java设计模式-刘伟),也看了网上很多教程,但是总觉得还是得自己总结一下,才不至于模糊。
本文章我会按时追加更新…,因为可能写着写着突然来活了,也可能快要下班了,也可能出门玩耍了,总之一有时间我就来更新。
我的介绍有很多会取自书籍中的介绍,因为说的确实很清楚易懂。也算是我对读书的一个笔记吧。
1. 设计模式概述
这里书中提到两个词,分别是招式和内功,是的,各种编程语言,各种开发工具框架的使用,这就是招式,而算法,数据结构,设计模式就是内功,是不是解释的很清楚明了,确实,我们往往更重视了招式而忘记了修炼内功。
1.1起源
首先,设计模式起源于建筑领域,模式是在特定情况下人们解决某类重复问题的一套成功或有效的解决方案,而软件设计模式是四人组在1994年归纳发表了23种,设计模式主要包括:包括问题描述【待解决的问题是什么】、前提条件【在何种环境或约束条件下使用】、解法【如何解决】和效果【有哪些优缺点】,设计模式可分为创建型,结构型,行为型三种。
2.面向对象设计原则
2.1单一职责原则
单一职责原则,用于控制类的粒度大小,定义:一个类只负责一个功能领域中的相应职责。一个类或者方法,承担的职责越多,被复用的可能性越小,也就是需要我们解耦。
2.2开闭原则
总结一句话就是对扩展开放,对修改关闭,开闭原则是最重要的面向对象设计原则。这主要是需要我们对系统抽象化,就是定义一个稳定的抽象层,将不同的实现交给具体的实现层。遇到修改系统的行为,就可以不动抽象层,新增实现层。
2.3里氏代换原则
定义:所用引用基类的地方必须能透明地使用其子类的对象。啥意思?就是在程序中,将一个基类对象替换为它的子类对象,程序不会产生任何问题,反过来则不成立。这就是让我们在写代码的时候最好使用基类类型来定义,在运行时再确定子类类型。通过这里我们也可以得出:里氏代换原则要求,子类必须实现父类中声明的所有方法。
2.4依赖倒转原则
定义:抽象不应该依赖于细节,细节应当依赖于抽象。啥意思?这就是要我们针对接口编程,使用接口或者抽象类来声明变量,参数类型,方法返回,不要使用具体类,具体类中只实现接口或者抽象类中存在的方法,不给出多余的方法。
注意:这里插一嘴,在大多数情况下,这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是
手段。细细想想是不是,开闭原则是目标,目标就是要对新增开放,对修改关闭,这是目标。而里氏代换原则是基础,就是基类可以替换为子类对象。而依赖倒转是手段,通过子类只实现接口中存在的方法,然后声明接口类型这个手段。
2.5接口隔离原则
定义:使用多个专门的接口,不使用单一的总接口。这个感觉和第一个单一职责原则,有点类似的概念在,确实,都是专事专干的意思,咱们可以先这么理解。但是也要我们控制好接口粒度,太小导致接口泛滥,太大违背接口隔离原则。
2.6合成复用原则
定义:尽量使用对象组合,而不是继承来达到复用的目的。就是新对象通过关联关系来使用一些已有对象。组合可以降低类与类之间的耦合度,使类更加灵活。
2.7迪米特法则
定义:一个软件实体应当尽量少的与其他实体发生相互作用。就是这个模块发生修改的时候,尽量少的影响其他模块。
3.六个创建型模式
3.1简单工厂模式(simpleFactory)
学习难度:两颗星,使用频率:三颗星
其实简单工厂模式不算23个设计模式之一,算是工厂方法的小弟吧,然后工厂方法的大哥应该是抽象工厂模式。
定义:定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的对象通常具有相同的父类,因为工厂方法的返回值是基类类型。总之就是需要什么类型,传入什么参数,得到什么对象。
结构:工厂角色,抽象产品角色,具体产品角色
这里我还是写个代码吧
/**
* 抽象产品角色
**/
public abstract class Animal {
//公共方法
public void eat(){
System.out.println("吃饭");
}
//声明抽象业务方法
public abstract void move();
}
/**
* 具体产品角色1
**/
public class Bird extends Animal {
@Override
public void move() {
System.out.println("鸟儿飞翔");
}
}
/**
* 具体产品角色2
**/
public class Cat extends Animal {
@Override
public void move() {
System.out.println("猫咪跑路");
}
}
/**
* 工厂类
**/
public class PetShop {
public static Animal getAnimal(String arg){
Animal animal = null;
if ("A".equalsIgnoreCase(arg)){
animal = new Cat();
}else if ("B".equalsIgnoreCase(arg)){
animal = new Bird();
}
return animal;
}
}
/**
* 客户类
**/
public class Client {
public static void main(String[] args) {
Animal animal = PetShop.getAnimal("a");
animal.eat();
animal.move();
}
}
附加运行结果
吃饭
猫咪跑路
进程已结束,退出代码0
简单工厂提供一个专门创建对象的工厂类,将对象使用和创建分离。
缺点:工厂类明显职责过重,当要添加新的具体产品时,需要修改工厂类代码,不符合开闭原则,扩展困难。
3.2工厂方法模式(factoryMethod)
学习难度:两颗星,使用频率:五颗星
前面说了小弟的缺点是违反开闭原则,这不,小弟的大哥来了,我们看看什么情况。定义:引入抽象工厂类,不需要提供一个统一的工厂类来创建所有的产品对象,针对不同的产品提供不同的工厂。
结构:抽象产品,具体产品,抽象工厂,具体工厂。确实,这里把工厂做了抽象,如果需要增加新的产品,只需要增加一个新的具体工厂类,和具体产品类,无需改动代码。不愧是大哥!!!
这里要不要上代码呢?算了还是上一下吧,主要是我觉得代码有点多,不过还是写仔细点吧
/**
* 抽象产品
**/
public interface Animal {
public void move();
}
/**
* 抽象工厂
**/
public interface AnimalFactory {
public Animal createAnimal();
}
/**
* 具体产品
**/
public class Bird implements Animal {
@Override
public void move() {
System.out.println("鸟儿在飞翔");
}
}
/**
* 具体工厂
**/
public class BirdFactory implements AnimalFactory {
@Override
public Animal createAnimal() {
return new Bird();
}
}
/**
* 客户端代码不用修改,只需要修改配置文件
**/
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
FileReader reader = new FileReader("路径/classinfo.properties");
//创建属性类对象Map
Properties properties = new Properties();
//加载
properties.load(reader);
String className = properties.getProperty("className");
System.out.println(className);
Class<?> c = Class.forName(className);
AnimalFactory o = (AnimalFactory) c.newInstance();
Animal animal = o.createAnimal();
animal.move();
}
}
这里我加了一个配置文件classinfo.properties,客户端读取配置文件,得到对象的具体工厂对象。
很明显,大哥直接解决了小弟那的痛点,有新的产品时候,只需要新增对应的具体工厂和具体产品就好了,符合开闭原则。
缺点:可以发现类是成对增加的,增加了系统负责度和开销。
休息一会,吃个饭,给大家换换脑子。
3.3抽象工厂模式(abstractFactory)
学习难度:四颗星 使用频率:五颗星
前面我们说了工厂方法模式的缺点是类成对增加,增加系统开销。为此考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产。
定义:
-
产品等级结构:即产品的继承结构,如电视机是父类,华为电视,小米电视为子类,抽象电视机和具体电视之间构成一个产品等级结构。
-
产品族:指由一个工厂生产的位于不同产品等级结构中的一组产品
-
结构:抽象工厂,具体工厂(一个工厂生产多个产品,减少了工厂类),抽象产品,具体产品。
这里不妨也给下代码,这样看着更具象化
/**
* 抽象工厂
**/
public interface Factory {
public TV createTv();
public Fridge createFridge();
}
/**
* 具体工厂
* 生产属于一个产品族的所有产品
**/
public class HairFactory implements Factory {
private String name;
@Override
public TV createTv() {
return new HairTv();
}
@Override
public Fridge createFridge() {
return new HairFridge();
}
}
/**
*
* 抽象产品
**/
public interface TV {
public void show();
}
/**
* 具体产品类
**/
public class HairTv implements TV {
@Override
public void show() {
System.out.println("用海尔电视机播放电影");
}
}
/**
* 抽象产品
**/
public interface Fridge {
public void refrigeration();
}
/**
* 具体产品
**/
public class HairFridge implements Fridge {
@Override
public void refrigeration() {
System.out.println("用海尔电冰箱冷藏");
}
}
public class Client {
public static void main(String[] args) {
Factory factory = new HairFactory();
TV tv = factory.createTv();
Fridge fridge = factory.createFridge();
tv.show();
fridge.refrigeration();
}
}
是的,如此一来,当我们增加产品的时候,只需要增加一个工厂,比如增加一个小米工厂,可以提供小米电视和小米冰箱。就不是成对增加了。就是增加新的产品族比较方便。(忘记产品族是啥意思?回头看看呗)
缺点:增加新的产品等级结构比较麻烦,比如增加空调这个产品等级结构,需要修改抽象层工厂的代码和具体工厂的代码,违背开闭原则。
3.4单例模式(singleton)
学习难度:一颗星 使用频率:四颗星
哇,这个单例模式面试可太爱问了,也必须知道,上定义:一个类只能有一个实例,并且必须自行创建这个实例,必须自行向整个系统提供这个实例。结构也是最简单,核心就是单例类。不过我们学习单例就必须知道懒汉式,饿汉式,双重检测,IoDH等。
首先是饿汉式,顾名思义就是一上来就加载。代码如下
/**
* 饿汉式单例
* 饿汉式是实现起来最简单的单列类
* 问题:不能实现延迟加载,不使用也会占据内存
**/
public class Eager {
//静态常量,类加载时对象就创建
private static final Eager instance = new Eager();
//构造私有
private Eager() {
}
//提供统一获取的方法
public static Eager getInstance(){
return instance;
}
}
还有一种是懒汉式,也就是延迟加载技术,需要的时间再加载。
/**
* 懒汉式单例
* 双重检测版本 线程安全
* 问题:线程安全控制繁琐,性能还受到影响
**/
public class Lazy {
private Lazy() {
}
// volatile 保证可见
private volatile static Lazy instance = null;
public static Lazy getInstance(){
//第一重判断
if (instance == null){
synchronized (Lazy.class){
if (instance == null){
instance = new Lazy();
}
}
}
return instance;
}
}
这里我们必须要使用双重检测,这样能保证单例对象唯一,假设我们不适用双重检测,有一下问题:某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象。
对比:
-
饿汉式类加载时就创建,无需考虑多线程的问题,唯一就是从资源利用方面来说,这是饿汉式的缺点;
-
而懒汉式使用的时候创建,但是要考虑多线程问题,有性能问题。
那么有没有一种办法,可以解决上面两个痛点呢?还真有,直接上代码吧
/**
* IoDH
* 在单例类中增加一个静态内部类,在内部类中创建单例对象
* 优点:即可保证线程安全,又不影响系统性能。
**/
public class Singleton {
private Singleton() {
}
//静态内部类
private static class HolderClass{
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return HolderClass.instance;
}
}
这里单例对象没有作为成员变量,所以类加载时不会被初始化,在调用的时候,才会创建,由java虚拟机来保证线程安全,不会有性能问题。这不失为最好的一种解决方式。鱼和熊掌这次兼得了。
3.5原型模式(prototype)
难度:三颗星 使用频率:三颗星
这里书中作者用西游记中猴哥拔毛变小猴的故事和我们平时最爱的复制粘贴引出原型模式,就是通过一个原型对象克隆出多个一模一样的对象。定义:原型模式是一种对象创建型模式,通过克隆方法所创建出来的对象是全新的对象,在内存中拥有新的地址。结构:抽象原型类(声明克隆方法的接口),具体原型类(实现克隆方法,返回一个自己的克隆对象),客户类。这里我们需要知道浅克隆和深克隆的区别和实现方式。
区分浅克隆和深克隆的区别:在复制原型对象的时候是否同时复制包含在原型对象中引用类型的成员变量;
-
浅克隆:基本类型的复制一份给克隆对象,引用类型就将地址复制一份给克隆对象
-
深克隆:可以使用序列化实现,实现序列化的对象其类必须实现 Serializable 接口
下面我们先看浅克隆
/**
* 抽象原型类
**/
public abstract class Prototype {
public abstract Prototype clone();
}
/**
* 具体原型类
**/
public class ConcretePrototype extends Prototype {
private String attr;//引用类型,非基本数据类型
public String getAttr() {
return attr;
}
public void setAttr(String attr) {
this.attr = attr;
}
@Override
public Prototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.setAttr(this.attr);
return concretePrototype;
}
}
/**
* 第二种实现方式
* Obiect类提供了 clone()方法
* 需要实现 Cloneable 标识接口,表示此类支持复制,否则会抛出一个 CloneNotSupportedException
* 此时 Object类相当于抽象原型类
**/
public class ConcretePrototype2 implements Cloneable {
private String attr;//引用类型,非基本数据类型
public String getAttr() {
return attr;
}
public void setAttr(String attr) {
this.attr = attr;
}
public ConcretePrototype2 clone() {
Object obiect = null;
try {
obiect = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return (ConcretePrototype2) obiect;
}
}
/**
* 客户类
**/
public class Client {
public static void main(String[] args) {
// ConcretePrototype prototype = new ConcretePrototype();
// prototype.setAttr("北京");
// ConcretePrototype clone = (ConcretePrototype) prototype.clone();
// System.out.println(prototype == clone); //false 说明为两个不同的对象
// System.out.println(prototype.getAttr() == clone.getAttr()); //true 说明引用数据类型指向同一个地址 浅克隆
ConcretePrototype2 concretePrototype2 = new ConcretePrototype2();
concretePrototype2.setAttr("上海");
ConcretePrototype2 clone1 = concretePrototype2.clone();
System.out.println(concretePrototype2 == clone1); //false
System.out.println(concretePrototype2.getAttr() == clone1.getAttr()); //true
}
}
这里给了两种浅克隆的实现方式,一种是我们手动复制,还有一种是Object类的clone()方法,这时候Object相当于是抽象原型类,通过测试我们知道浅克隆的引用数据类型指向同一个地址。
下面我们看看深克隆
public class Teacth implements Serializable {
private User user;
private String name;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Teacth deepClone() throws IOException, ClassNotFoundException {
//将对象写入流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacth"));
oos.writeObject(this);
//将对象从流中读出
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("teacth"));
Teacth o = (Teacth) ois.readObject();
return o;
}
}
public class User implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/**
* 客户类
**/
public class Client {
public static void main(String[] args) {
User user = new User();
user.setName("张三");
Teacth teacth = new Teacth();
teacth.setName("李四");
teacth.setUser(user);
Teacth teacth1 = null;
try {
teacth1 = teacth.deepClone();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(teacth == teacth1);//false
System.out.println(teacth.getUser() == teacth1.getUser());//false
}
}
这里使用序列化和反序列化来实现,可以看到两个引用类型是不同的,这就是深克隆。
这里书中作者还介绍了原型管理器,将多个原型存储在一个集合中提供给客户类。
/**
* 原型管理器,将多个原型对象存储在一个集合中
* 设置为单例,节约资源
**/
public class PrototypeManager {
private Hashtable table = new Hashtable();
private static PrototypeManager manager;
private PrototypeManager() {
table.put("A", new ConcretePrototype());//这里可以i放多个原型具体类
}
static {
manager = new PrototypeManager();
}
public void add(String key, Prototype prototype) {
table.put(key, prototype);
}
public Prototype get(String key) {
Prototype clone = ((Prototype) table.get(key)).clone();
return clone;
}
public static PrototypeManager getInstance() {
return manager;
}
}
/**
* 客户类
**/
public class Client {
public static void main(String[] args) {
PrototypeManager instance = PrototypeManager.getInstance();
PrototypeManager instance2 = PrototypeManager.getInstance();
Prototype prototype = instance.get("A");
Prototype clone = prototype.clone();
System.out.println(prototype == clone); //false
System.out.println(instance == instance2); //true
}
}
这里也是通过饿汉式,将原型管理器设置为单例。
可以知道,原型模式的优点无非就是简化对象的创建,缺点主要是每个类都要配备一个克隆方法,改造的时候,违背开闭原则。深克隆的时候,如果对象有多重且套,则每一个都需要支持深克隆,比较麻烦。
3.6建造者模式(builder)
难度:四颗星,使用频率:两颗星
可以看到这个使用频率比较少,建造者模式,顾名思义就是将包含多个组成部分的复杂对象的创建和客户端分离,定义:将复杂对象本身和它们的组装过程分开,关注如何一步一步地创建一个包含多个组成部分的复杂对象。结构:抽象建造者,具体建造者,产品(复杂对象,就是包含多个成员变量的对象),指挥者(安排复杂对象的建造次序,客户端一般和指挥者交互)。是的,不同的建造者定义了不同的创建过程,相互独立,增加新的建造者很方便,不涉及修改,符合开闭原则。
/**
* 复杂产品对象
**/
public class Actor {
private String type; //角色类型
private String sex;
private String face; //脸型
private String costume; //服装
private String hairstyle; //发型
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getFace() {
return face;
}
public void setFace(String face) {
this.face = face;
}
public String getCostume() {
return costume;
}
public void setCostume(String costume) {
this.costume = costume;
}
public String getHairstyle() {
return hairstyle;
}
public void setHairstyle(String hairstyle) {
this.hairstyle = hairstyle;
}
@Override
public String toString() {
return "Actor{" +
"type='" + type + '\'' +
", sex='" + sex + '\'' +
", face='" + face + '\'' +
", costume='" + costume + '\'' +
", hairstyle='" + hairstyle + '\'' +
'}';
}
}
/**
* 抽象建造者
**/
public abstract class ActorBuilder {
protected Actor actor = new Actor();//产品
public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();
//返回一个完整的产品对象
public Actor createActor(){
return actor;
}
}
/**
*
* 具体建造者
**/
public class HeroBuilder extends ActorBuilder {
@Override
public void buildType() {
actor.setType("英雄");
}
@Override
public void buildSex() {
actor.setSex("男");
}
@Override
public void buildFace() {
actor.setFace("冷酷");
}
@Override
public void buildCostume() {
actor.setCostume("盔甲");
}
@Override
public void buildHairstyle() {
actor.setHairstyle("飘逸");
}
}
/**
* 指挥者
**/
public class ActorController {
public Actor construct(ActorBuilder ab){
Actor actor;
ab.buildType();
ab.buildCostume();
ab.buildFace();
ab.buildHairstyle();
ab.buildSex();
actor = ab.createActor();
return actor;
}
}
public class Client {
public static void main(String[] args) {
ActorBuilder ab = new HeroBuilder();
ActorController actorController = new ActorController();//指挥者
Actor construct = actorController.construct(ab); //得到英雄对象
System.out.println(construct);
}
}
客户端和指挥者交互,给指挥者传入对应的具体建造者得到产品对象,但是缺点也明显就是:需要产品具有较多的共同点,如果产品的很多组成部分不同,不适合使用建造者模式。
4.七个结构型模式
4.1适配器模式(adapter)
学习难度:两颗星 使用频率:四颗星