软件设计与构造
一.CS/BSl两种结构模式
1.CS(Client/Server):客户端----服务器结构
C/S 结构的优点:
•1、响应速度快
C/S 结构是客户端与数据库直接相连,没有中间环节,因此响应速度快。
•2、软件设计个性化,可以满足用户个性化的需求
因为 C/S 结构有着自己的客户端,在客户端的设计上,可以根据客户的需求进行个性化的设计,可以满足客户复杂的个性化需求。特别是在操作界面、报表工具等方面。
•3、事务处理能力强大
C/S 结构充分利用客户端的硬件设施,将很多的数据处理工作在客户端完成,故数据处理能力比较强大,对一些复杂的业务流程,也容易实现。
C/S 结构的缺点:
•1、分布能力差
“成也萧何,败也萧何”,因为有客户端的安装带来的一系列个性化的方便的同时,也造成了在系统部署时,对每一个用户都要安装客户端才能进行使用,如用户数量庞大且分布广泛的话,给部署带来及大的不便。
• 2、维护成本高昂由于客户端数量庞大,对系统的维护和升级都是很大的考验。
• 3、跨平台使用难以实现
C/S 系统如需跨平台使用,必须重新开发在其系统平台下的客户端才能使用。
2.BS(Browser/Server):浏览器----服务器结构
优点
•1、分布性广,灵活方便
只要有浏览器并且能够上网就能够登录服务器进行信息的处理、采集工作。不受客户端的限制。
•2、维护简单方便
只要在服务器端进行配置就可以完成部署。如需升级,也只要在服务器端进行维护,客户端就自动登录最新的系统。
缺点
个性化特点明显降低,无法实现具有个性化的功能要求。
在跨浏览器上,BS架构不尽如人意。
在速度和安全性上需要花费巨大的设计成本。
功能弱化,难以实现传统模式下的特殊功能要求
二层C/S结构是单一服务器且以局域网为中心的。
三层C/S结构是分两类服务器(数据库服务器和应用服务器)和用户。
BS结构 | CS结构 | 三层CS结构 | |
---|---|---|---|
跨地域、跨网络 | 可以 | 不可以,只可以用于局域网,跨网络应用时需要额外的投入,且应用效果不理想 | 可以 |
操作性 | 不好,网页的操作方式对于大数量的操作效果不理想 | 好 | 好,在客户端方面与CS结构的客户端操作效果完全一致 |
服务器的压力 | 非常大,所有的计算都在服务器上,所以服务器压力非常大 | 小,客户端分担了部分计算压力 | 小,与CS结构完全一致 |
网络传输 | 速度慢,因为html的页面有大量的冗余信息,必然会导致网络流量大的问题 | 快,因为只需要传输数据 | 快,因为只需要传输数据 |
软件危机
1.软件危机是指在计算机软件的开发和维护过程中所遇到的一系列严重问题。
2.软件危机的典型表现:
(1)对软件开发成本和进度的估计常常很不准确;
(2)软件成本在计算机系统总成本中所占的比例逐年上升;
(3)软件开发生产率提高的速度,远远跟不上计算机应用迅速普及深入的趋势 。
(4)软件通常没有适当的文档资料;
(5)软件常常是不可维护的;
(6)软件产品的质量往往靠不住;
(7)用户对“已完成的”软件系统不满意的现象经常发生;
3.产生软件危机的原因:
(1)用户对软件需求的描述不精确。
(2)软件开发人员对用户需求的理解有偏差。
(3)缺乏处理大型软件项目的经验。
(4)开发大型软件易产生疏漏和错误。
(5)缺乏有力的方法学的指导和有效的开发工具的支持。
(6)面对日益增长的软件需求,人们显得力不从心。
jdbc
/**
-
JDBC快速入门
*/ public class JDBCDemo { public static void main(String[] args) throws Exception { //1. 注册驱动 //Class.forName("com.mysql.jdbc.Driver"); //2. 获取连接 String url = "jdbc:mysql://127.0.0.1:3306/db1"; String username = "root"; String password = "1234"; Connection conn = DriverManager.getConnection(url, username, password); //3. 定义sql String sql = "update account set money = 2000 where id = 1"; //4. 获取执行sql的对象 Statement Statement stmt = conn.createStatement(); //5. 执行sql int count = stmt.executeUpdate(sql);//受影响的行数 //6. 处理结果 System.out.println(count); //7. 释放资源 stmt.close(); conn.close(); } }
# # ResultSet
```java
/**
* 查询account账户表数据,封装为Account对象中,并且存储到ArrayList集合中
* 1. 定义实体类Account
* 2. 查询数据,封装到Account对象中
* 3. 将Account对象存入ArrayList集合中
*/
@Test
public void testResultSet2() throws Exception {
//1. 注册驱动
//Class.forName("com.mysql.jdbc.Driver");
//2. 获取连接:如果连接的是本机mysql并且端口是默认的 3306 可以简化书写
String url = "jdbc:mysql:///db1?useSSL=false";//jdbc:mysql://127.0.0.1:3306/db1
String username = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "select * from account";
//4. 获取statement对象
Statement stmt = conn.createStatement();
//5. 执行sql
ResultSet rs = stmt.executeQuery(sql);
// 创建集合
List<Account> list = new ArrayList<>();
// 6.1 光标向下移动一行,并且判断当前行是否有数据
while (rs.next()){
Account account = new Account();
//6.2 获取数据 getXxx()
int id = rs.getInt("id");
String name = rs.getString("name");
double money = rs.getDouble("money");
//赋值
account.setId(id);
account.setName(name);
account.setMoney(money);
// 存入集合
list.add(account);
}
System.out.println(list);
//7. 释放资源
rs.close();
stmt.close();
conn.close();
}
MVC模式和三层架构
MVC 模式和三层架构是一些理论的知识,将来我们使用了它们进行代码开发会让我们代码维护性和扩展性更好。
7.1 MVC模式
MVC 是一种分层开发的模式,其中:
-
M:Model,业务模型,处理业务
-
V:View,视图,界面展示
-
C:Controller,控制器,处理请求,调用模型和视图
![image-20210818163348642](../../../../桌面/我的资源/CorC++and%20makedown/Markdown笔记/javaweb笔记/assets/image-20210818163348642.png)
控制器(serlvlet)用来接收浏览器发送过来的请求,控制器调用模型(JavaBean)来获取数据,比如从数据库查询数据;控制器获取到数据后再交由视图(JSP)进行数据展示。
MVC 好处:
-
职责单一,互不影响。每个角色做它自己的事,各司其职。
-
有利于分工协作。
-
有利于组件重用
7.2 三层架构
三层架构是将我们的项目分成了三个层面,分别是 表现层
、业务逻辑层
、数据访问层
。
![image-20210818164301154](../../../../桌面/我的资源/CorC++and%20makedown/Markdown笔记/javaweb笔记/assets/image-20210818164301154.png)
- 数据访问层:对数据库的CRUD基本操作
- 业务逻辑层:对业务逻辑进行封装,组合数据访问层层中基本功能,形成复杂的业务逻辑功能。例如
注册业务功能
,我们会先调用数据访问层
的selectByName()
方法判断该用户名是否存在,如果不存在再调用数据访问层
的insert()
方法进行数据的添加操作 - 表现层:接收请求,封装数据,调用业务逻辑层,响应数据
而整个流程是,浏览器发送请求,表现层的Servlet接收请求并调用业务逻辑层的方法进行业务逻辑处理,而业务逻辑层方法调用数据访问层方法进行数据的操作,依次返回到serlvet,然后servlet将数据交由 JSP 进行展示。
三层架构的每一层都有特有的包名称:
- 表现层:
com.itheima.controller
或者com.itheima.web
- 业务逻辑层:
com.itheima.service
- 数据访问层:
com.itheima.dao
或者com.itheima.mapper
后期我们还会学习一些框架,不同的框架是对不同层进行封装的
![image-20210818165439826](../../../../桌面/我的资源/CorC++and%20makedown/Markdown笔记/javaweb笔记/assets/image-20210818165439826.png)
7.3 MVC 和 三层架构
通过 MVC 和 三层架构 的学习,有些人肯定混淆了。那他们有什么区别和联系?
![image-20210818165808589](../../../../桌面/我的资源/CorC++and%20makedown/Markdown笔记/javaweb笔记/assets/image-20210818165808589.png)
如上图上半部分是 MVC 模式,上图下半部分是三层架构。 MVC 模式
中的 C(控制器)和 V(视图)就是 三层架构
中的表现层,而 MVC 模式
中的 M(模型)就是 三层架构
中的 业务逻辑层 和 数据访问层。
可以将 MVC 模式
理解成是一个大的概念,而 三层架构
是对 MVC 模式
实现架构的思想。 那么我们以后按照要求将不同层的代码写在不同的包下,每一层里功能职责做到单一,将来如果将表现层的技术换掉,而业务逻辑层和数据访问层的代码不需要发生变化。
前后端交互
前端
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录表单</title>
</head>
<body>
<h2>登录表单</h2>
<form action="/adminLoginServlet" method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required><br>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required><br>
<label for="remember">记住我:</label>
<input type="checkbox" id="remember" name="remember"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
后端
@WebServlet("/adminLoginServlet")
public class AdminLoginServlet extends HttpServlet {
private AdminService service=new AdminServiceImpl();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1.获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
// 获取复选框数据
String remember = request.getParameter("remember");
// 2.调用service查询
Admin user = service.login(username, password);
// 3.判断是否存在数据库当中
if (user!=null){
// 登录成功,跳转到查询所有的BrandServlet里面去
String contextPath = request.getContextPath();
response.sendRedirect("https://www.baidu.com/");
}else{
// 登录失败
// 存储错误信息到request
request.setAttribute("login_msg","用户名或密码错误");
// 跳转到login.jsp
request.getRequestDispatcher("/login.html").forward(request,response);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
单例设计模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
4.1.1 单例模式的结构
单例模式的主要有以下角色:
- 单例类。只能创建一个实例的类
- 访问类。使用单例类
4.1.2 单例模式的实现
单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
-
饿汉式-方式1(静态变量方式)
/** * 饿汉式 * 静态变量创建类的对象 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance = new Singleton(); //对外提供静态方法获取该对象 public static Singleton getInstance() { return instance; } }
说明:
该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
-
饿汉式-方式2(静态代码块方式)
/** * 恶汉式 * 在静态代码块中创建该类对象 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance; static { instance = new Singleton(); } //对外提供静态方法获取该对象 public static Singleton getInstance() { return instance; } }
说明:
该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以和饿汉式的方式1基本上一样,当然该方式也存在内存浪费问题。
-
懒汉式-方式1(线程不安全)
/** * 懒汉式 * 线程不安全 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance; //对外提供静态方法获取该对象 public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
说明:
从上面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
-
懒汉式-方式2(线程安全)
/** * 懒汉式 * 线程安全 */ public class Singleton { //私有构造方法 private Singleton() {} //在成员位置创建该类的对象 private static Singleton instance; //对外提供静态方法获取该对象 public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
说明:
该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
-
懒汉式-方式3(双重检查锁)
再来讨论一下懒汉模式中加锁的问题,对于
getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式/** * 双重检查方式 */ public class Singleton { //私有构造方法 private Singleton() {} private static Singleton instance; //对外提供静态方法获取该对象 public static Singleton getInstance() { //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 if(instance == null) { synchronized (Singleton.class) { //抢到锁之后再次判断是否为null if(instance == null) { instance = new Singleton(); } } } return instance; } }
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用
volatile
关键字,volatile
关键字可以保证可见性和有序性。/** * 双重检查方式 */ public class Singleton { //私有构造方法 private Singleton() {} private static volatile Singleton instance; //对外提供静态方法获取该对象 public static Singleton getInstance() { //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际 if(instance == null) { synchronized (Singleton.class) { //抢到锁之后再次判断是否为空 if(instance == null) { instance = new Singleton(); } } } return instance; } }
小结:
添加
volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。 -
懒汉式-方式4(静态内部类方式)
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被
static
修饰,保证只被实例化一次,并且严格保证实例化顺序。/** * 静态内部类方式 */ public class Singleton { //私有构造方法 private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } //对外提供静态方法获取该对象 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
说明:
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder
并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
小结:
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
-
枚举方式
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
/** * 枚举方式 */ public enum Singleton { INSTANCE; }
说明:
枚举方式属于恶汉式方式。
工厂模式
4.2.1 概述
需求:设计一个咖啡店点餐系统。
设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。
具体类的设计如下:
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day02/笔记/img/工厂设计模式引入.png)
在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦。
在本教程中会介绍三种工厂的使用
- 简单工厂模式(不属于GOF的23种经典设计模式)
- 工厂方法模式
- 抽象工厂模式
4.2.2 简单工厂模式
简单工厂不是一种设计模式,反而比较像是一种编程习惯。
4.2.2.1 结构
简单工厂包含如下角色:
- 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品 :实现或者继承抽象产品的子类
- 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。
4.2.2.2 实现
现在使用简单工厂对上面案例进行改进,类图如下:
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day02/笔记/img/简单工厂模式.png)
工厂类代码如下:
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffee;
}
}
工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactory,CoffeeStore类中的orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类的耦合,同时又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。
后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。
4.2.2.4 优缺点
优点:
封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。
缺点:
增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。
4.2.2.3 扩展
静态工厂
在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的。代码如下:
public class SimpleCoffeeFactory {
public static Coffee createCoffee(String type) {
Coffee coffee = null;
if("americano".equals(type)) {
coffee = new AmericanoCoffee();
} else if("latte".equals(type)) {
coffee = new LatteCoffee();
}
return coffe;
}
}
4.2.3 工厂方法模式
针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。
4.2.3.1 概念
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
4.2.3.2 结构
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
4.2.3.3 实现
使用工厂方法模式对上例进行改进,类图如下:
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day02/笔记/img/工厂方法模式.png)
代码如下:
抽象工厂:
public interface CoffeeFactory {
Coffee createCoffee();
}
具体工厂:
public class LatteCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
}
public class AmericanCoffeeFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
咖啡店类:
public class CoffeeStore {
private CoffeeFactory factory;
public CoffeeStore(CoffeeFactory factory) {
this.factory = factory;
}
public Coffee orderCoffee(String type) {
Coffee coffee = factory.createCoffee();
coffee.addMilk();
coffee.addsugar();
return coffee;
}
}
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
4.2.3.4 优缺点
优点:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
4.2.4 抽象工厂模式
前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机、传智播客只培养计算机软件专业的学生等。
这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。
本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day02/笔记/img/image-20200401214509176.png)
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day02/笔记/img/image-20200401222951963.png)
4.2.4.1 概念
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
4.2.4.2 结构
抽象工厂模式的主要角色如下:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
4.2.4.2 实现
现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。类图如下:
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day02/笔记/img/抽象工厂模式.png)
代码如下:
适配器模式
5.2.1 概述
如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式。
定义:
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
5.2.2 结构
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
5.2.3 类适配器模式
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
【例】读卡器
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。
类图如下:
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day03/笔记/img/适配器模式.png)
代码如下:
//SD卡的接口
public interface SDCard {
//读取SD卡方法
String readSD();
//写入SD卡功能
void writeSD(String msg);
}
//SD卡实现类
public class SDCardImpl implements SDCard {
public String readSD() {
String msg = "sd card read a msg :hello word SD";
return msg;
}
public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}
//电脑类
public class Computer {
public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}
}
//TF卡接口
public interface TFCard {
//读取TF卡方法
String readTF();
//写入TF卡功能
void writeTF(String msg);
}
//TF卡实现类
public class TFCardImpl implements TFCard {
public String readTF() {
String msg ="tf card read msg : hello word tf card";
return msg;
}
public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}
//定义适配器类(SD兼容TF)
public class SDAdapterTF extends TFCardImpl implements SDCard {
public String readSD() {
System.out.println("adapter read tf card ");
return readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
writeTF(msg);
}
}
//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
SDAdapterTF adapter = new SDAdapterTF();
System.out.println(computer.readSD(adapter));
}
}
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。
5.2.4 对象适配器模式
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
【例】读卡器
我们使用对象适配器模式将读卡器的案例进行改写。类图如下:
![](../../../../BaiduNetdiskDownload/设计模式/Java设计模式资料day03/笔记/img/对象适配器模式.png)
代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。
//创建适配器对象(SD兼容TF)
public class SDAdapterTF implements SDCard {
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
this.tfCard = tfCard;
}
public String readSD() {
System.out.println("adapter read tf card ");
return tfCard.readTF();
}
public void writeSD(String msg) {
System.out.println("adapter write tf card");
tfCard.writeTF(msg);
}
}
//测试类
public class Client {
public static void main(String[] args) {
Computer computer = new Computer();
SDCard sdCard = new SDCardImpl();
System.out.println(computer.readSD(sdCard));
System.out.println("------------");
TFCard tfCard = new TFCardImpl();
SDAdapterTF adapter = new SDAdapterTF(tfCard);
System.out.println(computer.readSD(adapter));
}
}
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
5.2.5 应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
5.2.6 JDK源码解析
Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。
InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
如上代码中的sd(StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:
从上图可以看出:
- InputStreamReader是对同样实现了Reader的StreamDecoder的封装。
- StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。
结论:
从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。
dSD(adapter));
}
}
> 注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
### 5.2.5 应用场景
* 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
* 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
### 5.2.6 JDK源码解析
Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。
InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:
```java
public int read() throws IOException {
return sd.read();
}
public int read(char cbuf[], int offset, int length) throws IOException {
return sd.read(cbuf, offset, length);
}
如上代码中的sd(StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:
[外链图片转存中…(img-J4zvZczo-1705304397739)]
从上图可以看出:
- InputStreamReader是对同样实现了Reader的StreamDecoder的封装。
- StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。
结论:
从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。