软考-软件设计师-面向对象
一、面向对象基础
1、面向对象的基本概念
4个核心概念:对象、类、继承、消息传递
对象=属性(状态/字段)+行为(操作/方法)
对象的特性:清晰的边界、良好定义的行为、可扩展性
多态
(1)通用多态
1)参数多态:泛型T
2)包含多态:接口实现分离implements
(2)特定多态
3)过载多态:重载
4)强制多态:隐式/显式转换
2、面向对象设计原则
(1)单一责任原则:一个类只做一种类型的责任,目的单一;
(2)开放—封闭原则:对扩展开放,对修改关闭(多扩展,少修改);
(3)里氏替换原则:子类可以替换父类;
(4)依赖倒置原则:抽象不依赖于细节,细节依赖于抽象,高层模块不依赖于底层模块,两者都应该依赖其抽象;针对接口编程,不要针对实现编程;
抽象:接口和抽象类(如抽象类shape)
细节:具体实现类(如子类Rectangle矩形,子类Triangle三角形)
高层模块:调用类(Person类)
底层模块:实现类(Car类)
正常情况下:调用类(高层模块)依赖具体实现类(低层模块实现细节)
倒置后:高层模块与低层模块都依赖了实现类的接口(低层模块的细节抽象),底层模块的依赖箭头向上了,所以叫依赖倒置了。原始是直接调用各个具体方法,依赖倒置后是调用所有具体方法的抽象出来的接口。
(5)接口隔离原则:使用多个专门的接口比使用单一的总接口要好,建立单一接口,不要建立庞大臃肿的总接口,尽量细化接口,接口中的方法尽量少;
以上是五大原则
(6)迪米特原则(最少知识法则):类之间的松耦合,一个对象应当对其他对象有尽可能少的了解,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成影响;
以上是六大原则
(7)重用发布等价原则
(8)共同封闭原则
(9)共同重用原则
(10)无环依赖原则
(11)稳定依赖原则
(12)稳定抽象原则
依赖倒置例子:
例一
//基类
public abstract class Shape {
public abstract void draw();
}
//子类矩形
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
//子类三角形
public class Triangle extends Shape {
@Override
public void draw() {
System.out.println("绘制三角形");
}
}
原始的菜鸟写法:
public class Paint{
public static void main(String[] args) {
drawRectangle (new Rectangle ());
drawTriangle (new Triangle());
}
private static void drawRectangle (Rectangle rectangle){
rectangle.draw();
}
private static void drawTriangle (Triangle triangle){
triangle.draw();
}
}
依赖倒置的设计后的老鸟写法:
public class Paint{
public static void main(String[] args) {
drawShape(new Triangle());
}
private static void drawShape(Shape shape){
shape.draw();
}
}
如果要将矩形换成六边形,那么菜鸟写法就要同时修改调用类Paint和增加一个绘制类,而老鸟写法就只需要增加一个六边形的绘制类。
所以我们要对接口编程,举几个具体的例子:声明方法参数的类型,实例变量的类型,方法的返回值类型,类型强制转换等等场景。
例二
高层的意思就是依赖方,例如一个人出远门需要依赖交通工具,底层就是被依赖方,如交通工具。交通工具包括大巴、火车、飞机、高铁等。
反例UML图
菜鸟原始写法:
class Car {
public void drive() {
System.out.println("汽车...");
}
}
class Train {
public void drive() {
System.out.println("火车...");
}
}
class Plane {
public void drive() {
System.out.println("飞机...");
}
}
public class Person {
public void ride(Car car) {
car.drive();
}
public void ride(Train car) {
car.drive();
}
public void ride(Plane car) {
car.drive();
}
public static void main(String[] args) {
Person person = new Person();
person.ride(new Car());
person.ride(new Train());
person.ride(new Plane());
}
}
可以看到每个种类的交通工具都需要去重载一个方法才能执行,要是有100种交通出行方式,则需要写100个重载,这是非常臃肿的,下面我们使用依赖倒置原则看看正例如何解决。
依赖倒置后的老鸟写法:
正例的UML图
/**
* 定义抽象
*/
interface Vehicle {
void drive();
}
class Car implements Vehicle {
@Override
public void drive() {
System.out.println("汽车...");
}
}
class Train implements Vehicle {
@Override
public void drive() {
System.out.println("火车...");
}
}
class Plane implements Vehicle {
@Override
public void drive() {
System.out.println("飞机...");
}
}
public class Person {
/**
* 去掉多余重载用统一接口接收参数
* @param vehicle
*/
public void ride(Vehicle vehicle) {
vehicle.drive();
}
public static void main(String[] args) {
Person person = new Person();
person.ride(new Car());
person.ride(new Train());
person.ride(new Plane());
}
}
可以看到原本上层依赖的箭头是向下依赖的,而加了统一抽象后,上层依赖了抽象,实现的箭头则“倒置”了;我们即使如何扩展交通工具也不需要改变客户端的调用,只需要实现统一的抽象即可,这样就实现了客户端的解耦。
二、UML(统一建模语言)
注:UML2.0提供了13种图,没有制品图
构造块:事务、关系、图
1、事物
UML中有4种事物,结构事物、行为事物、分组事物、注释事物
(1)结构事物
模型中的静态部分,名词,包括类、接口、协作、用例、主动类、构件、制品和节点;
(2)行为事物
模型中的动态部分,动词,描述了跨越时间和空间的行为,包括交互、状态机和活动;
交互由特定语境中共同完成一定任务的一组对象之间交换的消息组成。
状态机描述了一个对象或一个交互在生命期内响应事件所经历的状态序列。
活动是描述计算机过程执行的步骤序列,注重步骤之间的流,而不关心哪个对象执行哪个步骤。
(3)分组事物
模型的组织部分,最主要的事物是包(Package),包是把元素组织成组的机制。
(4)注释事物
模型的解释部分,注解(Node)是一种主要的注释事物,依附于一个元素或一组元素之上,对其进行约束或解释的简单符号。
2、关系
依赖:一个事物发生变化会影响另一个事物;
关联:一组链,链是对象之间的连接。聚集是一种特殊类型的关联。在关联上可以标注重复度和角色;
泛化:一般化和特殊化的关系,描述特殊元素(子元素)的对象可替换一般元素(父元素)的对象;
实现:一个类元指定了另一个类元保证执行的契约;
3、图
可以分为结构图和行为图或者分为静态图和动态图。
结构图(静态图)包括:
a类图:类之间关系的图;
①类、②接口、③协作、④依赖、泛化和关联关系;
b对象图:对象之间关系的图,一般包括对象和链;
c包图:包之间关系的图;
d组合结构图:用于描述一个分类器(如类、构件、用例)的内部结构,分类器与系统其他组成部分之间的交互端口;
e构件图:展示了构件之间的组织和依赖,在该类图中存在两种类似棒棒糖和插座的符号,棒棒糖符号表示“供接口”;插座式的符号表示“需接口”,该图是描述系统构件与构件之间、类与接口之间的关系图;
f部署图:软件的部件应该部署在哪个硬件的节点上面,运行时处理节点和其中生存的构件的配置;
g制品图:系统的物理结构,制品(artifact)包括文件、数据库、物理比特集合
行为图(动态图)包括:
a用例图:展现了一组用例、参与者以及他们的关系,该图的归类有分歧,大部分时候归结为动态,小部分时候归结为静态,建模时,说明系统应该做什么,而不考虑怎么做。
①用例、②参与者、③用例之间的扩展关系(extend)和包含关系(include),参与者和用例之间的关联关系,用例与用例以及参与者与参与者之间的泛化关系。
b序列图:强调了时间顺序的交互图。有对象生命线,垂直的虚线。有控制焦点,瘦高的矩形,表示一个对象执行一个动作所经历的时间段。发送和接受消息沿垂直方向按时间顺序从上到下放置,X标记结束;
c通信图:强调接受和发送消息(操作和属性结合起来,比如setName(“张三”))的对象的结构组织的交互图,有路径、顺序号,简而言之,对象和对象之间的调用关系;
d计时图:对象状态随着时间改变的情况;
e状态图:由状态、转换、事件和活动组成,强调对象行为的事件顺序;
f活动图:特殊的状态图,展现了系统内从一个活动到另一个活动的流程,一般包括活动状态和动作状态、转换和对象,可以表示分支、合并、分岔、汇合,进程或其他计算机结构展示为计算机内部一步步的控制流和数据流;
g交互概览图:强调控制流的交互图;
交互图:对系统的动态方面进行建模,由一组对象和他们之间的关系组成,包含他们之间可能传递的消息。序列图、通信图、交互概览图、计时图均被称为交互图,交互图一般包含对象、链和消息;
框中的名称中带有“:”,说明这是一个对象,“:”冒号前是对象名,“:”后是类名
注意:以前的教程这里有问题,半圆是需接口,全圆是供接口
三、设计模式
设计模式的分类
设计模式可以被分成三种:创建型模式、结构型模式、行为型模式
1、创建新模式
1.1抽象工厂Abstract Factory
抽象工厂(接口)
抽象产品(接口)
具体(实际)工厂(实现类)
具体(实际)产品(实现类)
注:实际上,客户端就引用了抽象产品,并未引用任何实际产品。这里在客户端还创建了实际工厂,如果把创建工厂的代码放到AbstractFactory中,就可以连实际工厂也屏蔽了(只是屏蔽,也就是客户端是和实际工厂和抽象产品打交道)。
适用场景:
客户端(应用层)不依赖于产品类实例如何被创建、实现等细节;
强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码;
提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现。
优点:
具体产品在应用层的代码隔离,无需关心创建的细节;
将一个系列的产品统一到一起创建。
缺点:
规定了所有可能被创建的产品集合,产品簇中扩展新的产品困难;
增加了系统的抽象性和理解难度。
例一
假设我们希望为用户提供一个Markdown文本转换为HTML和Word的服务,它的接口定义如下:
抽象工厂
public interface AbstractFactory {
// 创建Html文档
HtmlDocument createHtml(String md);
// 创建Word文档
WordDocument createWord(String md);
}
注意到抽象工厂仅仅是一个接口,没有任何实现代码,同样的,因为HtmlDocument和WordDocument都比较复杂,现在我们并不知道如何实现它们,所以只有接口。
抽象产品
// Html文档接口
public interface HtmlDocument {
String toHtml();
void save(Path path) throws IOException;
}
// Word文档接口
public interface WordDocument {
void save(Path path) throws IOException;
}
这样,我们就定义好了抽象工厂(AbstractFactory)以及两个抽象产品(HtmlDocument和WordDocument)。因为实现它们比较困难,我们决定让供应商来完成。
现在市场上有两家供应商:FastDoc Soft的产品便宜,并且转换速度快,而GoodDoc Soft的产品贵,但转换效果好。我们决定同时使用这两家供应商的产品,以便给免费用户和付费用户提供不同的服务。
我们先看看FastDoc Soft的产品是如何实现的。首先,FastDoc Soft必须要有实际产品(FastHtmlDocument、FastWordDocument)。
实际产品
public class FastHtmlDocument implements HtmlDocument {
public String toHtml() {...}
public void save(Path path) throws IOException {...}
}
public class FastWordDocument implements WordDocument {
public void save(Path path) throws IOException {...}
}
然后,FastDoc Soft必须提供一个实际工厂(FastFactory)来生产这两种产品。
实际工厂
public class FastFactory implements AbstractFactory {
public HtmlDocument createHtml(String md) {
return new FastHtmlDocument(md);
}
public WordDocument createWord(String md) {
return new FastWordDocument(md);
}
}
这样,我们就可以使用FastDoc Soft的服务了。客户端编写代码如下:
客户端Client(客户端用到了实际工厂和抽象产品)
// 创建AbstractFactory,实际类型是FastFactory:
AbstractFactory factory = new FastFactory();
// 生成Html文档:
HtmlDocument html = factory.createHtml("#Hello\nHello, world!");
html.save(Paths.get(".", "fast.html"));
// 生成Word文档:
WordDocument word = factory.createWord("#Hello\nHello, world!");
word.save(Paths.get(".", "fast.doc"));
如果我们要同时使用GoodDoc Soft的服务怎么办?因为用了抽象工厂模式,GoodDoc Soft只需要根据我们定义的抽象工厂和抽象产品接口,实现自己的实际工厂(GoodFactory)和实际产品(GoodHtmlDocument、GoodWordDocument)即可:
// 实际工厂:
public class GoodFactory implements AbstractFactory {
public HtmlDocument createHtml(String md) {
return new GoodHtmlDocument(md);
}
public WordDocument createWord(String md) {
return new GoodWordDocument(md);
}
}
// 实际产品:
public class GoodHtmlDocument implements HtmlDocument {
public String toHtml() {...}
public void save(Path path) throws IOException {...}
}
public class GoodWordDocument implements HtmlDocument {
public void save(Path path) throws IOException {...}
}
客户端要使用GoodDoc Soft的服务,只需要把原来的new FastFactory()切换为new GoodFactory()即可。
注意到客户端代码除了通过new创建了FastFactory或GoodFactory外,其余代码只引用了产品接口,并未引用任何实际产品(例如,FastHtmlDocument),如果把创建工厂的代码放到AbstractFactory中,就可以连实际工厂也屏蔽了:
public interface AbstractFactory {
public static AbstractFactory createFactory(String name) {
if (name.equalsIgnoreCase("fast")) {
return new FastFactory();
} else if (name.equalsIgnoreCase("good")) {
return new GoodFactory();
} else {
throw new IllegalArgumentException("Invalid factory name");
}
}
}
客户端调用:
public static void main(String[] args) throws IOException {
/*
* 创建工厂方法——放在AbstractFactory中,可以屏蔽实际工厂
* 只引用了产品接口,并未引用任何实际产品
* 如果不屏蔽实际工厂,实际上,引用的就是实际工厂和抽象产品
* 综合:客户端用到了实际工厂和抽象产品
*/
AbstractFactory factory3 = AbstractFactory.createFactory("fast");
// 生成Html文档
HtmlDocument html3 = factory3.createHtml("#Hello\nHello, world!");
html.save(Paths.get(".", "fast.html"));
// 生成Word文档
WordDocument word3 = factory3.createWord("#Hello\nHello, world!");
word.save(Paths.get(".", "fast.doc"));
}
例二
手机有小米手机、华为手机,它们都是手机,这些具体的手机和抽象手机就构成了一个产品等级结构。同样的,路由器有小米路由器,华为路由器,这些具体的路由器和抽象路由器就构成了另外一个产品等级结构,实质上产品等级结构即产品的继承结构。小米手机位于手机产品等级结构中,小米路由器位于路由器的产品等级结构中,而小米手机和小米路由器都是小米公司生产的,就构成了一个产品族,同理,华为手机和华为路由器也构成了一个产品族 。划重点就是产品族中的产品都是由同一个工厂生产的,位于不同的产品等级结构。
抽象工厂
/**
* 抽象产品工厂(定义了同一个产品族的产品生产行为)
*/
public interface AbstractFactory {
/**
* 生产手机
* @return
*/
IPhoneProduct produceTelPhone();
/**
* 生产路由器
* @return
*/
IRouterProduct produceRouter();
}
抽象产品
/**
* 抽象产品——手机
*/
public interface PhoneProduct {
/**
* 开机
*/
void start();
/**
* 关机
*/
void shutdown();
/**
* 拨打电话
*/
void callUp();
/**
* 发送短信
*/
void sendSMS();
}
/**
* 抽象产品——路由器
*/
public interface RouterProduct {
/**
* 开机
*/
void start();
/**
* 关机
*/
void shutdown();
/**
* 开启wifi
*/
void openWifi();
/**
* 设置参数
*/
void setting();
}
实际工厂
/**
* 实际工厂——华为工厂.
*/
public class HuaweiProductFactory implements AbstractFactory {
@Override
public PhoneProduct producePhone() {
System.out.println(">>>>>>生产华为手机");
return new HuaweiPhone();
}
@Override
public RouterProduct produceRouter() {
System.out.println(">>>>>>生产华为路由器");
return new HuaweiRouter();
}
}
/**
* 实际工厂——小米工厂.
*/
public class XiaomiProductFactory implements AbstractFactory {
@Override
public PhoneProduct producePhone() {
System.out.println(">>>>>>生产小米手机");
return new XiaomiPhone();
}
@Override
public RouterProduct produceRouter() {
System.out.println(">>>>>>生产小米路由器");
return new XiaomiRouter();
}
}
实际产品
/**
* 实际产品——华为手机.
*/
public class HuaweiPhone implements PhoneProduct{
@Override
public void start() {
System.out.println("开启华为手机");
}
@Override
public void shutdown() {
System.out.println("关闭华为手机");
}
@Override
public void callUp() {
System.out.println("用华为手机打电话");
}
@Override
public void sendSMS() {
System.out.println("用华为手机发短信");
}
}
/**
* 实际产品——华为路由器.
*/
public class HuaweiRouter implements RouterProduct {
@Override
public void start() {
System.out.println("启动华为路由器");
}
@Override
public void shutdown() {
System.out.println("关闭华为路由器");
}
@Override
public void openWifi() {
System.out.println("打开华为路由器的wifi功能");
}
@Override
public void setting() {
System.out.println("设置华为路由器参数");
}
}
/**
* 实际产品——小米手机.
*/
public class XiaomiPhone implements PhoneProduct{
@Override
public void start() {
System.out.println("开启小米手机");
}
@Override
public void shutdown() {
System.out.println("关闭小米手机");
}
@Override
public void callUp() {
System.out.println("用小米手机打电话");
}
@Override
public void sendSMS() {
System.out.println("用小米手机发短信");
}
}
/**
* 实际产品——小米路由器.
*/
public class XiaomiRouter implements RouterProduct {
@Override
public void start() {
System.out.println("启动小米路由器");
}
@Override
public void shutdown() {
System.out.println("关闭小米路由器");
}
@Override
public void openWifi() {
System.out.println("打开小米路由器的wifi功能");
}
@Override
public void setting() {
System.out.println("设置小米路由器参数");
}
}
客户端
/**
* 客户端.
*/
public class Client {
public static void main(String[] args) {
System.out.println("===================小米系列产品=================");
//小米产品工厂实例
AbstractFactory xiaomiProductFactory = new XiaomiProductFactory();
//生产小米路由器
RouterProduct xiaomiRouter = xiaomiProductFactory.produceRouter();
xiaomiRouter.start();
xiaomiRouter.setting();
xiaomiRouter.openWifi();
xiaomiRouter.shutdown();
//生产小米手机
PhoneProduct xiaomiPhone = xiaomiProductFactory.producePhone();
xiaomiPhone.start();
xiaomiPhone.callUp();
xiaomiPhone.sendSMS();
xiaomiPhone.shutdown();
System.out.println("===================华为系列产品=================");
//华为产品工厂实例
AbstractFactory huaweiProductFactory = new HuaweiProductFactory();
//生产华为路由器
RouterProduct huaweiRouter = huaweiProductFactory.produceRouter();
huaweiRouter.start();
huaweiRouter.setting();
huaweiRouter.openWifi();
huaweiRouter.shutdown();
//生产华为手机
PhoneProduct huaweiPhone = huaweiProductFactory.produceTelPhone();
huaweiPhone.start();
huaweiPhone.callUp();
huaweiPhone.sendSMS();
huaweiPhone.shutdown();
}
}
从上面的整个示例可以发现,如果现在想增加一个产品等级,如新加一种笔记本产品,就需要修改抽象工厂接口。这种直接修改抽象接口的做法,会导致其所有实现子类都需要进行修改,违反了开闭原则。当然,如果这种修改是长期稳定的,那么也可以接受。看起来上面的例子可能大合适,整个工厂的产品等级数量不是很稳定(小米和华为的后续延伸产品还有很多),但足以描述抽象工厂的应用。
如果该场景以工厂方法模式实现,那么我们需要定义一个手机工厂、一个路由器工厂、然后小米手机工厂实现类,华为手机工厂实现类,小米路由器工厂实现类,华为路由器工厂实现类,其余的产品接口和产品实现类均与上相同,这样就多出了一些产品工厂,略显啰嗦。
实际使用中,都需要根据业务去权衡使用工厂方法还是抽象工厂,前者关注点在产品等级上,后者关注点在产品族上,对于稳定的产品族,也即是产品等级数量稳定,使用抽象工厂会更加有效率,毕竟不再是一个工厂生产一种产品,而是一个工厂生产多种同族产品,对于不稳定的产品族,单独使用工厂方法会显得更加灵活。
抽象工厂模式是一个工厂可以生产多种产品,工厂方法模式是一个工厂生产一种产品。
使用典范:
1)java.sql.Connection
这就是典型的抽象工厂接口,描述了不同的产品等级Statement、PreparedStatement、CallableStatement,它们都位于抽象接口Statement产品等级结构中。我们可以继续寻找该抽象工厂接口的实现类。
这里就以Mysql为例,可以找到Mysql对这个工厂接口的实现类ConnectionImpl,ConnectionImpl并不是直接实现了java.sql.Connection,而是通过实现自己扩展的MySQLConnection接口,该接口也是间接继承了java.sql.Connection。
以createStatement为例,跟踪其调用代码可以看到StatementImpl这个类就是实现了java.sql.Statement的具体产品类。
抽象工厂:Connection
具体工厂:ConnectionImpl
抽象产品:Statement
具体产品:StatementImpl
2)org.apache.ibatis.session.SqlSessionFactory
SqlSessionFactory是抽象工厂接口,Configuration和SqlSession都是在不同的产品等级上。通过IDEA工具可以通过UML图清晰得看到SqlSessionFactory的工厂实现类。
抽象工厂:SqlSessionFactory
具体工厂:DefaultSqlSessionFactory
抽象产品:SqlSession、Configuration
具体产品:DefaultSqlSession、Configuration
以上两个例子都和数据库操作相关,同样使用了抽象工厂模式。在jdbc中,客户端通过Connection工厂获取到Statement产品对象,然后通过该对象进行增删改查操作,对于mybatis这种数据库操纵框架而言(底层也是封装了jdbc api)有异曲同工,通过SeqSessionFactory工厂获取到SqlSession产品对象,然后进行增删改查操作。
1.2生成器Builder
抽象建造者(抽象类)
具体建造者(类)
指导者(类)
产品(类)
注:Builder模式是为了创建一个复杂的对象,需要多个步骤完成创建,或者需要多个零件组装的场景,且创建过程中可以灵活调用不同的步骤或组件。
很多时候,我们可以简化Builder模式,以链式调用的方式来创建对象。
用Builder模式编写一个URLBuilder,调用方式如下:
String url = URLBuilder.builder() // 创建Builder
.setDomain("www.liaoxuefeng.com") // 设置domain
.setScheme("https") // 设置scheme
.setPath("/") // 设置路径
.setQuery(Map.of("a", "123", "q", "K&R")) // 设置query
.build(); // 完成build
针对不同的情况构建不同的builder。
适用场景:
如果一个对象有非常复杂的内部结构(很多属性)
想把复杂对象的创建和使用分离
优点:封装性好,创建和使用分离;扩展性好、建造类之间独立
缺点:
产生多余的Builder对象
产品内部发生改变,建造者都要修改,成本较大
示例:
生成器模式可以用于描述KFC如何创建套餐:套餐是一个复杂对象,它一般包含主食(如汉堡、鸡肉卷等)和饮料(如果汁、可乐等)等组成部分,不同的套餐有不同的组成部分,而KFC的服务员(指导者)可以根据顾客的要求,一步一步装配这些组成部分,构造一份完整的套餐,然后返回给顾客。所有的材料就是不同的零件,根据需求组装成不同的套餐。
产品
/**
* 具体产品.
*/
public class Meal {
private String food;
private String drink;
public String getFood() {
return food;
}
public void setFood(String food) {
this.food = food;
}
public String getDrink() {
return drink;
}
public void setDrink(String drink) {
this.drink = drink;
}
}
抽象建造者
/**
* 抽象建造者.
*/
public abstract class MealBuilder {
Meal meal = new Meal();
public abstract void buildFood();
public abstract void buildDrink();
public Meal getMeal(){
return meal;
}
}
具体建造者
/**
* 具体建造者——套餐A.
*/
public class MealBuilderA extends MealBuilder {
@Override
public void buildFood() {
meal.setDrink("可乐");
}
@Override
public void buildDrink() {
meal.setFood("薯条");
}
}
指导者
/**
* 指导者.
* 构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象,它主要有两个作用,
* 一是:隔离了客户与对象的生产过程,二是:负责控制产品的生产过程。
*/
public class KFCWaiter {
private MealBuilder mealBuilder;
public KFCWaiter(MealBuilder mealBuilder) {
this.mealBuilder = mealBuilder;
}
public Meal construct(){
//准备食物
mealBuilder.buildFood();
//准备饮料
mealBuilder.buildDrink();
//准备完毕,返回一个完整的套餐给客户
return mealBuilder.getMeal();
}
}
/**
* 具体建造者——套餐B.
*/
public class MealBuilderB extends MealBuilder {
@Override
public void buildFood() {
meal.setDrink("柠檬果汁");
}
@Override
public void buildDrink() {
meal.setFood("鸡翅");
}
}
上面说到,如果我们的产品只有一种,比如,KFC餐厅的只有薯条和可乐这样一种套餐,那么我们完全可以简化建造者模式,由于产品种类固定一种,那么就无需提供抽象建造者接口,直接提供一个具体的建造者就行,其次,对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉“指导者”,把指导者的功能和Client的功能合并起来,也就是说,Client这个时候就相当于指导者,它来指导建造者类去构建需要的复杂对象。
将产品和建造者合并在一起,客户端不依赖指导者,直接通过产品中的建造器创建产品实例。
简化版的建造者模式
package com.mrx.www.builder;
/**
* 具体产品(/将产品和建造者合并在一起).
*/
public class Meal {
private String food;
private String drink;
//私有化构造,不允许客户端直接new
private Meal(MealBuilder mealBuilder){
this.food = mealBuilder.food;
this.drink = mealBuilder.drink;
}
public static class MealBuilder{
private String food; //这里可以设置一些默认值
private String drink; //这里可以设置一些默认值
public MealBuilder buildFood(){
this.food = "薯条";
return this;
}
public MealBuilder buildDrink(){
this.drink = "可乐";
return this;
}
public Meal getMeal(){
return new Meal(this);
}
}
@Override
public String toString() {
return "Meal{" +
"food='" + food + '\'' +
", drink='" + drink + '\'' +
'}';
}
}
/**
* 简化版的建造者模式
*/
public class Client {
public static void main(String[] args) {
Meal meal = new Meal.MealBuilder().buildFood().buildDrink().getMeal();
System.out.println(meal);
}
}
1.3工厂方法Factory Method
意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类(new不同的具体工厂)。Factory Method使一个类的实例化延迟到其子类。
Product:抽象产品(类、接口);
ConcreteProduct:具体产品(实现类);
Creator:抽象工厂(类、接口),定义了供子类实现的生产产品的抽象方法,也即是将对象的实例化推迟到子类去做;
ConcreteCreator:具体工厂(实现类),真正生产产品的类。
适用场景:
创建对象需要大量重复的代码
客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
一个类通过其子类来指定创建哪个对象
优点:
用户只需要关心所需产品对应的工厂,无需关心创建的细节
加入新产品符合开闭原则,提高可扩展性
缺点:
类的个数容易过多,增加复杂度
增加了系统的抽象性和理解难度
特点:
①工厂方法的目的是使得创建对象和使用对象是分离的,并且客户端总是引用抽象工厂和抽象产品。
②工厂方法可以隐藏创建产品的细节,且不一定每次都会真正创建产品,完全可以返回缓存的产品,从而提升速度并减少内存消耗。
③总是引用接口而非实现类,能允许变换子类而不影响调用方,即尽可能面向抽象编程。
示例
实现一个解析字符串转Number的Factory
抽象工厂
public interface NumberFactory {
Number parse(String s);
}
具体工厂
public class NumberFactoryImpl implements NumberFactory {
public Number parse(String s) {
return new BigDecimal(s);
}
}
抽象产品是Number,NumberFactoryImpl返回的实际产品是BigDecimal
通常会在接口Factory中定义一个静态方法getFactory()来返回真正的子类:
public interface NumberFactory {
// 创建方法
Number parse(String s);
// 获取工厂实例
static NumberFactory getFactory() {
return impl;
}
static NumberFactory impl = new NumberFactoryImpl();
}
在客户端中,我们只需要和抽象工厂接口NumberFactory以及抽象产品Number打交道。
客户端
NumberFactory factory = NumberFactory.getFactory();
Number result = factory.parse("123.456");
调用方可以完全忽略真正的工厂NumberFactoryImpl和实际的产品BigDecimal,这样做的好处是允许创建产品的代码独立地变换,而不会影响到调用方。
实际上大多数情况下我们并不需要抽象工厂,而是通过静态方法直接返回产品,即:
静态工厂方法(Static Factory Method):抽象工厂和产品放一起
public class NumberFactory {
public static Number parse(String s) {
return new BigDecimal(s);
}
}
简化的使用静态方法创建产品的方式称为静态工厂方法(Static Factory Method)。静态工厂方法广泛地应用在Java标准库中。如:
Integer n = Integer.valueOf(100);
Integer既是产品又是静态工厂。它提供了静态方法valueOf()来创建Integer。那么这种方式和直接写new Integer(100)有何区别呢?我们观察valueOf()方法:
public final class Integer {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
...
}
它的好处在于,valueOf()内部可能会使用new创建一个新的Integer实例,但也可能直接返回一个缓存的Integer实例。对于调用方来说,没必要知道Integer创建的细节。——特点②
如果调用方直接使用Integer n = new Integer(100),那么就失去了使用缓存优化的可能性。
我们经常使用的另一个静态工厂方法是List.of():
List<String> list = List.of("A", "B", "C");
这个静态工厂方法接收可变参数,然后返回List接口。需要注意的是,调用方获取的产品总是List接口,而且并不关心它的实际类型。即使调用方知道List产品的实际类型是java.util.ImmutableCollections$ListN,也不要去强制转型为子类,因为静态工厂方法List.of()保证返回List,但也完全可以修改为返回java.util.ArrayList。这就是里氏替换原则:返回实现接口的任意子类都可以满足该方法的要求,且不影响调用方。——特点③
静态工厂方法案例:
自己写的很多工具类都是,如日期工具类
使用典范:
java.util.Collection
其中的ArrayList就是一个抽象工厂类的实现类,里面实现了 iterator()工厂方法,其中返回的Itr() 返回的就是一个具体的产品,Iterator接口就是抽象产品接口。
抽象工厂和工厂方法的区别:
工厂方法模式只有一个抽象产品类(可以派生出多个具体产品类),而抽象工厂模式有多个抽象产品类。 工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
可以这样理解,就是抽象工厂方法的抽象工厂可以生产多种抽象产品,而工厂方法的抽象工厂只生产一种抽象产品。
1.3.1拓展阅读(简单工厂、工厂方法、抽象工厂)
简单工厂:
简单工厂模式不是23种里的一种,简而言之,就是有一个专门生产某个产品的类。比如下图中的鼠标工厂,专业生产鼠标,给参数0,生产戴尔鼠标,给参数1,生产惠普鼠标。
IProduct:抽象产品(接口)
Product:具体产品(实现类)
Creator:工厂类,提供工厂方法创建产品实例
适用场景:
工厂类负责创建的对象比较少;
客户端(应用层)只知道传入工厂类的参数,对于如何创建对象(逻辑)并不关心。
优点:
只需要传入一个正确的参数,就可以获取你所需要的对象而不需要知道其创建的细节。
缺点:
工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,违背了开闭原则。
使用典范:java.util.Calendar
工厂模式
工厂模式也就是鼠标工厂是个父类,有生产鼠标这个接口。戴尔鼠标工厂,惠普鼠标工厂继承它,可以分别生产戴尔鼠标,惠普鼠标。生产哪种鼠标不再由参数决定,而是创建哪种工厂就生产哪种鼠标。后续直接调用鼠标工厂.生产鼠标()即可。
抽象工厂模式
抽象工厂模式也就是不仅生产鼠标,同时生产键盘。也就是PC厂商是个抽象父工厂,有生产鼠标,生产键盘两个接口。戴尔工厂,惠普工厂继承它,可以分别生产戴尔鼠标+戴尔键盘,和惠普鼠标+惠普键盘。创建工厂时,由具体工厂创建。 后续工厂.生产鼠标()则生产戴尔鼠标,工厂.生产键盘()则生产戴尔键盘。
在抽象工厂模式中,假设我们需要增加一个工厂:
假设我们增加华硕工厂,则我们需要增加华硕工厂,和戴尔工厂一样,继承PC厂商。之后创建华硕鼠标,继承鼠标类。创建华硕键盘,继承键盘类。
在抽象工厂模式中,假设我们需要增加一个产品:
假设我们增加耳麦这个产品,则首先我们需要增加耳麦这个父类,再加上戴尔耳麦,惠普耳麦这两个子类。
之后在PC厂商这个父类中,增加生产耳麦的接口。最后在戴尔工厂,惠普工厂这两个类中,分别实现生产戴尔耳麦,惠普耳麦的功能。
三种总结:
简单工厂:小作坊,啥都干,让生产啥牌子产品生产啥牌子产品,如杂牌小店,根据客户需求生产华为耳机,生产小米耳机,客户就在这个店里提要求;
工厂方法:中型作坊,只生产某牌子的一种商品,如华为手机工厂只生成手机,小米手机工厂只生产手机,客户要啥手机就跑谁的工厂去买;
抽象工厂:大型作坊,生产某牌子的一系列产品,如华为工厂生产旗下一系列,小米工厂生产旗下一些列,客户要批发啥就去谁家。
1.4原型Prototype
意图:原型复制自身从而创建新的对象,简单理解为对象的复制。
在原型模式中主要的任务是实现一个接口,这个接口具有一个clone方法可以实现拷贝对象的功能,也就是上图中的ProtoType接口。由于在Java语言中,JDK已经默认给我们提供了一个Cloneable接口,所以我们不需要手动去创建ProtoType接口类了。Cloneable接口在java中是一个标记接口,它并没有任何方法,只有实现了Cloneable接口的类在JVM当中才有可能被拷贝。
我们如何实现原型拷贝?Java的Object提供了一个clone()方法,重写来自Object类的clone方法就可以了。
public class Student implements Cloneable {
private int id;
private String name;
private int score;
// 复制新对象并返回
@Override
public Student clone() throws CloneNotSupportedException {
return (Student) super.clone();
}
}
客户端
public static void main(String[] args) {
Student protoType = new Student();
try {
//通过clone生成一个Student类型的新对象
Student cloneObject = protoType.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
值得注意的是,使用clone方法创建的新对象的构造函数是不会被执行的,也就是说会绕过任何构造函数(有参和无参),因为clone方法的原理是从堆内存中以二进制流的方式进行拷贝,直接分配一块新内存。
深拷贝和浅拷贝
浅拷贝
浅拷贝会创建一个新对象,如果属性是基本类型,拷贝的就是基本类型的值;如果属性值是引用类型,实际上拷贝是其引用,当引用指向的值改变时也会跟着变化,复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。通过序列化和反序列化的方式实现对象的拷贝的,通过实现Serializable接口,能够使用这种方式做的前提是,对象以及对象内部所有引用到的对象都是可序列化的,否则,就需要仔细考察那些不可序列化的对象可否设成transient,从而将之排除在复制过程之外。
1.5单例Singleton
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的实现方式:
1、一个private构造方法,确保外部无法实例化;
2、通过private static变量持有唯一实例,保证全局唯一性;
3、通过public static方法返回此唯一实例,使外部调用方能获取到实例。
2、结构型模式
2.1适配器Adapter
Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
2.2桥接Bridge
2.3组合Composite
2.4装饰Decorator
2.5外观Façade
2.6享元Flyweight
2.7代理Proxy
3、行为型模式
职责链模式的理解:比如用户下一个订单,订单对象要处理下单,库存对象要处理库存是否足够、减少等,账户对象也要处理账户是否有余额、减少等,如果都交给用户去做就是三个大操作,为了减少用户的操作,把多个对象处理请求链接起来,只需要下单,至于那个库存和账户的问题,系统在处理下单的时候一起处理。
命令模式:抽象出待执行的动作以参数化某对象;
状态模式:状态就是类的属性(字段)
模板方法模式:比如各种数据库的jdbc的操作
3.1职责链Chain of responsibility
责任链模式:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
比如:请假审批,所有上级组成一条链
3.2命令Command
命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
3.3解释器Interpreter
3.4迭代器Iterator
3.5中介者Mediator
中介者模式(Mediator Pattern):定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。
当类与类之间的关系呈现网状时,引入一个中介者,可以使类与类之间的网状结构变成星形结构。将每个类与多个类的耦合关系简化为每个类与中介者的耦合关系。
中介者模式就是用于将类与类之间的多对多关系 简化成多对一、一对多关系的设计模式。
中介者模式的缺点也很明显:由于它将所有的职责都移到了中介者类中,也就是说中介类需要处理所有类之间的协调工作,这可能会使中介者演变成一个超级类。所以使用中介者模式时需要权衡利弊。
3.6备忘录Memento
备忘录模式:在不破坏封装的条件下,通过备忘录对象存储另外一个对象内部状态的快照,在将来合适的时候把这个对象还原到存储起来的状态。
比如:
备忘录类(有要存档的属性),玩家类(读档、存档)
3.7观察者Observer
观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
比如:一个罪犯和多个警察这种一对多观察,开关灯这种一对一观察
3.8状态State
状态模式(State Pattern):当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
如果一个对象有多种状态,并且每种状态下的行为不同,一般的做法是在这个对象的各个行为中添加 if-else 或者 switch-case 语句。但更好的做法是为每种状态创建一个状态对象,使用状态对象替换掉这些条件判断语句,使得状态控制更加灵活,扩展性也更好。
比如:用户对象有普通用户和VIP用户两种状态,每个状态下的投票功能、积分功能都不同,原本的用户类用枚举和每个方法里面的if-else来判断,耦合太高,这种就可以用状态模式,把普通用户和VIP用户变成一个状态类,实现统一的功能接口,用户类只需要调用状态类的对应方法即可。
3.9策略Strategy
策略模式(Strategy Pattern):定义了一系列算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
3.10模板方法Template method
3.11访问者Visitor
访问者模式:超市的各个商品,服务台的结账方式,手动和自动,每个商品都是支持两种方式的,结账对应Element,Accept(商品),具体服务台A,就是手动,具体服务台B就是自动,Visitor就是商品,每个具体商品都支持两种具体的结账方式(即商品定义了一个accept操作,接受结账对象),如果有新的结账方式,并不改变这些商品,就在服务台添加一个新的作用于这些商品的操作,比如新的积分兑换。