本文根据黑马课程和网上相关示例总结而成。
适配器模式
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类协作起来
// 要被适配的类
public class Adaptee{
@Override
public void request(){
System.out.println("连接网线上网");
}
}
// 客户端类
public class Computer{
// 电脑需要连接上转接器才可以上网
public void net(NetToUsb adapter){
// 上网的具体实现:找一个转接头
adapter.handleRequest();
}
public static void main(String[] args){
// 电脑,适配器,网线
Computer computer = new Computer();
Adaptee adaptee = new Adaptee();
Adapter adapter = new Adapter();
computer.net(adapter);
}
}
// 接口转换器的抽象实现
public class NetToUsb{
// 作用:处理请求:网线插在USB上
public void handleRequest();
}
// 真正的适配器(可以理解为将USB和网线连接通过适配器连接起来了):需要连接USB,连接网线(处理上网的请求,继承Adaptee)
public class Adapter extends Adaptee implements NetToUsb{
@Override
public void handleRequest(){
super.request(); // 表示可以上网了
}
}
// 1.继承(类适配器,单继承的方式)(以上是类适配器)
// 2.组合(对象适配器,常用的方式)(下面的模式)
public class Adapter2 implements NetToUsb{
private Adaptee adaptee;
public Adapter2(Adaptee adaptee){
this.adaptee = adaptee;
}
@Override
public void handleRequest(){
adaptee.request();
}
}
// 更改Computer类:main函数修改为
public static void main(String[] args){
Computer computer = new Computer();
Adaptee adaptee = new Adaptee();
Adapter adapter = new Adapter(adaptee); // 转换器先插网线
computer.net(adapter); // 其次连接电脑
}
上述的例子是这样的:电脑和一个设备因为接口不同所以无法连接,但是可以通过一个转接器(适配器)将不能连接的两个设备实现连接。
享元模式
运用共享技术来有效支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。
结构
享元(Flyweight)模板存在两种状态
- 内部状态,不会随着环境改变而改变的可共享部分
- 外部状态:指随环境改变而改变的不可共享部分
角色为:
- 抽象享元角色:在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部状态,同时也可以通过这些方法来设置外部状态
- 具体享元:实现了抽象享元,在具体享元类中为内部状态提供了存储空间。通常可以结合单例模式来设计具体享元类,为每一个具体的享元类提供唯一的享元对象
- 非享元:并不是所有的抽象享元子类都需要被共享,不能被共享的子类可设计为非共享具体享元类,当需要一个非共享具体享元类的对象时可以直接通过实例化创建
- 享元工厂:负责创建和管理享元角色,当客户对象请求一个享元对象时,享元工厂会检查系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在,则创建一个新的享元对象
案例
俄罗斯方块,如果在俄罗斯方块游戏中,每个不同的方块是一个实例对象,这些对象就要占用很多的内存空间,下面可以利用享元模式进行实现。
// 抽象享元角色:AbstractBox method: getShape() display(String color())
// 具体实现子类:IBox LBox OBox
// 工厂:map:HashMap<String, AbstractBox>
public abstract class AbstractBox {
// 获取图形的方法
public abstract String getShape();
// 显示图形及颜色
public void display(String color){
System.out.println("方块形状:" + getShape() + ",颜色为:" + color);
}
}
public class IBox extends AbstractBox {
@Override
public String getShape() {
return "I";
}
}
public class LBox extends AbstractBox {
@Override
public String getShape() {
return "L";
}
}
public class OBox extends AbstractBox {
@Override
public String getShape() {
return "O";
}
}
// 享元工厂
public class BoxFactory {
private HashMap<String, AbstractBox> map;
// 单例模式:构造方法私有
private BoxFactory(){
map = new HashMap<String, AbstractBox>();
map.put("I", new IBox());
map.put("L", new LBox());
map.put("O", new OBox());
}
// 一开始就进行了初始化,这是饿汉式单例模式
private static BoxFactory factory = new BoxFactory();
// 因为设计成单例模式,所以提供一个静态方法获取该工厂类对象
public static BoxFactory getInstance(){
return factory;
}
// 根据名称获取图形对象
public AbstractBox getShape(String name){
return map.get(name);
}
}
// 客户测试类
public class Client {
public static void main(String[] args) {
// 获取I图形对象
AbstractBox box1 = BoxFactory.getInstance().getShape("I");
box1.display("灰色");
// 获取L图形对象
AbstractBox box2 = BoxFactory.getInstance().getShape("L");
box2.display("绿色");
// 获取O图形对象
AbstractBox box3 = BoxFactory.getInstance().getShape("O");
box3.display("红色");
// 获取O图形对象
AbstractBox box4 = BoxFactory.getInstance().getShape("O");
box4.display("灰色");
// 输出结果为true,是同一个对象,享元模式对图形进行共享操作
System.out.println(box3 == box4);
}
}
优点:
- 极大减少内存中相似或相同对象数量,节约系统资源
- 享元模式中的外部状态相对独立,且不影响内部状态。比如说上面的颜色是外部状态,颜色的修改不影响图形,只要图形一样,不管什么颜色,均是同一对象
缺点:
- 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
适用场景:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源。因此,应当在需要多次重复使用享元对象时才值得使用享元模式
- Integer类使用了享元模式
模板方法模式
在面向对象编程时,可能会遇到这种情况:设计一个系统时知道了算法所需的关键步骤, 而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。
例如:去银行办理业务时,需要经过4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等。其中取号、排队和评分对每个客户都是一样的,可以在父类实现,但是办理具体业务是因人而异的,它可能是存款、取款或者转账等,可以延迟到子类中实现。
**定义:**定义一个操作中的算法框架,将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
结构
- 抽象类:负责给出一个算法的轮廓和骨架,由一个模板方法和若干个基本方法构成
- 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法
- 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分,基本方法分为三种
- 抽象方法:由抽象类声明,由其具体子类实现
- 具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承,比如银行示例中的取号、排队和对银行人员进行评分
- 钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般用于判断的逻辑方法,返回值类型为boolean类型
- 具体子类:实现抽象类中所定义的抽象方法和钩子方法
示例
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法进行模拟。
// 抽象方法为:倒蔬菜、倒调料品
// 具体实现:倒油、热油、翻炒
public abstract class AbstractClass{
// 模板方法定义:这个是按照一定步骤进行的
// 需要定义final,不让子类重写
public final void cookProcess(){
pourOil();
heatOil();
pourVegetable();
pourSauce();
fry();
}
// 基本方法定义:具体方法:第一步
public void pourOil(){
System.out.println("倒油");
}
// 具体方法:第二步
public void heatOil(){
System.out.println("热油");
}
// 抽象方法:第三步
public abstract void pourVegetable();
// 抽象方法:第四步
public abstract void pourSauce();
// 具体方法:第五步
public void fry(){
System.out.println("翻炒");
}
}
// 具体实现子类
public class ConcreteClass_BaiCai extends AbstractClass{
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是白菜");
}
@Override
public void pourSauce() {
System.out.println("下锅的料是醋");
}
}
// 客户测试类
public class Client {
public static void main(String[] args) {
// 炒白菜
// 创建对象
ConcreteClass_BaiCai baiCai = new ConcreteClass_BaiCai();
// 调用炒菜的功能
baiCai.cookProcess();
}
}
优点:
- 提高代码复用性,将相同部分的代码放在抽象的父类中,而将不同的放入不同的子类
- 实现了反向控制。通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制,并符合开闭原则
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这会导致一种反向的控制结构,提高了代码阅读的难度
适用场景:
- 算法的整体步骤很固定,只有个别部分易变,这是可以适用模板方法模式,将容易变化的部分抽象出来,用子类实现
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制
- InputStream类适用到了模板方法模式
观察者模式
定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使它们能够自动更新自己。
观察者模式的角色
- 抽象主题(抽象被观察者):抽象主题角色把所有观察者对象保存在一个集合中,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象
- 具体主题(具体被观察者):该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer(抽象观察者):观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- 具体观察者,实现抽象观察者定义的更新接口,一遍在得到主题更改时更新自身的状态。
示例1:使用微信公众号时,当关注的公众号中有新内容更新的话,就会推送给关注公众号的微信用户端。微信用户是观察者,微信公众号是被观察者,有多个微信用户关注了这个公众号
// 创建抽象主题:微信公众号
public interface Subject{
// 增加观察者对象
void attach(Observer observer);
// 删除观察者对象
void detach(Observer observer);
// 通知观察者更新信息
void notify(String message);
}
// 创建抽象观察者
public interface Observer{
// 更新信息
void update(String message);
}
// 创建具体抽象主题
public class SubscriptionSubject implements Subject{
// 观察者对象集合
private List<Observer> observerList = new ArrayList<Observer>();
public void attach(Observer observer){
observerList.add(observer);
}
public void detach(Observer observer){
observerList.remove(observer);
}
public void notify(String message){
observer.update(message);
}
}
// 创建具体观察者对象
public class weixinUser implements Observer{
// 需要知道观察者的名字
private String name;
public void update(String message){
System.out.println(name + "-" + message);
}
}
// 创建一个客户类,实现上述模式
public class Client{
public static void main(String[] args){
// 创建被观察者
SubscriptionSubject subject = new SubscriptionSubject();
// 创建观察者对象
subject.attach(new weixinUser("lisi"));
subject.attach(new weixinUser("wangwu"));
subject.attach(new weixinUser("zhaoliu"));
// 订阅更新后,需要通知所有的观察者对象,需要说明更新信息
subject.update("专栏更新了");
}
}
示例2:对用户显示数学饼状图需要数据支撑,数学饼状图在开发中,其实分为两个部分,一个是视图部分(以饼状图呈现出的样子),一个是数据部分,即各国的金牌数量,由于将数据和视图抽离,因为一旦数据部分更新,视图部分得不到最新的数据,难以维持一致性。此时,需要一个时刻关注数据变化的观察者,一旦观察者感知到数据变化则立即更新视图,视图类本身也可以作为一个观察者,但是更好的方法是单独分离出一个观察者类来维护两个类之间的一致性。订阅者是谷歌数据源和百度数据源(该示例来自网上一篇博客分析,现已不知道对应链接)
// 第一种方法:定义接口,实现接口的方式
// 视图类
Class View{
public void show(Object data){
System.out.println(data);
}
}
// 抽象数据源(被观察对象)
public interface DataSource{
// 添加观察者对象
void attach(Observer observer);
// 更新数据,如果相同,则不更新,不同,通知观察者更新数据
void updateDate(String newData);
// 通知观察者
void notifyObserver(String data);
}
// 抽象观察者类
public interface Observer{
void update(DataSource ds, String data);
}
// 具体被观察者对象,这里只展示GooleDataSource,BaiduDataSource代码类似
public class GooleDataSource implements DataSource{
// 观察者对象列表
List<Observer> observerList = new ArrayList<Observer>();
// 值对象列表
HashSet<String> data = new HashSet<>();
@Override
public void attach(Observer observer) {
observerList.add(observer);
}
@Override
public void updateDate(String newData) {
if(!data.contains(newData)){
data.add(newData);
notifyObserver(newData);
}
}
@Override
public void notifyObserver(String data){
for (Observer observer : observerList) {
observer.update(this,data);
}
}
}
// 客户类
public class ClientTest {
public static void main(String[] args) {
View view = new View();
view.show("初始状态");
System.out.println();
// 定义与View相关的数据源
DataSource bds = new BaiduSource();
DataSource gds = new GooleDataSource();
// 为View添加观察数据源的观察者
Observer observer1 = new ObserverA(view);
Observer observer2 = new ObserverA(view);
bds.attach(observer1);
gds.attach(observer2);
// 手动更新数据
bds.updateDate("这是百度新数据--" + new Date());
System.out.println();
gds.updateDate("这是谷歌新数据--" + new Date());
}
}
第一种方法还可以再优化,上述代码中使用了两个观察者,其实两个设置不同种类的观察者,用来观察不同的内容,但是当增加观察者时,代码会变得很复杂度,而且代码会增加很多。
// 第二种方法,不定义接口,使用抽象类的方式实现
//视图类
class View {
//通过复杂转换将数据可视化,这里简单的打印
public void show(Object data) {
System.out.println(data);
}
}
abstract class DataSource{
// 相关的数据
protected String data = "";
// 存储观察者
protected List<Observer> observers = new ArrayList<>();
public String getData(){
return data;
}
public void addObserver(Observer observer){
observers.add(observer);
}
abstract public void notifyObserver();
}
// 数据源类的实现类之一:百度数据源类,谷歌数据源类代码类似,不再予以展示
class BaiduDataSource extends DataSource{
@Override
protected void updateData(String newData){
if(!newData.equals(data)){
data = newData;
notifyObserver();
}
}
@Override
public void notifyObserver(){
for(Observer observer: observers){
observer.update(this, data);
}
}
}
// 观察者接口
interface Observer{
void update(DataSource ds, String data);
}
class ObserverA implements Observer{
private View view;
public ObserverA(View view){
this.view = view;
}
@Override
public void update(DataSource ds, String data){
System.out.println("观察到" + ds.getClass().getSimpleName() + "发生变化,更新视图");
view.show(data);
}
}
// 客户测试类与第一种方法相同,不再赘述
第二种方法跟第一种方法存在一样的问题,也是观察者模式的缺点之一,当观察者越来越多的时候,代码会变得更加难以扩展维护。二者可以优化的点是:一旦有变化则通知所有的观察者,但是有些观察者对这些消息不感兴趣,所以应该只通知那些对该变化感兴趣的观察者们,所以可以定义一个Aspect类表示该变化的特点,可以采用哈希表保存观察者:
Map<Aspect, List<Observer>> map = new HashMap<>();
// 观察者注册时,必须表明自己对哪些方面的变化感兴趣
public void addObserver(Aspect aspect, Observer observer){
map.put(aspect, observer);
}
优点:
- 降低了目标和观察者之间的耦合关系,两者之间是抽象耦合关系,符合依赖倒置原则
- 目标和观察者之间建立了一套触发机制
缺点:
- 目标和观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用
- 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率,并且可能会导致意外的更新
策略模式
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
结构
- 抽象策略(Strategy):抽象角色,通过有接口或抽象类实现,该角色给出所有具体策略类所需的接口
- 具体策略(Concrete Strategy):抽象角色的实现类,提供具体的算法实现或行为
- 环境(Context,对封装的算法进行管理的角色):持有一个策略类的引用,最终给客户端调用
案例
一家百货公司在定年度的促销活动,针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动(这些促销活动在这些节日可以通用),由促销员将促销活动展示给客户
// 定义抽象策略接口
public interface Strategy{
void show();
}
// 具体策略实现类
public StrategyA implements Strategy{
@Override
public void show(){
System.out.println("买一送一");
}
}
public StrategyB implements Strategy{
@Override
public void show(){
System.out.println("满200减50");
}
}
public StrategyC implements Strategy{
@Override
public void show(){
System.out.println("满300减60");
}
}
// 定义一个环境,即管理策略的引用
public class Salesman{
private Strategy strategy;
public Salesman(Strategy strategy){
this.strategy = strategy;
}
public Strategy getStrategy(){
return strategy;
}
public void setStrategy(Strategy strategy){
this.strategy = strategy;
}
// 由促销员将促销活动展示出来
public void salesmanShow(){
strategy.show();
}
}
// 定义客户类
public class Client {
public static void main(String[] args) {
// 春节来了,使用春节促销活动
SalesMan salesMan = new SalesMan(new StrategyA());
// 展示促销活动
salesMan.salesManShow();
System.out.println("===========");
// 中秋节
salesMan.setStrategy(new StrategyB());
salesMan.salesManShow();
}
}
优点
- 策略类之间可以自由切换
- 易于扩展,增加一个新的策略,只需要增加一个具体的策略类即可,符合开闭原则
缺点
- 客户端必须知道所有的策略类,并自行决定使用哪一种策略类
- 策略模式将造成很多策略类,可以通过使用享元模式在一定程序上减少对象的数量
使用场景
- 一个系统需要动态地在集中算法中选择一种时,可以将每个算法封装在策略类中
- 多个类之区别于表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为