软件工程和软件的生命周期
什么是软件工程?
采用工程化的方法来开发和维护软件,把工程管理技术和技术方法结合起来,以经济的开发出高质量的软件,有效的去维护它,将开发、系统化、规范化、可定量化的过程,就是软件工程
软件的生命周期
软件生命周期(开发阶段),在每个开发阶段都要产生健全的,符合工程规范的文档,因此,软件产品不仅仅是程序,而是这些文档的总和
在不同的生命周期阶段,需要提供的文档如:
1、可行性分析阶段:可行性分析报告
从经济可行性,技术可行性,操作可行性,法律可行性等方面研究论证项目的可行性
2、需求分析阶段:软件需求规格说明书
明白软件“做什么”
3、系统设计阶段:概要设计、详细设计、数据库设计说明
明白“怎么做”,需求分析阶段回答“做什么”的问题,而系统设计阶段要回答“怎么做”的问题。
4、系统实现阶段(编码):通过单元测试的源代码
5、测试阶段:软件测试报告
6、维护阶段:软件问题报告、软件变动记录、软件维护记录。
- 改正性维护:诊断和改正系统使用过程中发现的软件错误
- 适应性维护:修改软件以适应环境的变化
- 完善性维护:根据用户的要求改进或扩充软件使它更完善
- 预防性维护:即修改软件为将来的维护活动做准备
而为了更好的使软件生命周期的任务可以有序的进行,用一定的软件工程模型对各阶段的各项任务予以规划和约束
常用的工程模型:
瀑布模式(Waterfall-Model)
反映软件生命周期各个阶段的明确任务,自上而下,逐级过渡的结构模式,项目开发阶段从一个阶段“流向”另一个阶段,因此就把这种结构模式称为瀑布模型
特点:
- 严格按照需求 ->分析->设计->编码->测试的阶段进行
- 阶段间具有顺序性和依赖性(必须要等到前一个阶段的工作完成之后,才能顺序开启后一阶段的工作)
- 质量保证(每个阶段都必须完成后(文档交付),才能进行下一阶段,每个阶段结束前,都会对完成的文档进行评审,以便及早的发现问题,改正错误)
优点:
- 可以保证整个软件产品较高的质量
- 保证缺陷能够提前的被发现和解决
- 可以保证系统在整体上的充分把握,使系统具备良好 的扩展性和可维护性
缺点:
- 缺乏灵活性,太过线性理想化( 每次需求发生变更都要从头再来)
- 前期就需要把需求做到最全。所以对于前期需求不明确,而又很难短时间明确清楚的项目则很难很好的利用瀑布模型。
- 瀑布模型强调的保证软件的质量,往往忽略人力,时间,资源等成本因素。对于中小型的项目,需求设计和开发人员往往在项 目开始后就会全部投入到项目中,而不是分阶段投入,因此采用瀑布模型会导致项目人力资源过多的闲置的情况
螺旋模型(Spiral-Model)
螺旋模型将开发过程分为几个螺旋周期,每个螺旋周期大致和瀑布模型相符合,螺旋模型沿着螺旋线旋转,即在坐标的4个象限上分别表示了4个方面的活动,如图所示:
优点:
- 对可选方案和约束条件的强调有利于已有软件的重用,也有助于把软件质量作为软件开发的一个重要目标;
- 减少了过多的测试(浪费资金)或测试不足(产品故障多)所带来的风险;
- 在螺旋模型中维护只是模型的另一个周期,在维护和开发之间并没有本质区别;
- 它是风险驱动的方法体系;
缺点:
- 采用螺旋模型需要具有相当丰富的风险评估经验和专门知识
- 在风险较大的项目开发中,如果未能够及时标识风险,会造成重大的损失
- 过多的迭代次数会增加开发成本,延迟提交时间。
增量模型(Incremental-Model)
在增量模型中,软件被作为一系列的增量构件来设计、实现、集成和测试,每一个构件是由多种相互作用的模块所形成的提供特定功能的代码片段构成。
增量模型在各个阶段并不是交付一个可运行的完整产品,而是交付满足客户需求的一个子集可运行产品。整个产品被分解成若干个构件,开发人员逐个构件地交付产品,这样做的好处是软件开发可以较好地适应变化,客户可以不断地看到所开发的软件,从而降低开发风险。
在使用增量模型时,第一个增量往往是实现基本需求的核心产品。核心产品交付用户使用后,经过评价形成下一个增量的开发计划,它包括对核心产品的修改和一些新功能的发布。这个过程在每个增量发布后不断重复,直到产生最终的完善产品。
例如,使用增量模型开发字处理软件。可以考虑,第一个增量发布基本的文件管理、编辑和文档生成功能,第二个增量发布更加完善的编辑和文档生成功能,第三个增量实现拼写和文法检查功能,第四个增量完成高级的页面布局功能。
优点:
- 采用增量模型的优点是人员分配灵活,刚开始不用投入大量人力资源
- 如果核心产品很受欢迎,则可增加人力实现下一个增量
- 增量能够有计划的管理技术风险
- 能满足客户的需求,顺应市场变化。
- 代价低:即时开发的增量不满足用户需求,那么对一个增量进行修改,相比对整个软件进行修改的代价要小得多
缺点:
- 并行开发构件有可能遇到不能集成的风险,软件必须具备开放式的体系结构
- 增量模型的灵活性可以使其适应这种变化的能力大大优于瀑布模型和快速原型模型,但也很容易退化为边做边改模型,从而是软件过程的控制失去整体性
喷泉模型 ( Fountain-Model )
以用户需求为动力,以对象为驱动的模型,主要就是用来描述面向对象的软件开发过程。
喷泉模型与传统的结构化生存期比较,具有更多的增量和迭代性质,生存期的各个阶段可以相互重叠和多次反复,而且在项目的整个生存期中还可以嵌入子生存期。就像水喷上去又可以落下来,可以落在中间,也可以落在最底部。
优点:
- 可以提高软件项目开发效率,节省开发时间,适应于面向对象的软件开发过程.
缺点:
- 开发过程中,就需要大量的开发人员(人力成本就很大)
- 面临可能随时加入各种信息、需求和资料的情况
什么是设计模式?
软件工程中,设计模式是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。
设计模式的目的?
编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,而设计模式能让程序,具有更好的:
1. 代码重用性
2. 可读性
3. 可扩展性
4. 可靠性
5. 使程序呈现高内聚,低耦合的特性
设计模式的七大原则
一、单一职责原则
对于一个类而言,应该仅有一个引起它变化的原因,永远不要让一个类存在多个改变的理由。一个类只应该做一个与职责相关的业务,不应该把过多的职责放在一个类中完成
二、接口隔离原则
使用专门的接口比用统一接口好,便于项目的组织和分工。不要让开发者面对用不到的方法。
三、开闭原则
一个软件实体(类、模块、方法等)应该对扩展开放,对修改关闭。开闭原则是设计原则的核心原则。软件需要变化时,尽量通过扩展软件实体的方式来完成,而不是通过修改原有代码来完成
四、里氏替换原则
在一个软件系统中,子类应该能够完全替换任何父类能够出现的地方,并且在经过替换后,不会让调用父类的客户程序从行为上有任何改变。
五、依赖倒置原则
高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象,
而依赖倒置的中心思想是面向抽象编程。
六、聚合/组合原则
尽可能使用聚合/组合的方式,而不是使用继承来达到代码的复用
七、迪米特法制
一个对象应该对其他对象保证最少的了解,要很好的封装类,仅向外部暴露你提供的服务,内部的细节不需要让外界知道。
UML类图
UML
统一建模语言,是一种用于软件系统设计和架构建模的一种可视化建模语言。
UML从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等9种图
类图概述
显示系统中类,接口以及它们之间的静态结构和关系的一种静态模型,其中最基本的元素是类和接口
举例:
用类图来表示类
- 代码形式
public class Student{
private int age;
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}
- 类图形式
- 属性的完整表示方式是:可见性 名称:类型 [ = 初始化值 ](可以有初始化值可以没有)
- 方法的完整表示方式是:可见性 名称(参数列表)[ : 返回类型]
注意:
- 类图是一个矩形,并且划分了三层,第一层显示类的名称,第二层显示类的特性–属性,第三层显示类的行为–方法
- 特例:抽象类和抽象方法以斜体表示
- 访问修饰符:
- -:私有(private)
- +:公有(public)
- #:受保护(protected)
类图表示接口:
- 代码示例
public interface Run{
void longRun();
void sprint();
}
- 类图形式
注意:用一个双层的矩形表示接口,第一层,头部会有<>,然后在其下方书写接口名称,第二层,接口方法
用类图表示类与类的关系
一、关联关系
关联关系是对象对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,A类中有B类的成员变量。
使用:实线+箭头,由拥有者指向被拥有者
二、聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间关系。
一个类是另一个类的属性,但是和关联关系不同,这两个类之间有逻辑关系。
例如学校和老师的关系,学校包含老师,但如果学习停办了,老师依然存在
使用:带空心菱形的实心线,菱形指向整体。
三、组合关系
是整体与部分的关系,但部分不能离开整体而单独存在,属于强聚合。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在了,部分对象也将不存在,部分对象不能脱离整体对象而存在。
例如:头和嘴的关系,没有了头,嘴也就不存在了。
使用:带实心菱形的实线,菱形指向整体
四、依赖关系
如果A指向B,则说明A中使用了B,使用方式包括,A类中有B类的实例化对象局部变量;A类中有方法把B类实例化对象当做了参数
使用:虚线+箭头表示,由使用者指向被使用者
五、继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系
使用:实线+空心三角形表示,由子类指向父类
六、实现关系
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所有声明的所有的抽象操作。
使用:虚线+空心三角形表示,由实现类去指向接口
设计模式的分类
设计模式分为3种类型,共23种
一、创建型模式
创建一些特殊的对象,或者在特殊要求下创建对象
创建型模式又分为5种:
单例模式
- 单例设计模式:
- 类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。核心思想是,无论使用该类的方法多少次,都只产生一个该类的对象,可以很好的减少系统内存的消耗
- 单例模式的结构
- 单例模式主要有以下角色:
- 单例类:只能创建一个实例的类
- 访问类:使用单例的类
- 单例模式主要有以下角色:
- 单例模式的实现
- 单例模式有八种方式:
一、饿汉模式(静态常量)
步骤如下:
1. 构造器私有化(防止被new)
2. 类的内部创建对象
3. 向外部提供一个静态的公共方法
4. 代码实现:
/**
* 访问类
*/
public class Test{
public static void main(String[] args){
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2) // true
}
}
/**
* 饿汉模式(静态常量)
*/
class Singleton{
// 构造器私有化
private Singleton(){}
// 类的内部创建对象
private static Singleton singleton = new Singleton();
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return singleton;
}
}
优点:
- 写法简单,在类加载的时候就完成实例化。避免了线程同步问题,执行效率高。
缺点
- 类加载时,会直接实例化单例对象,不管我们有没有使用到该单例对象,浪费内存
二、饿汉模式(静态代码块)
步骤如下:
1. 构造器私有化(防止被new)
2. 类的内部创建对象
3. 在静态代码块中,创建单例对象
4. 向外部提供一个静态的公共方法
5. 代码实现:
/**
* 访问类
*/
public class Test{
public static void main(String[] args){
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2) // true
}
}
/**
* 饿汉模式(静态代码块)
*/
class Singleton{
// 构造器私有化
private Singleton(){}
// 类的内部创建对象
private static Singleton singleton;
//在静态代码块中,创建单例对象
static{
singleton = new Singleton();
}
// 提供一个公有的静态方法,返回实例对象
public static Singleton getInstance(){
return singleton;
}
}
优缺点说明:
这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面一致。
三、懒汉模式(线程不安全)
步骤如下:
1. 构造器私有化(防止被new)
2. 类的内部创建对象
3. 向外部提供一个静态的公共方法,当使用到该方法时,才去创建 singleton;
4. 代码实现:
/**
* 懒汉模式(线程不安全)
*/
public class LazySingleton{
// 构造器私有化
private LazySingleton(){}
// 类的内部创建对象
private static LazySingleton singleton;
// 提供一个静态的公有方法,当使用到该方法时,才去创建 singleton;
public static LazySingleton getInstance(){
if (singleton == null){
singleton = new LazySingleton();
}
return singleton;
}
优点:
- 不会占用内容
缺点:
- 在单线程情况下是安全的,但是如果在多线程的情况下,当一个线程进入了if(singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时就会产生多个实例。不建议使用
四、懒汉模式(线程安全,同步方法)
步骤如下:
1. 构造器私有化(防止被new)
2. 类的内部创建对象
3. 向外部提供一个静态的公共方法,当使用到该方法时,才去创建 singleton,加入同步处理的代码,解决线程安全问题;
4. 代码实现:
/**
* 懒汉模式(线程安全,同步方法)
*/
public class LazySingleton{
// 构造器私有化
private LazySingleton(){}
// 类的内部创建对象
private static LazySingleton singleton;
// 提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题;
public static synchronized LazySingleton getInstance(){
if (singleton == null){
singleton = new LazySingleton();
}
return singleton;
}
优点:
- 解决了线程安全
缺点:
- 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。方法进行同步效率太低。不建议使用
五、懒汉模式(双重检查锁)
步骤如下:
1. 构造器私有化(防止被new)
2. 类的内部创建对象
3. 向外部提供一个静态的公共方法,当使用到该方法时,才去创建 singleton,加入双重检查代码,解决线程安全问题;
4. 代码实现:
/**
* 懒汉模式(双重检查锁)
*/
public class LazySingleton{
// 构造器私有化
private LazySingleton(){}
// 类的内部创建对象
private static LazySingleton singleton;
// 提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载的问题;
/*
双重检验 首先先判断实例是否为null,
如果为null则用synchronized锁住类,
然后再同步块中,再一次判断实例是否为null,
为null则初始化实例
*/
public static LazySingleton getInstance(){
if (singleton == null){ // 判断对象是否被初始化
// 对象没有被初始化,则加上类锁
synchronized (LazySingleton.class) {
// 同时只有一个线程能够到这里
if (singleton == null) {
// 创建实例对象
singleton = new LazySingleton();
}
}
}
// 返回实例对象的引用
return singleton;
}
}
优点:
- 线程安全;延迟加载;效率高
- 这样,实例化代码只用执行一次,后面再次访问时,判断if(singleton == null),直接return实例化对象,也避免了反复进行方法同步。
六、静态内部类
步骤如下:
1. 构造器私有化(防止被new)
2. 写一个静态内部类,该类中有一个静态属性instance
3. 向外部提供一个静态的公共方法,直接返回Inner.instance;
4. 代码实现:
public class InnerSingleton {
private InnerSingleton(){}
// 静态内部类
private static class Inner{
private static InnerSingleton instance = new InnerSingleton();
}
public static InnerSingleton getInstance(){
return Inner.instance;
}
优点:(推荐使用)
- 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
- 静态内部类方法在InnerSingleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance 方法时,才会装载Inner类,从而完成InnerSingleton 的实例化。
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM会保证线程的安全,在类进行初始化时,别的线程是无法进入的,这样避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。
七、枚举
步骤如下:
1. 构造器私有化(防止被new)
2. 写一个枚举类,自定义一个枚举值,
3. 向外部提供一个静态的公共方法,直接返回 SinEnum.TEST.es;
4. 代码实现:
/**
* 通过枚举创建 单例模式
* 实现单例的最佳方法,更加简洁,支持自动化序列机制,防止多次实例化,
*/
public class EnumSingleton {
private EnumSingleton(){}
private static enum SinEnum{
// 自定义的枚举值 如果没有该自定义枚举值 就无法获取枚举对象
TEST;
private EnumSingleton es = new EnumSingleton();
}
public static EnumSingleton getInstance(){
return SinEnum.TEST.es;
}
}
优点:不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
原型模式
- 概述:
- 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的对象
- 结构:
- 原型模式包含如下角色:
1. 抽象原型类:规定了具体原型对象必须实现clone()方法
2. 具体原型类:实现抽象原型类的clone()方法,它是可被复制的对象。
3. 访问类:使用具体原型类中的clone()方法来复制新的对象
- 原型模式的克隆分为浅克隆和深克隆
1. 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,属性中的引用类型,仍指向原有属性所指向的对象的内存地址。
2. 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
- 代码:
/**
* 人类
* 想要调用Object的clone方法,原型类需要实现Cloneable接口
*/
public class Man implements Cloneable,Serializable{
/**
* 姓名
*/
private String name;
/**
* 人拥有的汽车
*/
public Car car=new Car();
// 浅度克隆的实现
// 调用Object的clone方法
public Man clone() throws CloneNotSupportedException{
Object obj= super.clone();
return (Man) obj;
}
// 深度克隆的实现
public Man depthClone() throws IOException,ClassNotFoundException{
// 获取对象信息,把当前对象写入另一块内存
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(bo);
objOut.writeObject(this);
// 读取内存 创建对象
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream objIn = new ObjectInputStream(bi);
Object obj = objIn.readObject();
return (Man) obj;
}
}
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
Man man = new Man();
Man man1 = man.clone();
System.out.println(man == man1); // false
System.out.println(man.car == man1.car);// turn
Man man2 = man.depthClone();
System.out.println(man2 == man);// false
System.out.println(man2.car == man.car);// false
}
}
注意:深度克隆的克隆对象所涉及的自定义类,需要实现序列化接口,在需要克隆的类中,添加一个方法,完成序列化反序列化即可。
工厂方法模式
- 工厂模式:
- 在Java中,如果对象创建的时候直接new该对象,就会对该对象耦合严重,如果需要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。这时如果使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果需要更换对象,直接在工厂里更换该对象即可,达到了与对象解藕的目的。
- 核心思想:将对象的创建和对象的使用分开进行
- **分类**:
一、简单工厂模式
- 结构
- 简单工厂包含如下角色:
- 抽象产品 :定义了产品的规定,描述了产品的主要特性和功能。
- 具体产品:实现或者继承抽象产品的子类
- 具体工厂:提供了创建产品的方法,调用者通过该方法来获取产品。
- 简单工厂包含如下角色:
- 例如
- A B C — 具体的产品— 抽象产品(接口)
根据抽象出产品去创建实际的产品—此时只需要知道产品类型(名称)
假设有两样产品 手机 平板 ----- 都有行为 玩 - 步骤如下:
- A B C — 具体的产品— 抽象产品(接口)
- 定义了一个Product接口,含有方法play
public interface Product {
void play();
}
- 提供实现Product接口的实现类
// 手机
public class Phone implements Product{
@Override
public void play() {
System.out.println("play phone");
}
}
// 平板
public class IPad implements Product{
@Override
public void play() {
System.out.println("play IPad");
}
}
- 定义工厂类(Factory)
public class Factory {
public Product getProduct(String type){
if (type == null){
return null;
}
if (type.equals("Phone")){
System.out.println("生产 Phone");
return new Phone();
}else if (type.equals("IPad")){
System.out.println("生产 IPad");
return new IPad();
}
return null;
}
}
- 通过工厂类获取想要的对象,不用再关心具体的细节
public class Test{
public static void main(String[] args) {
Factory factory = new Factory();
// 需要一个手机, 只需要告知给工厂即可,不用管任何的细节,屏蔽了具体的实现
Product Phone = factory.getProduct("Phone");
Product IPad = factory.getProduct("IPad");
Product no = factory.getProduct("no");
Phone.play();
IPad.play();
System.out.println(no);
}
}
优点:
- 封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在源代码中修改,这样就降低了客户代码修改的可能性,更容易扩展
缺点
- 增加新的产品还是需要修改工厂类的代码,违背了“开闭原则”
二、工厂方法模式
针对简单工厂中的缺点,使用工厂方法可以很好的解决其中的问题,完全遵循开闭原则
- 概念:
- 定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象,工厂方法使一个产品类的实例化延迟到其工厂的子类
- 结构:
- 工厂方法模式的角色:
- 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品:定义了产品的规范,描述了产品的主要功能和特性。
- 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应
- 工厂方法模式的角色:
- 步骤如下:
- 定义抽象产品接口
public interface Product {
void play();
}
- 根据抽象产品接口,提供实现产品接口的实现类
// 手机
public class Phone implements Product{
@Override
public void play() {
System.out.println("play phone");
}
}
// 平板
public class IPad implements Product{
@Override
public void play() {
System.out.println("play IPad");
}
}
- 定义抽象工厂接口
/**
* 抽象工厂
*/
public interface Factory {
/** 方法返回值 抽象产品 */
public Product create();
}
- 为不同的具体产品,给出不同的具体工厂类
// 平板工厂
public class IPadFactory implements Factory{
@Override
public Product create() {
System.out.println("生产 IPad");
return new IPad();
}
}
// 手机工厂
public class PhoneFactory implements Factory{
@Override
public Product create() {
System.out.println("生产 Phone");
return new Phone();
}
}
- 测试
public class Test {
public static void main(String[] args) {
Factory factory = new PhoneFactory();
Factory factory1 = new IPadFactory();
Product Phone = factory.create();
Product IPad = factory1.create();
Phone.play();
IPad.play();
}
}
从上述代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码,这样就解决了简单工厂模式的缺点
工厂方法模式是简单工厂模式的进一步抽象,由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,解决了它的缺点。
优点:
- 用户只需知道具体工厂的名称就可以得到所要的产品,无须知道产品的具体创建过程
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则。
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,会增加程序的复杂度
三、抽象工厂模式
- 概念:
- 是一种为访问类提供一个创建一组相关或相互依赖对象的接口,并且访问类无须指定所要产品的具体类就能得到同一种类而不同品牌的产品
- 结构:
- 抽象工厂模式的主要角色如下:
- 抽象工厂:提供了创建产品的接口,它包含了多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂:主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品:定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品
- 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系
- 抽象工厂模式的主要角色如下:
- 步骤如下:
- 对生产的产品进行细化,然后抽象
/**
* 手机
*/
public interface Phone {
public void call();
}
/**
* 平板
*/
public interface IPad {
void study();
}
- 具体产品类(例如ApplePhone和AppleIPad,HuaweiPhone和HuaweiIPad)
/**
* AppleIPad
*/
public class AppleIPad implements IPad{
@Override
public void study() {
System.out.println("使用AppleIPad学习");
}
}
/**
* ApplePhone
*/
public class ApplePhone implements Phone{
@Override
public void call() {
System.out.println("使用ApplePhone打电话");
}
}
/**
* HuaweiIPad
*/
public class HuaweiIPad implements IPad {
@Override
public void study() {
System.out.println("HuaweiIPad学习");
}
}
/**
* HuaweiPhone
*/
public class HuaweiPhone implements Phone{
@Override
public void call() {
System.out.println("使用HuaweiPhone打电话");
}
}
- 根据不同的品牌给出工厂(需要先对工厂进行抽象)抽象工厂两个功能----造手机和造平板
/**
* 抽象工厂
*/
public interface Factory {
public Phone productPhone();
public IPad productIPad();
}
- 给出具体的Huawei工厂和Apple工厂
/**
* Huawei工厂
*/
public class HuaweiFactory implements Factory{
@Override
public Phone productPhone() {
System.out.println("生产 华为 Phone");
return new HuaweiPhone();
}
@Override
public IPad productIPad() {
System.out.println("生产 华为 IPad");
return new HuaweiIPad();
}
}
/**
* Apple工厂
*/
public class AppleFactory implements Factory{
@Override
public Phone productPhone() {
System.out.println("生产 苹果 IPhone");
return new ApplePhone();
}
@Override
public IPad productIPad() {
System.out.println("生产 苹果 IPad");
return new AppleIPad();
}
}
- 测试
public class Test {
public static void main(String[] args) {
Factory factory;
Phone phone;
IPad iPad;
factory = new HuaweiFactory();
phone = factory.productPhone();
iPad = factory.productIPad();
phone.call();
iPad.study();
factory = new AppleFactory();
phone = factory.productPhone();
iPad = factory.productIPad();
phone.call();
iPad.study();
}
}
如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。
优点:当一个产品族中多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:当一个品牌需要增加一个新的产品时,所有的工厂类都需要进行修改
建造者模式
- 概述:
- 将一个复杂对象构建与表示分离,使得同样的构建过程可以创建不同的表示
- 分离了部件的构造和装配,从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况
- 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,不需要知道内部的具体构造细节。
- 结构:
- 建造者模式包含如下角色:
1. 抽象建造者类:这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的对象部件的创建
2. 具体建造者类:实现Builder接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
3. 产品类:要创建的复杂对象
4. 指挥者类:调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或某种顺序创建
- 代码:
public class Client{
public static void main(String[]args){
// 盖普通房子
CommonHouse commonHouse = new CommonHouse();
// 准备创建房子的指挥者
HouseDirector houseDirector=new HouseDirector(commonHouse);
// 完成盖房子,返回产品(普通房子)
House house=houseDirector.constructHouse();
// 盖高楼
HighBuilding highBuilding=new HighBuilding();
// 重置建造者
houseDirector.setHouseBuilder(highBuilding);
//完成盖房子,返回产品(高楼)
houseDirector.constructHouse();
}
}
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(" 普通房子屋顶 ");
}
}
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(" 高楼的透明屋顶 ");
}
}
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;
}
}
// 抽象的建造者
abstract class HouseBuilder {
protected House house = new House();
// 将建造的流程写好,抽象的方法
public abstract void buildBasic();
public abstract void buildWalls();
public abstract void roofed();
// 建造房子好,将产品(房子)返回
public House buildHouse() {
return house;
}
}
class HouseDirector {
HouseBuilder houseBuilder = null;
//构造器传入 houseBuilder
public HouseDirector(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
//通过 setHouseBuilder 传入 houseBuilder
public void setHouseBuilder(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
//如何处理建造房子的流程,交给指挥者
public House constructHouse() {
houseBuilder.buildBasic();
houseBuilder.buildWalls();
houseBuilder.roofed();
return houseBuilder.buildHouse();
}
}
优点
- 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
- 在建造者模式中,客户端没不必知道产品内部组成的细节,将产品本身与产品的创建过程解藕,使得相同的创建过程可以创建不同的产品对象
- 扩展性好,符合开闭原则
缺点
- 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,建造者模式就不适用,这也导致其使用范围受限。
创建者模式对比
- 工厂方法模式和建造者模式的区别
- 工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,通过一步一步地精确构造创建出一个复杂的对象。
- 抽象工厂模式和建造者模式的区别
- 抽象工厂模式,超级工厂,可以生产不同品牌的各种产品,抽象出超级工厂,也要抽象出产品,然后根据不同的品牌给出该品牌商品的工厂实现类,简单来说就是每个产品使用一个具体工厂来生产。每个具体工厂使用多个工厂方法生成对应的多个产品。然后,抽象出一个抽象工厂类提供接口给客户,只用关心什么产品由什么工厂生产即可
- 建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品
二、结构型模式
用于描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合和聚合来组合对象。
结构型模式分为7种:
- 代理模式
- 适配器模式
- 桥接模式
- 装饰模式
- 外观模式
- 亨元模式
- 组合模式
常用的结构型模式
一、代理模式
- 概念
为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象
这样做的好处是,可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。核心含义在于一个业务操作除了真实实现之外,也需要有代理支持,代理负责所有与真实操作有关的辅助性功能实现,而真实主题只负责核心业务操作。
Java中代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种
- 结构:
- 代理模式分为三种角色:
- 抽象主题类:通过接口或抽象类声明真实主题和代理对象实现的业务方法
- 真实主题类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终引用的对象。
- 代理类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
- 代理模式分为三种角色:
静态代理
场景如下:
老板请公司员工吃饭,老板只负责给钱,而订餐和开发票结账这些操作由员工去做。
- 代码:
/**
* 代理类
*/
public class Staff implements IEat{
// 目标对象
private IEat boss;
// 通过构造方法传入
public Staff(IEat boss){
this.boss = boss;
}
@Override
public void eat() {
System.out.println("订餐");
boss.eat();
System.out.println("结账,开发票");
}
}
/**
* 目标类
*/
public class Boss implements IEat{
@Override
public void eat() {
System.out.println("请大家吃火锅");
}
}
/**
* 接口
*/
public interface IEat {
public void eat();
}
/**
* 测试
*/
public class Test {
public static void main(String[] args) {
IEat staff = new Staff(new Boss());
live.eat();
}
}
需注意:
- 需要代理类和目标类去实现统一接口,然后通过构造方法给代理对象注入目标对象,就能在代理对象中调用目标对象的方法,并且可以给其附加非功能性代码。
优点:
- 在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展
缺点:
- 因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类。一旦接口增加方法,目标对象与代理对象都要维护
动态代理(JDK代理)
Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法来获取代理对象。
代码:
/**
* 代理类
*/
public class Staff implements InvocationHandler {
private IEat target;
public Staff(IEat target){
this.target = target;
}
//获取代理对象
public Object getProxy(){
/*
Proxy类中的newProxyInstance()方法
第一个参数:目标对象的类加载器
第二个参数:目标对象实现的所有接口
第三个参数:代理类的对象
*/
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("订餐");
Object returnInfo = method.invoke(target,args); // 执行目标对象的方法
System.out.println("结账,开发票");
return returnInfo;
}
}
/**
* 目标类
*/
public class Boss implements IEat{
@Override
public void eat() {
System.out.println("请大家吃火锅");
}
}
/**
* 接口
*/
public interface IEat {
public void eat();
}
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
Staff staff = new Staff(new Boos());
IEat ie = (IEat) staff.getProxy();
ie.eat();
}
}
动态代理(Cglib代理)
同样是上面的案例,使用Cglib代理实现。
静态代理和JDK代理模式都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理,这就是Cglib代理
Cglib是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
Cglib是第三方提供的包,所有需要引入jar包的坐标:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
代码:
public class CglibProxy implements MethodInterceptor {
private IEat target;
public CglibProxy(IEat target){
this.target = target;
}
//给目标对象创建代理对象 (自定义)
public Object getProxyInstance(){
// 创建工具类(Enhancer)对象
Enhancer en = new Enhancer();
// 设置目标对象父类
en.setSuperclass(target.getClass());
// 设置回调函数
en.setCallback(this);
// 创建代理对象
return en.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("订餐");
Object returnInfo = null;
//
returnInfo = methodProxy.invokeSuper(o,objects);// 调用目标对象的方法并传入
System.out.println("结账,开发票");
return returnInfo;
}
}
public class Test {
public static void main(String[] args) {
IEat proxy = (IEat) new CglibProxy(new Boss()).getProxyInstance();
proxy.eat();
}
}
三种代理的对比
- jdk代理和Cglib代理
- 使用Cglib实现动态代理,Cglib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高,唯一需要注意的是,Cglib不能对声明为final的类或者方法进行代理,因为Cglib原理是动态生成被代理类的子类。
- 动态代理和静态代理
- 动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理。这样,在接口方法数量比较多的时候,可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现这种问题。
代理模式的优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
- 代理对象可以扩展目标对象的功能
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度。
缺点:
- 增加了系统的复杂度
二、适配器模式
- 概述:
把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法在一起工作的两个类能够一起工作
核心思想:使原本不兼容的两个接口可以兼容,相当于搭建了两个接口间的桥梁。
- 结构:
- 适配器模式包含以下主要角色:
- 目标接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者类:它是被访问和适配的现存组件库中的组件接口
- 适配器类:它是一个转化器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
- 适配器模式包含以下主要角色:
场景如下:现在有一台座机,可以通话,还有一个照相机,可以进行拍照,那么能不能有一台设备把两者的功能合一呢?
实现适配器的方案:分别是
- 继承
- 代码:
/**
* 座机
*/
public class Tel {
public void call(){
System.out.println("给你打call");
}
}
/**
* 照相机
*/
public class Camera {
public void make(){
System.out.println("拍照");
}
}
public class Phone extends Tel{
private Camera camera = new Camera();
public void make(){
camera.make();
}
}
- 依赖
- 代码
public interface ICall {
void call();
}
public interface IMake {
void make();
}
/**
* 座机
*/
public class Tel implements ICall {
public void call(){
System.out.println("给你打call");
}
}
/**
* 照相机
*/
public class Camera implements IMake{
public void make(){
System.out.println("拍照");
}
}
public class Phone2 implements ICall,IMake{
private Camera camera = new Camera();
private Tel tel = new Tel();
@Override
public void call() {
tel.call();
}
@Override
public void make() {
camera.make();
}
}
优点:
- 可以让没有任何关联的类,一起运行;
- 提高了类的复用
- 灵活性好
缺点:
- 过多的使用适配器,会导致系统非常混乱,不容易具体把控
- java是单继承
三、装饰器模式
-
概述:
对象功能的扩展能够根据需要来动态地实现 -
结构:
- 装饰模式中的角色:
- 抽象构件角色:定义一个抽象接口以规范准备接收附加责任的对象
- 具体构件角色:实现抽象构件,通过装饰角色为其添加一些职责
- 抽象装饰角色:继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能
- 具体装饰角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
-
案例
咖啡店里提供了各式各样的咖啡每样咖啡都有自己的描述属性和收费行为。另外它们还提供各种配料:奶、砂糖、冰块、不同的咖啡加入不同的配料计算的价格是不一样的。- 代码:
/**
* 根据统一信息抽象出一个材料接口
* 材料接口
*/
public interface Stuff {
// 获取材料名称
String getName();
// 获取材料价格
int getPrice();
}
// 然后通过所需的类去实现这个接口
/**
* 爱尔兰咖啡
*/
public class AStuff implements Stuff{
@Override
public String getName() {
return "爱尔兰咖啡";
}
@Override
public int getPrice() {
return 20;
}
}
/**
* 卡布奇诺
*/
public class KStuff implements Stuff{
@Override
public String getName() {
return "卡布奇诺";
}
@Override
public int getPrice() {
return 30;
}
}
/**
* 牛奶
*/
public class Milk implements Stuff{
// 将装饰对象加入到类中
private Stuff coffee;
public Milk(Stuff coffee){
this.coffee = coffee;
}
@Override
public String getName() {
return coffee.getName() + " 加奶";
}
@Override
public int getPrice() {
return coffee.getPrice() + 5;
}
/**
* 糖
*/
public class Sugar implements Stuff{
// 将装饰对象加入到类中
private Stuff coffee;
public Sugar(Stuff coffee){
this.coffee = coffee;
}
@Override
public String getName() {
return coffee.getName() + " 加糖";
}
@Override
public int getPrice() {
return coffee.getPrice() + 2;
}
/**
* 测试
*/
public class Test {
public static void main(String[] args) {
// 需要一个双倍奶加双倍糖的爱尔兰咖啡
Stuff aStuff= new AStuff();
aStuff = new Milk(aStuff); // 加第一份奶
aStuff = new Milk(aStuff); // 加第二份奶
aStuff = new Sugar(aStuff);// 加第一份糖
aStuff = new Sugar(aStuff);// 加第二份糖
System.out.println(aStuff.getName() +" 总价:"+ aStuff.getPrice());
// 需要一份双倍奶单份糖的卡布奇诺
Stuff kStuff = new KStuff();
kStuff = new Milk(kStuff);// 加第一份奶
kStuff = new Milk(kStuff); // 加第二份奶
kStuff = new Sugar(kStuff); // 加第一份糖
System.out.println(kStuff.getName() +" 总价:"+ kStuff.getPrice());
}
}
三、行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务。
行为型模型分为11种:
- 模版方法模式
- 策略模式
- 命令模式
- 职责链模式
- 状态模式
- 观察者模式
- 中介者模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 解释器模式
一、观察者模式
-
概述
定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。 -
结构
- 在观察者模式中有如下角色:
- 抽象主题(抽象被观察者):抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象
- 具体主题(具体被观察者):该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有观察者发送通知
- 抽象观察者:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
-
代码:
/**
* 商品 观察主题
* 此类如果需要让它成为主题类,则需要继承Observable类
*/
public class Product extends Observable {
private double price;
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
// 通知观察者注意到主题的变化
this.setChanged();// 设置变化点
this.notifyObservers(price); // 通知观察者
}
}
/**
* 代理商1号 观察者类
* 需要实现一个接口 Observe
*/
public class ProductProxy1 implements Observer {
private double price;
/**
* 当主题类的值发生变化后,会调用该方法
* @param o 主题对象
* @param arg 主题更新的值对象.
*/
@Override
public void update(Observable o, Object arg) {
double factoryPrice = (double)arg;
this.price = factoryPrice * 1.5;
}
public double getPrice() {
return price;
}
}
public class Test {
public static void main(String[] args) {
// 产生主题对象 -- 被观察者
Product product = new Product();
// 观察者
ProductProxy1 productProxy1 = new ProductProxy1();
// 给主题对象添加观察者
product.addObserver(productProxy1);
// 主题发生变化
product.setPrice(2000);
System.out.println("出厂价格:"+ product.getPrice() +
"\n代理商售卖价格:"+productProxy1.getPrice());
}
}