第一章:程序设计原则—单一职责、接口隔离、依赖倒置、里式替换
第二章:程序设计原则—开闭原则、迪米特法则、合成复用原则
推荐书籍:
- 设计模式:可复用面向对象软件的基础
- 设计模式之禅(第2版)
喜欢的请不要忘记三连加关注哦!更多优质技术文章和经验分享陆续推出。有想了解的方面也支持评论区点播!
一、设计模式简单介绍1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,在本教程中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4 位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)匿名著称。
1.1、什么是设计模式软件工程中的设计模式(design pattern)是对软件设计中普遍存在(反复出现)的的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛等人在1990年从建筑设计领域引入到计算机科学中的
设计模式考虑的是一个软件的结构上怎么更合理,如何提高稳定性、复用性、扩展性上考虑,而不仅仅是功能实现上。如果站在一个功能实现上可能用不到设计模式。但是如果站在软件体系、结构的角度上来考虑比较有意义。
说白了就是写代码的套路,前人将一些常见的场景进行优化,变成一个个的编码套路,模板,我们按照这个套路来写代码的话,代码的可读性,维护性,扩展性等就会更强。这就是`设计模式`
1.2、设计模式为什么重要- 利用好设计模式,在客户说新增功能时系统有很强的扩展性
- 在某同事离职你接手他的工作时,设计模式有很强的维护性,易于阅读
- 目前程序员门槛越来越高,面试都会问你在实际项目中使用过什么设计模式,怎么使用的,解决了什么问题
- 在很多系统或者框架中都会使用设计模式
- 成为一个合格的软件工程师,学习设计模式非常重要
在程序编写过程中,程序员面临着耦合性、内聚性、维护性、扩展性、重用性、稳定性等方面带来的挑战。设计模式就是来解决软件开发中的以上问题。设计模式包含了面向对象的精髓,懂了设计模式,你就懂了面向对象分析和设计(OOA/D)的精要
1.4、设计模式分类1.4.1、创建型模式这些设计模式用于描述“`怎样创建对象`”,它的主要特点是“将对象的创建与使用分离”:
- 单例模式
- 工厂模式
- 抽象工厂模式
- 原型模式
- 建造者模式
这些设计模式关注类和对象的组合,用于描述如何将类或对象按某种布局组成更大的结构:
- 适配器模式
- 桥接模式
- 装饰模式
- 组合模式
- 外观模式
- 享元模式
- 代理模式
用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责:
- 模板方法模式
- 命令模式
- 访问者模式
- 迭代器模式
- 观察者模式
- 中介者模式
- 备忘录模式
- 解释器模式
- 状态模式
- 策略模式
- 责任链模式
提示:将以3章分别讲述3类设计模式
二、单例模式单例模式是比较常见的一种设计模式,面试的时候有时候会让你写一下单例,我就遇到过让我写单例、工厂、观察者模式的面试。
单例(Singleton)指一个类只能有一个实例,且该类能自行创建这个实例。例如Windows中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成的内存浪费,或出现多个窗口显示内容不一致的错误。
单例模式在现实生活中应用也很广泛,例如公司CEO、部门经理等都属于单例模型,如果领导太多,意见不符,对公司的发展会造成很不好的影响。J2EE标准中的ServletContext 和 ServletContextConfig、Spring框架中的ApplicationContext、数据库中的连接池都是单例,这里可以细品一下为什么设计成单例的?
单例模式有3个特点:
- 单例类只有一个对象实例
- 该单例对象必须由该单例类自行创建(如果可以交给用户创建那么肯定保障不了单例,每个用户都可能new一个)
- 单例类对外提供一个访问该单例的全局访问点
通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
单例模式结构主要包含两个角色:
- 单例类:包含一个实例且能自行创建这个实例的类
- 访问类:使用单例的类
单例大概有8种实现方式,其中重要的推荐使用的会单独标记出来,请注意文字提示哦
2.1.1、饿汉式(静态常量)
// 测试类
public class SingletonTest1 {
public static void main(String[] args) {
// 获取单例对象
Singletion01 instance01 = Singletion01.getInstance();
// 获取单例对象
Singletion01 instance02 = Singletion01.getInstance();
// 比较两个对象引用地址是否相同
System.out.println(instance01 == instance02);
}
}
// 单例类
class Singletion01 {
// 实例化单例对象
private static final Singletion01 instance = new Singletion01();
// 私有化构造方法
private Singletion01() {
}
// 对外提供方法返回单例对象
public static Singletion01 getInstance() {
return instance;
}
}
优点
- 这种写法比较简单,就是在类装载的时候就完成实例化,避免线程同步问题
缺点
- 在类装载的时候完成实例化,没有达到Lazy Loading效果,如果从开始至终,从未使用过这个实例,则会造成内存浪费
这种单例模式可用,可能造成内存浪费
2.1.2、饿汉式(静态代码块)
// 测试类
public class SingletonTest2 {
public static void main(String[] args) {
// 获取单例对象
Singleton instance01 = Singleton.getInstance();
// 获取单例对象
Singleton instance02 = Singleton.getInstance();
// 比较两个对象引用地址是否相同
System.out.println(instance01 == instance02);
}
}
// 单例类
class Singleton {
// 声明静态变量
public static Singleton singleton;
// 在静态代码块中创建对象,并赋值给静态变量,我们知道类只能被加载一次,可保证单例
static {
singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
它的优缺点等和静态常量是一样的
2.1.3、懒汉式(线程不安全)
// 测试类
public class SingletonTest03 {
public static void main(String[] args) {
// 获取单例对象
Singleton singleton1 = Singleton.getInstance();
// 获取单例对象
Singleton singleton2 = Singleton.getInstance();
// 比较两个对象引用地址是否相同
System.out.println(singleton1 == singleton2);
}
}
// 单例类
class Singleton {
// 声明静态变量
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
// 判断是否实例化
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点
- 起到了 Lazy Loading 的效果,但是只能在单线程下使用
缺点
- 多线程情况下,一个线程进入到 if(singleton == null) 判断,还未来得及执行时,另一个线程也通过了这个判断,就会产生多个实例。所以在多线程环境下不可使用这种方式
在实际开发中,不要使用这种方式
2.1.4、懒汉式(同步方法)
// 测试类
public class SingletonTest4 {
public static void main(String[] args) {
// 获取单例对象
Singleton singleton1 = Singleton.getInstance();
// 获取单例对象
Singleton singleton2 = Singleton.getInstance();
// 比较两个对象引用地址是否相同
System.out.println(singleton1 == singleton2);
}
}
// 单例类
class Singleton {
private static Singleton singleton;
private Singleton() {}
// 同步方法加锁,解决多线程安全问题
public static synchronized Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
优点
- 解决了线程安全问题
缺点
- 效率低,每个线程想要获取实例时都需要判断锁,获取锁,释放锁。而其实创建实例的方法执行一次就可以了。后期想要获取该实例,直接return即可
在实际开发中,不推荐使用
2.1.5、同步代码块
// 测试类
public class SingletonTest5 {
public static void main(String[] args) {
// 获取单例对象
Singleton singleton1 = Singleton.getInstance();
// 获取单例对象
Singleton singleton2 = Singleton.getInstance();
// 比较两个对象引用地址是否相同
System.out.println(singleton1 == singleton2);
}
}
// 单例类
class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
// 判断是否实例化过
if(singleton == null) {
// 同步代码块
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
这种方式的本意是解决第四种方法,效率低的问题,但是不能保证线程安全,一样的在判断空完了之后没来的获取锁,另一个线程进来,也是可以再创建对象的,其实就是加锁的地方不对。所以开发中不要使用
2.1.6、双重检查
// 测试类
public class SingletonTest6 {
public static void main(String[] args) {
// 获取单例对象
Singleton singleton1 = Singleton.getInstance();
// 获取单例对象
Singleton singleton2 = Singleton.getInstance();
// 比较两个对象引用地址是否相同
System.out.println(singleton1 == singleton2);
}
}
// 单例类
class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
// 判断是否创建实例
if(singleton == null) {
// 同步锁,解决多线程安全问题
synchronized (Singleton.class) {
// 再次判空,保障不被重复创建
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
优点
- 双重检查概念是多线程开发中经常使用的。通过第一组判断,避免反复调用同步代码块,提高效率
- 线程安全、延迟加载,效率较高
在开发中,推荐使用这种单例设计模式
2.1.7、静态内部类
public class SingletonTest7 {
public static void main(String[] args) {
// 获取单例对象
Singleton singleton1 = Singleton.getInstance();
// 获取单例对象
Singleton singleton2 = Singleton.getInstance();
// 比较两个对象引用地址是否相同
System.out.println(singleton1 == singleton2);
}
}
// 单例类
class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
// 静态内部类
private static class SingletonInstance{
// 创建外部类对象
private static final Singleton INSTANCE = new Singleton();
}
// 返回外部类对象
public static synchronized Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
优点
- 类加载时静态内部类并不会被加载,静态内部类加载时,线程是安全的
- 在调用getInstance方法时才会加载 SingletonInstance(内部类)
- 避免线程不安全,利用静态内部类特点实现延迟加载、效率高
推荐使用
2.1.8、枚举该方式是 《Effective Java》一书的作者 《Joshua Bloch》提出的,大佬发现市面上很多人都子啊研究单例咋整,咋优雅,咋安全,咋性能好。这哥们觉得好low啊,搞得好复杂,利用JDK1.5之后推出的枚举类,一出手就是 最优解决方案。
public class SingletonTest8 {
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println(instance1 == instance2);
}
}
enum Singleton {
INSTANCE;
}
优点
- 借助JDK1.5的枚举实现单例,不仅避免线程同步问题,而且还能防止反序列化重新创建新的对象
推荐使用
2.2、单例的优缺点优点
- 单例可以保证内存中只有一个实例,减少内存开销
- 可以避免对资源的多重占用
- 单里设置全局访问点,可以优化和共享资源的访问
缺点
- 单例一般没有接口,扩展困难,如果要扩展,必须修改源码,违反开闭原则
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个对象
- 单例模式的生成代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则
对于Java 来说,单例是保证一个JVM中只存在单一实例,主要应用场景:
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少GC
- 某类只要求生成一个对象的时候,如一个班中的班长,每个人的身份证
- 某些类创建实例时资源占用过多,或实例化耗时较长,且经常使用
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等
- 频繁访问数据库或文件的对象
- 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等
推荐使用枚举、双重检查和饿汉式方式,强烈建议使用枚举。
单元素的枚举类型已经成为实现Singleton的最佳方法
-- 出自 《effective java》
三、简单工厂模式该设计模式并不包含在GOF提出的23种设计模式之中,但是也是值得拿来说一说的。
工厂模式定义一个常见产品对象的工厂接口,将产品对象的实际 创建工作 推迟到具体子工厂类中,这满足创建型模式中所要求的“创建与使用相分离”的特点。
按照实际业务场景划分,工厂模式分为 `简单工厂模式`, `工厂方法模式`, `抽象工厂模式`。我们把被创建的对象称为"产品",把创建产品的对象称为"工厂",`如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”`。
在简单工厂模式中创建实例的方法通常为静态(static)方法,因此简单工厂模式(Simple Factory Pattern)又叫作`静态工厂方法模式`(Static Factory Method Pattern)。
简单来说,简单工厂模式有一个具体的工厂类,可以生成多个不同的产品,属于创建型设计模式。
简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。
3.1、代码实现我们通过原始方式和工厂模式方式来实现购买披萨的场景。我们想要通过一个商店来订购不同种类的披萨,比如奶酪披萨、希腊披萨等
3.1.1、原始方式
// 1、将披萨类做成一个抽象类
public abstract class Pizza {
protected String name;
public abstract void prepare();
public void bake() {
System.out.println(name "baking;");
}
public void cut() {
System.out.println(name "cut;");
}
public void box() {
System.out.println(name "box;");
}
public void setName(String name) {
this.name = name;
}
}
// 2、奶酪披萨
public class CheesePizza extends Pizza{
@Override
public void prepare() {
System.out.println("给奶酪披萨准备原材料");
}
}
// 3、希腊披萨
public class GreekPizza extends Pizza{
@Override
public void prepare() {
System.out.println("给希腊披萨准备原材料");
}
}
// 4、披萨商店
public class OrderPizza {
private static Scanner scanner = new Scanner(System.in);
/**
* 订购披萨
*/
public void getPizza() {
Pizza pizza = null;
// 订购披萨类型
while (true) {
System.out.println("请输入披萨种类:");
String pizzaType = scanner.next();
if("cheese".equals(pizzaType)) {
pizza = new CheesePizza();
pizza.setName("cheese");
}else if("greek".equals(pizzaType)) {
pizza = new GreekPizza();
pizza.setName("greek");
}else {
break;
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}
}
}
// 5、购买披萨
public class PizzaStore {
public static void main(String[] args) {
// 创建披萨订单实例
OrderPizza orderPizza = new OrderPizza();
// 订购披萨
orderPizza.getPizza();
}
}
如果现在需要新增一个新种类的披萨,那么
- 创建一个新种类的披萨类
- 在披萨订购的代码中新增一个条件判断
- 致命的是,订购方往往很多,每一处都要修改,这个修改量就很大了
// 披萨抽象类
public abstract class Pizza {
protected String name;
public void setName(String name) {
this.name = name;
}
public abstract void prepare();
public void bake() {
System.out.println(name "baking;");
}
public void cut() {
System.out.println(name "cut;");
}
public void box() {
System.out.println(name "box;");
}
}
// 希腊披萨
public class GreekPizza extends Pizza{
@Override
public void prepare() {
System.out.println(name "披萨准备原材料");
}
}
// 奶酪披萨
public class CheesePizza extends Pizza{
@Override
public void prepare() {
System.out.println(name " 准备原材料");
}
}
// 披萨工厂根据用户的而不同需求,生产不同类别的披萨
public class SimpleFactory {
public Pizza createPizza(String pizzaType) {
Pizza pizza = null;
if("cheese".equals(pizzaType)) {
pizza = new CheesePizza();
pizza.setName("cheese");
}else if("greek".equals(pizzaType)) {
pizza = new GreekPizza();
pizza.setName("greek");
}
return pizza;
}
}
// 订购披萨
public class OrderPizza {
private static Scanner scanner = new Scanner(System.in);
private SimpleFactory simpleFactory;
// 传入工厂
public OrderPizza(SimpleFactory simpleFactory) {
this.simpleFactory = simpleFactory;
}
public void getPizza() {
while (true) {
System.out.println("请输入披萨类型:");
// 从工厂获取披萨
Pizza pizza = simpleFactory.createPizza(scanner.next());
if (pizza != null) {
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
}else {
System.out.println("暂无该类型披萨!");
break;
}
}
}
}
// 购买披萨
public class PizzaStore {
public static void main(String[] args) {
// 创建披萨订购类,并传入披萨工厂
OrderPizza orderPizza = new OrderPizza(new SimpleFactory());
orderPizza.getPizza();
}
}
这样如果要新增一个种类的披萨,则需要修改
- 新增一个披萨类
- 在工厂中新增一个披萨判断
- 订购披萨处则不需要修改,因为都是从同一个工厂中获取的披萨
这里如果是编码功底比较弱的同学,多思考思考,可以不着急往下看,一定要理解透。这样我们修改的地方是不是就少了?是不是相比于原来的实现方式提高了一些扩展性
3.1.3、静态工厂改进其实就是将工厂类中获取实例的方法改为静态的,方便调用
public class SimpleFactory {
public static Pizza createPizza(String pizzaType) {
Pizza pizza = null;
if("cheese".equals(pizzaType)) {
pizza = new CheesePizza();
pizza.setName("cheese");
}else if("greek".equals(pizzaType)) {
pizza = new GreekPizza();
pizza.setName("greek");
}
return pizza;
}
}
优点
- 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
- 客户端无需知道所创建具体产品的类名,只需知道参数即可。
- 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类
缺点
- 简单工厂模式的工厂类型单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则。
- 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
- 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
- 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。
简单工厂模式的结构
- 简单工厂(SimpleFactory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
- 抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
- 具体产品(ConcreteProduct):是简单工厂模式的创建目标。
在以上案例中,SimpleFactory 这个类就是一个简单工厂,Pizza 这个类就是一个抽象产品,因为不知道要哪一个具体的披萨,CheesePizza 和 GreekPizza 是两个具体的产品,也是最终工厂要生产的具体产品,以及后边可能还会有什么胡椒披萨,水果披萨等等子产品。
工厂中接收的是抽象类,这样可以接收各种类型的披萨,这不就是 依赖倒置原则 么
四、工厂方法模式简单工厂模式违背了开闭原则,即新增一个类型的产品时就要新增一个产品类和一个工厂类。比如要生产蛋糕了,面包了。而工厂方法模式是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原代码的情况下引进新产品,满足开闭原则。
4.1、实现方式工厂方法模式由抽象工厂、具体工厂、抽象产品和具体产品等4个要素构成
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应
// 抽象工厂类
public abstract class PizzaFactory {
abstract Pizza createPizza(String name);
}
// 抽象产品类
public abstract class Pizza {
protected String name;
public void setName(String name) {
this.name = name;
}
public abstract void prepare();
public void bake() {
System.out.println(name "=baking;");
}
public void cut() {
System.out.println(name "=cut;");
}
public void box() {
System.out.println(name "=box;");
}
}
// 具体工厂1
public class BJPizzaFactory extends PizzaFactory{
@Override
Pizza createPizza(String name) {
Pizza pizza = null;
if("pepper".equals(name)) {
pizza = new BJPepperPizza();
}else if("cheese".equals(name)) {
pizza = new BJCheesePizza();
}
return pizza;
}
}
// 具体工厂2
public class LDPizzaFactory extends PizzaFactory{
@Override
Pizza createPizza(String name) {
Pizza pizza = null;
if("pepper".equals(name)) {
pizza = new LDPepperPizza();
}else if("cheese".equals(name)) {
pizza = new LDCheesePizza();
}
return pizza;
}
}
// 工厂1生产产品
public class BJCheesePizza extends Pizza{
@Override
public void prepare() {
System.out.println("给 " name " 披萨准备原材料");
}
}
public class BJPepperPizza extends Pizza{
@Override
public void prepare() {
System.out.println("给 " name " 披萨准备原材料");
}
}
// 工厂2生产产品
public class LDCheesePizza extends Pizza{
@Override
public void prepare() {
System.out.println("给 " name " 披萨准备原材料");
}
}
public class LDPepperPizza extends Pizza{
@Override
public void prepare() {
System.out.println("给 " name " 披萨准备原材料");
}
}
// 订购产品
public class OrderPizza {
private static Scanner scanner = new Scanner(System.in);
private PizzaFactory pizzaFactory;
public OrderPizza(PizzaFactory pizzaFactory) {
this.pizzaFactory = pizzaFactory;
}
public void getPizza() {
while (true) {
System.out.println("请输入披萨类型");
String name = scanner.next();
Pizza pizza = pizzaFactory.createPizza(name);
if(pizza != null) {
pizza.setName(name);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.bake();
}else {
System.out.println("没有这种披萨");
break;
}
}
}
}
// 测试类
public class PizzaStore {
public static void main(String[] args) {
// 只需要知道用哪个工厂即可
OrderPizza orderPizza = new OrderPizza(new BJPizzaFactory());
orderPizza.getPizza();
}
}
当需要生成的产品不多且不会增加,一个具体工厂类就可以完成任务时,可删除抽象工厂类。这时工厂方法模式将退化到简单工厂模式
4.2、工厂方法模式优缺点优点
- 用户只需要知道具体的工厂名称,就可以得到想要的产品,无需知道产品的创建细节
- 灵活性增强,对于新产品的创建,只需要多写一个相应的工厂类
- 典型的解耦框架,高层模块只需要知道产品的抽象类,无需关心其他实现类,满足迪米特法则,依赖倒置原则,里氏替换原则
缺点
- 类的数量可能过多,增加复杂度
- 增强系统的抽象性和理解难度
- 抽象产品只能生产一种产品,可以使用抽象工厂模式解决
- 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口
- 客户不关心创建产品的细节,只关心产品的品牌
抽象工厂模式是一种为访问类提供一个创建`一组相关或相互依赖对象的接口`,且访问类无需指定所需产品的具体类就可以得到同族的不同等级的产品模式结构。抽象工厂模式是工厂方法模式的升级版,工厂方法模式只生产一个等级的产品(比如只生产电视、手机),而抽象工厂模式可以生产多个等级的产品(一个工厂可以生产手机也可以生产电视等)。
5.1、实现方式
抽象工厂模式同工厂方法模式一样,也是由抽象工厂、具体工厂、抽象产品和具体产品等 4 个要素构成,但抽象工厂中方法个数不同,抽象产品的个数也不同
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品(如手机、路由器、笔记本)。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
// 1、抽象产品
// 抽象手机产品
public interface IPhoneProduct {
void start();
void shoutdown();
void call();
void sendSMS();
}
// 抽象路由器产品
public interface IRouter {
void start();
void shoutdown();
void wifi();
void setting();
}
// 2、具体产品
// 小米手机
public class XiaomiPhone implements IPhoneProduct{
@Override
public void start() {
System.out.println("小米手机开机");
}
@Override
public void shoutdown() {
System.out.println("小米手机关机");
}
@Override
public void call() {
System.out.println("小米手机打电话");
}
@Override
public void sendSMS() {
System.out.println("小米手机发短信");
}
}
// 小米路由器
public class XiaomiRouter implements IRouter{
@Override
public void start() {
System.out.println("小米路由器开机");
}
@Override
public void shoutdown() {
System.out.println("小米路由器关机");
}
@Override
public void wifi() {
System.out.println("小米路由器发射信号");
}
@Override
public void setting() {
System.out.println("小米路由器设置");
}
}
// 华为手机
public class HuaweiPhone implements IPhoneProduct{
@Override
public void start() {
System.out.println("华为手机开机");
}
@Override
public void shoutdown() {
System.out.println("华为手机关机");
}
@Override
public void call() {
System.out.println("华为手机打电话");
}
@Override
public void sendSMS() {
System.out.println("华为手机发短信");
}
}
// 华为路由器
public class HuaweiRouter implements IRouter{
@Override
public void start() {
System.out.println("华为路由器开机");
}
@Override
public void shoutdown() {
System.out.println("华为路由器关机");
}
@Override
public void wifi() {
System.out.println("华为路由器发射信号");
}
@Override
public void setting() {
System.out.println("华为路由器设置");
}
}
// 3、抽象工厂
public interface IFactory {
IPhoneProduct createPhone();
IRouter createRouter();
}
// 4、具体工厂
// 小米工厂
public class XiaomiFactory implements IFactory{
@Override
public IPhoneProduct createPhone() {
return new XiaomiPhone();
}
@Override
public IRouter createRouter() {
return new XiaomiRouter();
}
}
// 华为工厂
public class HuaweiFactory implements IFactory{
@Override
public IPhoneProduct createPhone() {
return new HuaweiPhone();
}
@Override
public IRouter createRouter() {
return new HuaweiRouter();
}
}
// 5、使用工厂
public class Client {
public static void main(String[] args) {
System.out.println("============小米工厂==========");
XiaomiFactory xiaomiFactory = new XiaomiFactory();
IPhoneProduct phone = xiaomiFactory.createPhone();
phone.start();
phone.shoutdown();
phone.sendSMS();
phone.call();
xiaomiFactory.createRouter();
}
}
另一方面,当系统中只存在一个等级结构的产品时(比如只生产电视了),抽象工厂模式将退化到工厂方法模式
5.2、抽象工厂优缺点优点
- 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
- 当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
- 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。
缺点
- 当产品族(同一厂商)中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。
- 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
- 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
- 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
又称 静态工厂模式,可以通过一个工厂类创建并提供某一个产品对象,当需要新增一个同类产品时,需要修改工厂代码实现。不符合开闭原则
5.4.2、工厂方法模式通过对工厂类抽象,具体的工厂类实现抽象的工厂类实现新增同类产品时不需要修改原工厂,只需要新建工厂类创建产品即可。但是弊端在于会产生大量的工厂类
5.4.3、抽象工厂模式以上两种工厂模式都是创建同一类产品,如果我们要生产不同种类的产品则需要继续抽象工厂,我们可以直接将工厂抽象,不通的厂商可以实现抽象工厂,进而生产产品。
总的来说,开发时简单工厂的应用是最多的,虽然它不符合开闭原则,但是在开发难度、系统架构、功能实现上来说是最优的
六、原型模式原型(Prototype)模式的定义如下:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。例如,Windows 操作系统的安装通常较耗时,如果使用系统镜像复制就快了很多。
6.1、实现方式由于 Java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。
原型模式包含以下主要角色。
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
// 1、原始对象
public class Sheep {
private String name;
private int age;
public Sheep(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Sheep{"
"name='" name '\''
", age=" age
'}';
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 1、创建一个对象
Sheep sheep1 = new Sheep("喜洋洋", 3);
// 2、创建对象,将原始对象的数据存进去,很傻吧~~~
Sheep sheep2 = new Sheep(sheep1.getName(), sheep1.getAge());
Sheep sheep3 = new Sheep(sheep1.getName(), sheep1.getAge());
Sheep sheep4 = new Sheep(sheep1.getName(), sheep1.getAge());
Sheep sheep5 = new Sheep(sheep1.getName(), sheep1.getAge());
Sheep sheep6 = new Sheep(sheep1.getName(), sheep1.getAge());
// 通过hashCode判断是否一样
System.out.println(sheep1 "hashCode = " sheep1.hashCode());
System.out.println(sheep2 "hashCode = " sheep2.hashCode());
System.out.println(sheep3 "hashCode = " sheep3.hashCode());
System.out.println(sheep4 "hashCode = " sheep4.hashCode());
System.out.println(sheep5 "hashCode = " sheep5.hashCode());
System.out.println(sheep6 "hashCode = " sheep6.hashCode());
}
}
以上方式实现的优点在于便于理解。缺点在于:
- 如果原型的参数变多,进行克隆就比较麻烦
// 原始对象,实现Cloneable接口,重写 clone 方法
public class Sheep implements Cloneable{
private String name;
private int age;
public Sheep(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Sheep{"
"name='" name '\''
", age=" age
'}';
}
// 重写clone方法,
@Override
protected Object clone() throws CloneNotSupportedException {
// 将Sheep返回
Sheep sheep = null;
sheep = (Sheep) super.clone();
return sheep;
}
}
// 测试类
public class Clent {
public static void main(String[] args) throws CloneNotSupportedException {
Sheep sheep = new Sheep("喜洋洋", 12);
Sheep sheep1 = (Sheep)sheep.clone();
Sheep sheep2 = (Sheep)sheep.clone();
Sheep sheep3 = (Sheep)sheep.clone();
Sheep sheep4 = (Sheep)sheep.clone();
Sheep sheep5 = (Sheep)sheep.clone();
System.out.println(sheep);
System.out.println(sheep1);
System.out.println(sheep2);
System.out.println(sheep3);
System.out.println(sheep4);
System.out.println(sheep5);
}
}
以上属于浅拷贝,意思是,如果原型类中包含对象,clone仅仅是指向了类中所使用的对象,并没有创建新的对象
public class Sheep implements Cloneable{
private String name;
private int age;
private Sheep friend;
public Sheep(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Sheep getFriend() {
return friend;
}
public void setFriend(Sheep friend) {
this.friend = friend;
}
@Override
public String toString() {
return "Sheep{"
"name='" name '\''
", age=" age
", friend=" friend
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Sheep sheep = null;
sheep = (Sheep) super.clone();
return sheep;
}
}
比如上述代码,Sheep类中引用了一个Sheep类,在拷贝的时候,被引用friend并没有创建新的对象,而是多个对象引用同一个friend。我们可以通过深拷贝解决
- 通过clone实现深拷贝
- 通过序列化实现深拷贝
public class Sunwukong implements Cloneable, Serializable {
private String name;
private int age;
public Sunwukong(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Sunwukong{"
"name='" name '\''
", age=" age
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return (Sunwukong)super.clone();
}
}
public class LiuErNiHou implements Cloneable, Serializable {
private String name;
private Sunwukong sunwukong;
public LiuErNiHou(String name, Sunwukong sunwukong) {
this.name = name;
this.sunwukong = sunwukong;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Sunwukong getSunwukong() {
return sunwukong;
}
public void setSunwukong(Sunwukong sunwukong) {
this.sunwukong = sunwukong;
}
@Override
public String toString() {
return "LiuErNiHou{"
"name='" name '\''
", sunwukong=" sunwukong
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
LiuErNiHou liuer = null;
liuer = (LiuErNiHou)super.clone();
liuer.sunwukong = (Sunwukong) sunwukong.clone();
return liuer;
}
}
public class Client {
public static void main(String[] args) throws CloneNotSupportedException {
Sunwukong sunwukong = new Sunwukong("孙悟空", 500);
LiuErNiHou liuErNiHou = new LiuErNiHou("六耳猕猴",sunwukong);
LiuErNiHou liu1 = (LiuErNiHou)liuErNiHou.clone();
LiuErNiHou liu2 = (LiuErNiHou)liuErNiHou.clone();
LiuErNiHou liu3 = (LiuErNiHou)liuErNiHou.clone();
LiuErNiHou liu4 = (LiuErNiHou)liuErNiHou.clone();
System.out.println(liuErNiHou "hashcode=>" liuErNiHou.getSunwukong().hashCode());
System.out.println(liu1 "hashcode=>" liu1.getSunwukong().hashCode());
System.out.println(liu2 "hashcode=>" liu2.getSunwukong().hashCode());
System.out.println(liu3 "hashcode=>" liu3.getSunwukong().hashCode());
System.out.println(liu4 "hashcode=>" liu4.getSunwukong().hashCode());
}
}
public class LiuErNiHou implements Serializable {
private String name;
private Sunwukong sunwukong;
public LiuErNiHou(String name, Sunwukong sunwukong) {
this.name = name;
this.sunwukong = sunwukong;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Sunwukong getSunwukong() {
return sunwukong;
}
public void setSunwukong(Sunwukong sunwukong) {
this.sunwukong = sunwukong;
}
@Override
public String toString() {
return "LiuErNiHou{"
"name='" name '\''
", sunwukong=" sunwukong
'}';
}
/**
* 序列化克隆
* @return
*/
public Object serClone() {
// 创建流对象
ByteArrayInputStream bis = null;
ByteArrayOutputStream bos = null;
ObjectInputStream ois = null;
ObjectOutputStream oos = null;
try {
// 序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
return (LiuErNiHou)ois.readObject();
}catch (Exception e) {
return null;
}finally {
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
推荐使用序列化方式实现深克隆
6.2、原型模式优缺点优点
- Java自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作
缺点
- 需要为每一个类都配置一个 clone 方法
- clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则
- 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。
原型模式通常适用于以下场景。
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
- 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
- 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。
在 Spring 中,原型模式应用的非常广泛,例如 scope='prototype'、JSON.parseObject() 等都是原型模式的具体应用。
七、建造者模式在软件开发过程中有时需要创建一个复杂的对象,这个复杂对象通常由多个子部件按一定的步骤组合而成。例如,计算机是由 CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的,采购员不可能自己去组装计算机,而是将计算机的配置要求告诉计算机销售公司,计算机销售公司安排技术人员去组装计算机,然后再交给要买计算机的采购员。
建造者模式的定义是将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,`它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成`。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
7.1、实现方式建造者模式由产品、抽象建造者、具体建造者、指挥者4部分组成
- 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件
- 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
- 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
- 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
我们以建造房子为例
7.1.1、原始方式
// 产品抽象类
public abstract class AbstractHouse {
// 打地基
public abstract void buildBase();
// 建墙体
public abstract void buildWalls();
// 封顶
public abstract void roofed();
// 构建
public void build() {
buildBase();
buildWalls();
roofed();
}
}
// 具体产品:普通房子
public class CommonHouse extends AbstractHouse{
@Override
public void buildBase() {
System.out.println("普通房子打10米地基");
}
@Override
public void buildWalls() {
System.out.println("普通房子砌10cm墙体");
}
@Override
public void roofed() {
System.out.println("普通房子用瓦片封顶");
}
}
// 具体产品:高层建筑
public class HeightBuilding extends AbstractHouse{
@Override
public void buildBase() {
System.out.println("普通房子打100米地基");
}
@Override
public void buildWalls() {
System.out.println("普通房子砌20cm墙体");
}
@Override
public void roofed() {
System.out.println("普通房子用钢筋混凝土封顶");
}
}
// 客户端
public class Client {
public static void main(String[] args) {
CommonHouse commonHouse = new CommonHouse();
commonHouse.build();
}
}
以上方式优点在于比较好理解,容易操作。但是该方式将产品本身和创建过程封装在一起,耦合性太强。我们可以使用建造者模式将产品和产品的建造过程解耦
7.1.2、建造者模式实现
// 产品
public class House {
private String baise;
private String wall;
private String roofed;
public String getBaise() {
return baise;
}
public void setBaise(String baise) {
this.baise = baise;
}
public String getWall() {
return wall;
}
public void setWall(String wall) {
this.wall = wall;
}
public String getRoofed() {
return roofed;
}
public void setRoofed(String roofed) {
this.roofed = roofed;
}
}
// 抽象建造者
public abstract class HouseBuilder {
// 创建的产品
protected House house = new House();
// 构建房子流程
public abstract void buildBasic();
public abstract void buildWalls();
public abstract void roofed();
public House getResult() {
return house;
}
}
// 具体建造者,建造普通住宅
public class CommonHouse extends HouseBuilder{
@Override
public void buildBasic() {
System.out.println("普通房子打地基5米");
}
@Override
public void buildWalls() {
System.out.println("普通房子打砌墙10cm");
}
@Override
public void roofed() {
System.out.println("普通房子盖屋顶");
}
}
// 具体建造者,建造高层建筑
public class HighBuilding extends HouseBuilder{
@Override
public void buildBasic() {
System.out.println("高层建筑打地基100米");
}
@Override
public void buildWalls() {
System.out.println("高层建筑砌墙20cm");
}
@Override
public void roofed() {
System.out.println("高层建筑盖屋顶");
}
}
// 指挥者
public class HouseDirector {
HouseBuilder houseBuilder = null;
public HouseDirector(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
public void setHouseBuilder(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
// 如何建造房子由指挥者决定
public House constructHouse() {
houseBuilder.buildBasic();
houseBuilder.buildWalls();
houseBuilder.roofed();
// 获取产品
return houseBuilder.getResult();
}
}
// 使用者
public class Client {
public static void main(String[] args) {
CommonHouse commonHouse = new CommonHouse();
// 创建房子指挥者
HouseDirector houseDirector = new HouseDirector(commonHouse);
House house = houseDirector.constructHouse();
System.out.println("=======创建高层建筑=======");
houseDirector.setHouseBuilder(new HighBuilding());
House house1 = houseDirector.constructHouse();
}
}
优点
- 封装性好,构建和表示分离
- 扩展性好,各个具体的构建者相互独立,有利于系统的解耦
- 客户端不必知道产品内部组成细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险
缺点
- 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大
建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。
7.3、建造者模式应用场景建造者模式唯一区别于工厂模式的是针对复杂对象的创建。也就是说,如果创建简单对象,通常都是使用工厂模式进行创建,而如果创建复杂对象,就可以考虑使用建造者模式。
当需要创建的产品具备复杂创建过程时,可以抽取出共性创建过程,然后交由具体实现类自定义创建流程,使得同样的创建行为可以生产出不同的产品,分离了创建与表示,使创建产品的灵活性大大增加。
- 相同的方法,不同的执行顺序,产生不同的结果。
- 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
- 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
- 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。
- 建造者模式注重建造过程的调用顺序,而工厂模式更注重创建对象
- 创建对象的复杂度不同,建造者模式创建复杂的对象,由各种复杂的部件组成。工厂模式创建出来的对象都一样。
- 关注点不同,工厂模式只需要将对象创建出来就行了,而建造者不仅要创建出对象,还要知道对象由哪些部分组成
- 建造者中创建顺序不一样,最终创建出来的对象也不一样。
本章节介绍了`创建型设计模式`,这些设计模式在实际开发中还都是比较常见的。而且设计模式是对思想上的一个升华,转变,需要时间来消化吸收,建议通过理论和代码实现来理解。再结合之前介绍的 `设计思想`来思考这些设计模式用到了哪些设计思想。
需要特别注意的是:设计模式的编码方式并不是固定的死格式,理解了它的理论和目的之后,根据实际场景编写程序。在程序复杂度和程序实现之间取舍。
,