10_00 创建模式-工厂-单例
1. 设计原则与设计模式
- 软件开发唯一的真理是“软件一定会变化”。因为你的软件解决的是现实生活中的业务问题,而现实生活中的业务流程总是在不停地变化。
做到OOD的代码需要满足的条件:
- 面向对象
- 复用
- 能以最小的代价满足变化
- 不用改变现有代码满足扩展
- 面向对象的设计原则:提高面向对象设计复用性
设计目标:
- 设计目可扩展性(Extensibility):新功能易加入系统。
- 灵活性(Flexibility):允许代码修改平稳发生,不会涉及很多其他模块。
- 可插入性(Pluggability):容易将一个类换为另一个具有同样接口的类。
软件复用重要性
- 较高的生产率
- 较高的软件质量
- 恰当使用复用,可改善系统的可维护性
面向对象设计
- 使一个系统可在更高的层次上提供了可复用性
- 抽象化和继承:使概念和定义可复用
- 多态:使实现和应用可复用
- 抽象化和封装:可保持和促进系统的可维护性
复用
- 抽象层次是一个应用系统作战略性判断和决定的地方,那么抽象层次就应当是较为稳定的,应当是复用的重点。
- 复用的焦点不再集中在函数和算法等具体实现细节上,而是集中在最重要的含有宏观商业逻辑的抽象层次上。
- 既然如果抽象层次的模块相对独立于具体层次的模块的话,那么具体层次内部的变化就不会影响到抽象层次的结构,所以抽象层次的复用就会较为容易。
- 面向对象设计中,可维护性复用是以设计原则和设计模式为基础的。
面向对象设计原则(重要)
- 开闭原则OCP: Open-Closed Principle
- 里氏代换原则LSP: Liskov Substitution Principle
- 依赖倒置原则DIP: Dependency Inversion Principle
- 接口隔离原则ISP: Interface Segregation Principle
- 组合复用原则CRP: Composition Reuse Principle
- 迪米特法则LoD: Law of Demeter
- 单一职责原则(SRP)
2.开-闭原则OCP
软件组成实体应该是对扩展开放的(可扩展的),但是对修改是关闭的。
- 开放-封闭法则认为应该试图去设计出永远也不需要改变的模块。
- 关键在于抽象化:可给系统定义一个一劳永逸,不再更改的抽象设计,此设计允许有无穷无尽的行为在实现层被实现。抽象层预见所有扩展。
- 一个软件系统的所有模块 不可能都满足 OCP,但是应该努力最小化这些不满足OCP的模块数量。
3. 设计模式
- 设计模式的思想根源是基本原则的宏观运用,本质上是没有任何模式的
- 设计模式就是实现了基本原则,从而达到了代码复用、增加可维护性的目的。
什么叫模式?
- “每一个模式描述了在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,就能一次又一次地使用该解决方案而不必重复劳动”
- 尽管软件技术发展非常快,但是仍然有非常多的设计模式可以让我们套用
- 设计模式可以帮助人们简便地复用以前成功的设计方案,提高工作效率
设计模式的目的?
- 为了提高代码可重用性、让代码更容易被他人理解、保证代码可靠性。
- 设计模式使代码编写真正工程化。
- 设计模式是软件工程的基石脉络,如同大厦的结构一样。
- 学习如何合理地组织我们的代码,如何解耦,如何真正地达到对修改封闭对扩展开放的效果
设计模式基本要素
- 模式名称(pattern name)
- 问题( problem)
- 解决方案( solution)
- 效果( consequences)
设计模式的分类
- 创建型:抽象了对象实例化的过程
- 结构型:如何组合类和对象以获得更大的结构
- 行为型:描述算法和对象间职责的分配
4.设计模式-工厂模式(重要)
- 简单工厂模式(Simple Factory):不利于产生系列产品;
- 工厂方法模式(Factory Method):又称为多形性工厂;
- 抽象工厂模式(Abstract Factory):又称为工具箱,产生产品族,但不利于产生新的产品;
- 这三种模式从上到下逐步抽象,并且更具一般性。
简单工厂
- 客户端不但知道了接口,同时还知道了具体的实现就是Impl。
- 接口的思想是“封装隔离”,而Impl这个实现类,应该是被接口Api封装并同客户端隔离开的,也就是说,客户端不应该知道具体的实现类是Impl。
疑惑:什么叫创建实例的类可以是接口、抽象类?
简单工厂的思路
- 虽然不能让模块外部知道模块内的具体实现,但是模块内部是可以知道实现类的,而且创建接口是需要具体实现类的。
- 在模块内部新建一个类,在这个类里面来创建接口(实现)对象,然后把创建好的接口对象返回给客户端
- 这样外部应用就只需要根据这对外交互的类来获取相应的接口对象,然后就可以操作接口定义的方法了。
- 这样的类称为简单工厂,即Factory
效果
- 客户端可以通过Factory来获取需要的接口对象
- 客户端调用接口的方法来实现需要的功能
- 客户端不再关心具体实现了。
/** 接口的定义,该接口可以通过简单工厂来创建*/
public interface Api {
/** * 示意,具体的功能方法的定义 */
public void operation(String s);
}
/*** 接口的具体实现对象A . */
public class ImplA implements Api{
public void operation(String s) {
//实现功能的代码,示意一下
System.out.println("ImplA s=="+s);
}
}
/** * 工厂类,用来创造Api对象 */
public class Factory {
/** * 具体的创造Api对象的方法
* @param condition 示意,从外部传入的选择条件 ; @return 创造好的Api对象 */
public static Api createApi(int condition){
//根据某些条件去选择究竟创建哪一个具体的实现对象, 这些条件可以从外部传入,也可以从其它途径获取。如果只有一个实现,可省略条件。
Api api = null;
if(condition == 1){
api = new ImplA();
}else if(condition == 2){
api = new ImplB(); }
return api; } }
/** * 客户端,使用Api接口 */
public class Client {
public static void main(String[] args) { //通过简单工厂来获取接口对象
Api api = Factory.createApi(1);
api.operation("正在使用简单工厂");
} }
- 简单工厂方法的内部主要实现的功能是“选择合适的实现类”来创建实例对象。
- 实现选择,需要选择的条件或者是选择的参数,其来源通常有几种:
- 来源于客户端,由Client来传入参数
- 来源于配置文件,从配置文件获取用于判断的值
- 来源于程序运行期的某个值,比如从缓存中获取某个运行期的值
- 缺点:由于是从客户端在调用工厂的时候,传入选择的参数,这就说明客户端必须知道每个参数的含义,也需要理解每个参数对应的功能处理。这就要求必须在一定程度上,向客户暴露一定的内部实现细节。
优缺点
- 帮助封装
简单工厂虽然很简单,但是非常友好地帮助我们实现了组件的封装,然后让组件外部能真正面向接口编程。 - 解耦
通过简单工厂,实现了客户端和具体实现类的解耦。
客户端根本就不知道具体是由谁来实现,也不知道具体是如何实现的,客户端只是通过工厂获取它需要的接口对象。 - 可能增加客户端的复杂度
如果通过客户端的参数来选择具体的实现类,那么就必须让客户端能理解各个参数所代表的具体功能和含义,这会增加客户端使用的难度,也部分暴露了内部实现,这种情况可以选用可配置的方式来实现。 - 简单工厂的本质是:选择实现。
- 实现简单工厂的难点就在于 “如何选择”实现,前面讲到了几种传递参数的方法,那都是静态的参数,还可以实现成为动态的参数。
比如:在运行期间,由工厂去读取某个内存的值,或者是去读取数据库中的值,然后根据这个值来选择具体的实现等等。
5. 工厂方法
- 定义一个创建产品对象的工厂接口,让子类决定实例化哪一个类,将实际创建工作推迟到子类当中
- 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。
/* 工厂方法所创建的对象的接口*/
abstract class Product {
//可以定义Product的属性和方法
}
/** * 具体的Product对象 */
public class ConcreteProduct extends Product {
//实现Product要求的方法
}
/*** 客户端使用Facotry对象的情况下,Factory的基本实现结构*/
abstract class Factory {
public abstract Product factoryMethod();
}
/**具体的创建器实现对象*/
class ConcreteFactory extends Factory {
public Product factoryMethod() {
return new ConcreteProduct();
} }
/*客户端程序*/
Factory factory;
factory = new ConcreteFactory();
//可通过配置文件实现
Product product;
product = factory.factoryMethod();
……
在实际使用时,具体工厂类在实现工厂方法时除了创建具体产品对象之外,还可以负责产品对象的初始化工作以及一些资源和环境配置工作,例如连接数据库、创建文件等。
//日志记录器接口:
//抽象产品
interface Logger {
public void writeLog(); }
//数据库日志记录器:具体产品
class DatabaseLogger implements Logger {
public void writeLog() {
System.out.println("数据库日志记录");
} }
//文件日志记录器:具体产品
class FileLogger implements Logger {
public void writeLog() {
System.out.println("文件日志记录。");
} }
//日志记录器工厂接口:抽象工厂
interface LoggerFactory {
public Logger createLogger(); }
//数据库日志记录器工厂类:具体工厂
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//连接数据库,代码省略 ;创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器代码省略
return logger; } }
//文件日志记录器工厂类:具体工厂
class FileLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//创建文件日志记录器对象
Logger logger = new FileLogger();
//创建文件,代码省略
return logger;
} }
class Client {
public static void main(String args[]) {
LoggerFactory factory;
Logger logger;
//可引入配置文件实现
factory = new FileLoggerFactory();
logger = factory.createLogger();
logger.writeLog();
}
}
简单工厂的优缺点
- 优点:实现了责任分割
- 利用判断逻辑,决定实例化哪一个产品类
- 客户端可以免除直接创建产品类对象的责任,仅仅使用该产品
- 缺点:没有完全做到“开-闭”
- 一旦增加新的产品,需要修改工厂的代码
- 但是客户代码不需要修改
简单工厂的问题
- 所有具体产品对象的创建都放在一个类中,一旦增加新的产品,当然工厂类要被修改
- 工厂方法:使用多态来应对
- 提供一个抽象工厂的接口
- 具体工厂分别负责创建具体产品对象
- 增加新的产品只需要相应增加新的具体工厂类
Factory Method(工厂方法)模式
- 意图
- 定义一个用于创建对象的接口,让实现类(具体工厂类)决定实例化哪一个类
- 使一个类的实例化延迟到实现类
- 优点
- 封装了创建具体对象的工作
- 使得客户代码“针对接口编程”,保持对变化的**“关闭”(产品)**
- 缺点
- 虽然保证了工厂方法内的对修改关闭,但对于使用工厂方法的类,如果要更换另外一种产品,仍然需要修改实例化的具体工厂类
(原本这个工厂生产A 但是想改为生产B了 需要把产品把A换成B)
6. 抽象工厂
- 工厂方法模式或简单工厂关注的是单个产品对象或产品类的创建,比如创建CPU的工厂方法,它就只关心如何创建CPU的对象,而创建主板的工厂方法,就只关心如何创建主板对象。
- 如何创建一系列的产品类对象,而且这一系列类对象是构建新的产品对象所需要的组成部分,也就是这一系列被创建的对象相互之间是有约束的。
/** * 抽象工厂的接口,声明创建抽象产品对象的操作 */
public interface Factory {
/** * 示例方法,创建抽象产品A的对象
@return 抽象产品A的对象 */
public ProductA createProductA();
/** * 示例方法,创建抽象产品B的对象
@return 抽象产品B的对象 */
public ProductB createProductB();
}
/** * 抽象产品A的接口 */
public interface ProductA {
//定义抽象产品A相关的操作
}
/**
/ * 产品A的具体实现 */
public class ProductA1 implements ProductA {
//实现产品A的接口中定义的操作
}
public class ProductA2 implements ProductA {
//实现产品A的接口中定义的操作
}
//ProductB系列是类似的代码
/** * 具体的工厂实现对象,实现创建具体的产品对象的操作 */
public class Factory1 implements Factory {
public ProductA createProductA() {
return new ProductA1();
}
public ProductB createProductB() {
return new ProductB1();
}
}
/** * 具体的工厂实现对象,实现创建具体的产品对象的操作 */
public class Factory2 implements Factory {
public ProductA createProductA() {
return new ProductA2();
}
public ProductB createProductB() {
return new ProductB2();
}
}
public class Client {
public static void main(String[] args) {
//创建抽象工厂对象
Factory af = new Factory1();
//通过抽象工厂来获取一系列的对象,如产品A和产品B
af.createProductA();
af.createProductB();
}
}
抽象工厂模式的本质:选择产品簇的实现。(重要)
- 工厂方法是选择单个产品的实现,虽然一个类里面可以有多个工厂方法,但是这些方法之间一般是没有联系的,即使看起来像有联系。
- 抽象工厂着重的就是为一个产品簇选择实现,定义在抽象工厂里面的方法通常是有联系的,它们**都是产品的某一部分或者是相互依赖的。**如果抽象工厂里面只定义一个方法,直接创建产品,那么就退化成为工厂方法了。
何时选用抽象工厂模式
建议在如下情况中,选用抽象工厂模式:
- 如果希望一个系统独立于它的产品的创建,组合和表示的时候,即,希望一个系统只是知道产品的接口,而不关心实现的时候。
- 如果一个系统要由多个产品系列中的一个来配置的时候,即,可以动态地切换产品簇的时候。
- 如果要强调一系列相关产品的接口,以便联合使用它们的时候。
抽象工厂与工厂方法
- 工厂方法模式一般是针对单独的产品对象的创建,而抽象工厂模式注重产品簇对象的创建,这是它们的区别。
- 如果把抽象工厂创建的产品簇简化,这个产品簇就只有一个产品,那么这个时候的抽象工厂跟工厂方法是差不多的,也就是抽象工厂可以退化成工厂方法,而工厂方法又可以退化成简单工厂,这是它们的联系。
- 抽象工厂的需求:创建一个产品家族
-负责在抽象工厂中创建产品的方法,通常是以“工厂方法”来实现的
7. 设计原则:依赖倒置原则DIP
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象;
- 抽象不应该依赖细节;细节应该依赖抽象。
- 抽象(抽象类或接口)不应该依赖于细节(具体实现类)
- 细节(具体实现类)应该依赖抽象
- 抽象:即抽象类或接口,两者是不能够实例化的。
- 细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化。
- 而依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合。
- 这个原则也是OO设计原则中最难以实现的了,如果没有实现这个原则,那么也就意味着开闭原则(对扩展开放,对修改关闭)也无法实现。
DIP常见应用
- AGP插槽。主板和显卡之间关系的抽象。主板和显卡通常是使用AGP插槽来连接的,这样,只要接口适配,不管是主板还是显卡更换,都不是问题。
- 驾照。司机和汽车之间关系的抽象。有驾照的司机可以驾驶各种品牌汽车。
- 电源插座。
- 设计模式中最能体现DIP原则的是抽象工厂模式。在抽象工厂模式中,工厂和产品都可以是抽象的,如果客户要使用的话,只要关注于工厂和产品的接口即可,不必关注于工厂和产品的具体实现。
疑惑:为什么是抽象工厂???
- 依赖倒置原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合
- 采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,减少并行开发引起的风险,提高代码的可读性和可维护性。
遵循倒置依赖原则的指导方针
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
- 变量不可以持有具体类的引用
- 如果使用new,就会持有具体类的引用
- 可以改用工厂来避开这样的做法
- 不要让类派生自具体类
- 如果派生自具体类,就会依赖具体类
- 请派生自一个抽象(接口或抽象类)
- 不是随时都要遵循这个原则
- 如直接实例化字符串对象
依赖倒置有三种方式来实现
- 通过构造函数传递依赖对象
比如在构造函数中的需要传递的参数是抽象类或接口的方式实现。 - 通过setter方法传递依赖对象
即在setXXX方法中的参数为抽象类或接口,来实现传递依赖对象 - 接口声明实现依赖对象
即在函数声明中参数为抽象类或接口,来实现传递依赖对象
通过构造函数传递依赖对象
在类中通过构造函数声明依赖对象,按照依赖注入的说法这种方式叫做构造函数注入
通过setter方法传递依赖对象
在抽象中设置setter方法声明依赖关系,依照依赖注入的说法就是setter依赖注入
接口声明实现依赖对象
理解“倒置”
- “正置”:依赖正置就是类间的依赖是实实在在地实现类间的依赖,也就是面向实现编程,这也是正常人的思维方式,我要开奔驰车就依赖奔驰车,我要使用笔记本电脑就直接依赖笔记本电脑
- 而编写程序需要的是对现实世界的事物进行抽象,抽象的结果就是有了抽象类和接口,然后我们根据系统设计的需要产生了抽象间的依赖,代替了人们传统思维中的事物间的依赖,“倒置”就是从这里产生的。
问题: - 司机类和奔驰车类之间是一个紧耦合的关系,其导致的结果就是系统的可维护性大大降低。
- 代码可读性降低,两个相似的类需要阅读两个文件。
- 系统稳定性降低,这里只是增加了一个车类就需要修改司机类,这不是稳定性,这是易变性。被依赖者的变更让依赖者来承担修改的成本。
设计原则:针对接口编程
“针对接口编程”的一些建议
- 变量、参数、返回值等应声明为抽象类
- 不要继承非抽象类
- 不要重载父类的非抽象方法
当然这些只是建议
实际情况要权衡利弊
设计模式-Singleton单件/单例模式
- 单例模式:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例单例模式。
- 如何实例化一个唯一的对象
- 这样做的用途
- 有些对象我们只需要一个:
注册表 日志 线性池 缓存
单例模式的要点:
- 某个类只能有一个实例;
- 它必须自行创建这个实例;
- 它必须自行向整个系统提供这个实例。
经典单件模式实现
这个经典模式多个线程处理时不能获得同一个实例
处理多线程
多了一个 synchronized
急切(饿汉式)创建实例
双重检查加锁
关于双重加锁
- 被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确地处理该变量。
- 注意:在Java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java5及以上的版本。
- 由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高,因此一般建议,没有特别的需要,不要使用。
END