分享

设计模式入门教程(一)

 夜猫速读 2022-05-05 发布于湖北

1、设计模式简介

相信你只要接触过软件编程,那么一定听说过设计模式。所谓设计模式,是为解决特定问题,一套通用的、可重用的软件设计方案。我们通常所说的设计模式,是针对面向对象语言而言。对于每一位使用面向对象语言的从业者,在学习完该语言的基础知识后,也一定了解如何编写面向对象的程序。但是,面向对象究竟有什么好处,我们又应该如何灵活运用面向对象来设计程序呢?

1. 面向对象

面向对象出现之前,程序是面向过程的。两者在软件设计上有着很大的不同。面向过程,我们首要思考逻辑过程是什么,如何设计这个过程。面向对象,首先要考虑的是有哪些对象,对象有什么行为,最后才是行为的逻辑。

面向对象的可以让你程序的设计和真实世界更为契合。这会带来如下优点:

  1. 现实世界有千百年积累下来数以万亿计的优秀设计,无论是具体的机械设备,还是方法论,或者工程理论。我们都可以拿来作为软件设计的参考。

  2. 面向对象开发出的软件,让其他开发者更容易理解。我们每个人都熟知我们所生活的世界。面向对象可以让枯燥的代码更加鲜活,甚至凭你的经验,也能猜出对象的某个行为应该是怎样的逻辑。

夸了半天面向对象,你可能会想,面向对象确实不错,我所使用的语言也是面向对象的,那我编写的程序天然也就拥有了面向对象的优点。这种想法是错误的。语言仅仅是个工具,而面向对象编程是一种思维。如果对面向对象编程没有深入理解,那么你写出的程序,也只是披着面向对象的外衣而已。此外,即使你充分理解了面向对象的思维,也不一定能够灵活运用面向对象解决问题。那么,此时就轮到设计模式出场了!

2. 设计模式来自哪里

设计模式不是凭空想出来的。作为通用的设计方式,设计模式是跨语言的。要想做到跨语言,那么它的根基一定是万物都要遵守的规律。设计模式来源于真实世界,前辈们通过不断地归纳总结、实践,将一些已经存在的设计理论运用于软件领域,并很好地解决了软件设计上的问题。最终呈现给我们这些丰富的设计模式。当我们学习设计模式之后,就可以深刻体会到为什么设计模式来源于真实世界。这里我先举个例子,比如订阅者模式。一听名字你一定可以映射到现实生活中某些类似的方式,比如订报纸,订牛奶。订的人就是订阅者,送的人就是发布者。发布订阅的核心思想,再加上软件的特性,就构成了订阅者模式。

3. 设计原则

那么使用设计模式能为软件开发带来什么好处呢?这要从设计模式的设计原则说起,一般来说有6大原则,如下:

  1. 单一职责原则顾名思义,一个类最好只有一个职责。这样的好处是引起类发生变化的原因会很少。我们开发新需求的时候,就会很少去修改这个类。而且职责越单一也越容易被复用。

  2. 开闭原则软件应该对扩展开放,对修改关闭。通俗易懂的说,就是你的软件不能因为加功能,就不断地修改已有的类。而是应该通过增加类,以插拔的方式来实现。举个例子,Macbook的变压器插头是可以替换的,如果说某一天插口的标准换了,那么苹果只需要开发一个新的插头就好了,而不需要重新开发整个变压器。开闭原则确保了代码最大程度的可复用性。并且确保了成熟代码的稳定性。

  3. 里氏代换原则子类型可以替换掉自己的父类。这意味着我们编写的软件,所有使用父类的地方,都可以替换为子类对象,而程序的行为不会发生改变。通过里氏代换原则,我们可以实现开闭原则,通过增加子类实现新的功能,而不是不断地修改父类。在需要的地方则用子类代替父类。如何实现里氏代换原则呢?首先子类不能重写父类的非抽象方法,一旦重写了非抽象方法,就会改变父类的行为。但是子类可以增加自己的方法和属性,以此达到扩展的目的。

  4. 依赖倒转原则简单说就是应该依赖接口,而不是实现。也就是我们常说的面向接口编程。这样类和类之间就不会直接依赖,从而能够实现开闭原则。类依赖接口,当需要扩展的时候我们可以替换实现。

  5. 迪米特法则也称为最少知道原则。如果两个类没有必要直接通信,那么两个类就没有必要相互作用。可以通过第三方来间接调用。类之间的耦合度越弱,越容易被复用。在弱耦合的关系中,一个类的修改,造成的影响会很小。所以我们在做设计的时候需要考虑哪些应该对外暴露,哪些应该封装起来。不同功能模块间的调用,应该由更为高层的类来实现,从而屏蔽掉底层的实现。

  6. 接口隔离原则接口隔离原则指导你如何设计接口。不要让接口变得臃肿,而是应该把接口按照行为不同细拆。比如你要生产一把可以拼刺刀的步枪,那它应该实现两个接口,刀的接口和枪的接口。而不是使用一个接口覆盖所有刀和枪的所有行为。这样不同的接口可以组合使用。而且如果你只需要刀或者枪的行为,可以单独实现需要的接口, 不需要实现一个大而全的接口,从而去实现很多用不到的方法。

如果你的代码满足以上设计原则,就会更为健壮、灵活和优雅。那么如何做到上面这些原则呢?很简单,学习好设计模式,灵活运用设计模式解决你的问题。

4. 为什么要学习设计模式

前面已经给出了设计模式的定义----为解决特定的问题,一套通用的、可重用的软件设计方案。我们面对的问题不一样,需要选择不同的设计模式来解决问题。

这就好比木匠有 20 种工具,分别用于做不同的事情。而设计模式就是软件设计的工具,根据你遇到的问题不同,供你选择使用。而学习设计模式的目的,就是让你熟知工具的样子,工具能够做什么事情,解决什么样的问题。当你再遇到设计问题时,自然就会想到采用什么设计模式来解决。

设计模式有多厉害呢?我可以讲一个亲身经历,曾经我有一位同事写了一段代码来完成一个功能。code review时,我和他说你可以看一下设计模式,这段代码使用XX模式来实现会更为的优雅。两天后他找到我说:“设计模式太厉害了!感觉我前几年代码都白写了!” 你不要觉得夸张,他的这个感觉,也是我初学设计模式后的感觉----原来程序还可以这么写!以前我们解决问题的工具是锤子、斧子,而现在全都是机械化工具。设计模式就是这么神奇的东西。

5. 学习基础

只要你掌握了一门面向对象的编程语言,并且熟悉面向对象的相关知识即可。当然如果有一定的编程基础更好。此外需要了解一些UML相关的知识,方便理解类图。

6. 小结

本节从面向对象讲起,讲解了什么是设计模式,程序设计的原则有哪些,为什么要学习设计模式。设计模式是写好程序的根基。我们常常提到代码要高内聚、松耦合,代码要健壮等等。这些优秀代码的特性,全都来自于设计模式。足以见得设计模式的重要性。本小结内容比较多,总结如下:

2、工厂模式

工厂模式是平时开发过程中最常见的设计模式。工厂模式解决类的实例化问题,它属于创建型模式。工厂模式也经常会和其他设计模式组合使用。

试想你去麦当劳买一个汉堡。你只需要告诉收银员要一个xx汉堡。过一会就会有一个此类型的汉堡被制作出来。而你完全不需要知道这个汉堡是怎么被制作出来的。这个例子中你就是客户端代码,麦当劳就是工厂,负责生产汉堡。汉堡是接口,而具体的某一种汉堡,比如说香辣鸡腿堡,就是实现了汉堡接口的类。

我们继续通过另外一个例子,深入理解工厂模式。现在我们给某款音乐软件开发一个推荐功能。需求是能够根据用户选择的音乐风格,推荐不同风格的歌曲清单。那么你打算怎么实现呢?

1. 音乐推荐器1.0版本

如果之前没有学习过设计模式,很可能你的实现会是这样。编写 RecommendMusicService 类,里面有一个 Recommend方法。根据输入的风格不同,执行不同的推荐逻辑。代码如下:

public class RecommendMusicService {
    public List<String> recommend(String style) {
        List<String> recommendMusicList = new ArrayList<>();
        if ("metal".equals(style)) {
            recommendMusicList.add("Don't cry");
        } else if ("country".equals(style)) {
            recommendMusicList.add("Hotel california");
        } else if ("grunge".equals(style)) {
            recommendMusicList.add("About a girl");
        }else {
            recommendMusicList.add("My heart will go on");
        }
        return recommendMusicList;
    }
}

是不是觉得 recommed 方法太长了? OK,我们重构下,把每种音乐风格的推荐逻辑封装到相应的方法中。这样推荐方法就可以复用了。

public class RecommendMusicService {
    public List<String> recommend(String style) {
        List<String> recommendMusicList = new ArrayList<>();
        if ("metal".equals(style)) {
            recommendMetal(recommendMusicList);
        } else if ("country".equals(style)) {
            recommendCountry(recommendMusicList);
        } else if ("grunge".equals(style)) {
            recommendGrunge(recommendMusicList);
        }else {
            recommendPop(recommendMusicList);
        }
        return recommendMusicList;
    }
    private void recommendPop(List<String> recommendMusicList) {
        recommendMusicList.add("My heart will go on");
        recommendMusicList.add("Beat it");
    }
    private void recommendGrunge(List<String> recommendMusicList) {
        recommendMusicList.add("About a girl");
        recommendMusicList.add("Smells like teen spirit");
    }
    private void recommendCountry(List<String> recommendMusicList) {
        recommendMusicList.add("Hotel california");
        recommendMusicList.add("Take Me Home Country Roads");
    }
    private void recommendMetal(List<String> recommendMusicList) {
        recommendMusicList.add("Don't cry");
        recommendMusicList.add("Fade to black");
    }
}

这样是不是很完美了!recommend 方法精简了很多,而且每种不同的推荐逻辑都被封装到相应的方法中了。那么,如果再加一种风格推荐怎么办?这有什么难,recommed 方法中加分支就好啦。然后在 RecommendMusicService 中增加一个对应的推荐方法。等等,是不是哪里不太对?回想一下设计模式6大原则的开闭原则----对扩展开放,对修改关闭。面对新风格推荐的需求,我们一直都在修改 RecommendMusicService 这个类。以后每次有新风格推荐要添加,都会导致修改 RecommendMusicService 。显然这是个坏味道。

那么如何做到实现新的风格推荐需求时,满足开闭原则呢?

2. 音乐推荐器2.0版本

添加新需求时,如何做到不修改,去扩展?是不是想到了单一职责?是的,类的职责越单一,那么它就越稳定。RecommendMusicService 类的职责太多了,负责n种风格的推荐。OK,那么我们第一件事就是要减少 RecommendMusicService 类的职责,把每种不同风格的推荐提取到不同的类当中。比如MetalMusicRecommendServicePopMusicRecommendServiceCountryMusicRecommendService。这些类都可以通过 recommed 方法生成推荐的歌曲清单。而 RecommendMusicService 类只是通过调用不同 MusicRecommendService 的 recommed 方法来实现推荐。代码如下:

MetalMusicRecommendService 类:

public class MetalMusicRecommendService {
    public List<String> recommend(){
        List<String> recommendMusicList = new ArrayList<>();
        recommendMusicList.add("Don't cry");
        recommendMusicList.add("Fade to black");
        return recommendMusicList;
    }
}

同类型的还有 GrungeMusicRecommendServicePopMusicRecommendServiceCountryMusicRecommendService

现在我们来改造 MusicRecommendService 类:

public class RecommendMusicService {
    private MetalMusicRecommendService metalMusicRecommendService = new MetalMusicRecommendService();
    private GrungeMusicRecommendService grungeMusicRecommendService = new GrungeMusicRecommendService();
    private CountryMusicRecommendService countryMusicRecommendService = new CountryMusicRecommendService();
    private PopMusicRecommendService popMusicRecommendService = new PopMusicRecommendService();
    public List<String> recommend(String style) {
        List<String> recommendMusicList = new ArrayList<>();
        if ("metal".equals(style)) {
            metalMusicRecommendService.recommend();
        } else if ("country".equals(style)) {
            countryMusicRecommendService.recommend();
        } else if ("grunge".equals(style)) {
            grungeMusicRecommendService.recommend();
        }else {
            popMusicRecommendService.recommend();
        }
        return recommendMusicList;
    }
}

改造后,如果有了新音乐风格推荐的需求,只需要增加相应的 xxxMusicRecommendService 类。然后在 RecommendMusicService 中增加相应分支即可。这样就做到了开闭原则。那么还有什么违背设计原则的地方吗?RecommendMusicService 是不是依赖的 xxMusicRecommendService 类太多了?

没错,而且这么多类,实际上都是做推荐的事情,且都是通过 recommend 方法提供推荐结果。这完全可以抽象出接口,比如 MusicRecommendInterface。那么 RecommendMusicService 依赖 MusicRecommendInterface 就可以了。这解决了依赖反转问题----应该依赖接口,而不是依赖具体实现

我们又复习了单一职责和依赖反转原则。不愧是指导设计模式的原则,真的是无处不在。依赖 MusicRecommendInterface 没问题,但是不同的音乐风格,怎么能实例化 MusicRecommendInterface 的某个具体实现呢?工厂模式于是就应运而生了!

3. 音乐推荐器3.0版本

我们回顾一下文章开头说到,工厂模式解决的是类的实例化。无论你需要哪种风格的 MusicRecommendService,只需要告诉工厂,工厂会给你实例化好你需要的具体实现。而工厂能做到这些是基于继承和多态。RecommendMusicService 只需要依赖 MusicRecommendInterface,具体需要哪个MusicRecommendService 的实现,只需要告诉 RecommendServiceFactory 即可。MusicRecommendService 拿到具体的实现后调用它的 recommand 方法,就可以得到相应风格的推荐歌曲清单。

首先我们需要定义所有 MusicRecommendService 要实现的接口,很简单,只有一个 recommend 方法:

public interface MusicRecommendInterface {
    List<String> recommend();
}
代码块123

我们2.0版本中的 xxxMusicRecommendService 都需要实现此接口,例如:

public class GrungeMusicRecommendService implements MusicRecommendInterface {
    public List<String> recommend() {
        List<String> recommendMusicList = new ArrayList<>();
        recommendMusicList.add("About a girl");
        recommendMusicList.add("Smells like teen spirit");
        return recommendMusicList;
    }
}

不同音乐风格的推荐逻辑在各自实现的 recommend() 方法中。下面就是工厂模式中的工厂代码了,其实很简单,只是根据不同的参数实例化不同的实现并返回。

public class MusicRecommendServiceFactory {
    MusicRecommendInterface createMusicRecommend(String style) {
        if ("metal".equals(style)) {
            return new MetalMusicRecommendService();
        } else if ("country".equals(style)) {
            return new CountryMusicRecommendService();
        } else if ("grunge".equals(style)) {
            return new GrungeMusicRecommendService();
        } else {
            return new PopMusicRecommendService();
        }
    }
}

我们再来看看 RecommendMusicService 的代码:

public class RecommendMusicService {
    private MusicRecommendServiceFactory recommendMusicServiceFactory = new MusicRecommendServiceFactory();
    public List<String> recommend(String style) {
        MusicRecommendInterface musicRecommend = recommendMusicServiceFactory.createMusicRecommend(style);
        return musicRecommend.recommend();
    }
}

是不是简单多了,已经不再依赖那么多的 MusicRecommendInterface 的实现了。它要做的事情仅仅是通过工厂得到想要的 RecommendMusicService 实现,然后调用它的 recommend() 方法,就可以得到你想要的推荐结果。类图如下:以上三种实现方式总结如下:

4. 小结

本节我们通过音乐推荐器的例子,实践了如何找到程序中违反设计原则的地方,并通过工厂模式来解决这些问题。使用设计模式可以让程序更符合程序设计原则,从而写出更为健壮的代码。我们应牢记工厂模式解决的是类的实例化问题。这个例子很简单,不过涉及到的知识点却很多。有封装、多态、单一职责和依赖反转等。可见要想把程序设计好,必须熟练掌握这些基本概念和原则。

3、策略模式

大家一定都使用过电子地图。在地图中输入出发地和目的地,然后再选取你的出行方式,就可以计算出最优线路以及预估的时长。出行方式有驾车、公交、步行、骑行等。出行方式不同,计算的线路和时间当然也不同。其实出行方式换个词就是出行策略。而策略模式就是针对此类问题的设计模式。生活中这种例子太多了,比如购物促销打折的策略、计算税费的策略等等。相应的策略模式也是一种常用的设计模式。本节我们会以电子地图为例,比较工厂模式和策略模式,讲解策略模式的原理。最后结合工厂模式改造策略模式的代码实现,以达到更高的设计目标。

1. 实现策略模式

接下来我们就以电子地图为例,讲解如何用策略模式实现。不过先别着急,上一节我们学习了工厂模式,看起来电子地图也可以用工厂模式来实现。所以我们先来看看用工厂模式如何实现。下面的例子为了方便展示,接口入参只有出行方式,省略了出发地和目的地。计算结果是预估时长。

1.1 工厂模式实现电子地图

首先我们需要一个策略接口,不同策略实现该接口。再搭配一个策略工厂。客户端代码只需要根据用户的出行方式,让工厂返回具体实现即可,由具体的实现来提供算法计算。以工厂模式实现的电子地图代码如下。TravelStrategy接口代码:

public interface TravelStrategy {
    int calculateMinCost();
}

TravelStrategy接口的实现代码:

public class SelfDrivingStrategy implements TravelStrategy {
    @Override
    public int calculateMinCost() {
        return 30;
    }
}

TravelStrategyFactory代码:

public class TravelStrategyFactory {
    public TravelStrategy createTravelStrategy(String travelWay) {
        if ("selfDriving".equals(travelWay)) {
            return new SelfDrivingStrategy();
        } if ("bicycle".equals(travelWay)) {
            return new BicycleStrategy();
        } else {
            return new PublicTransportStrategy();
        }
    }
}

TravelService对外提供计算方法,通过工厂生成所需要的 strategy。代码如下:

public class TravelService {
    private TravelStrategyFactory travelStrategyFactory = new TravelStrategyFactory();
    public int calculateMinCost(String travelWay) {
        TravelStrategy travelStrategy = travelStrategyFactory.createTravelStrategy(travelWay);
        return travelStrategy.calculateMinCost();
    }
}

代码结构和我们上一节讲解的音乐推荐器几乎一模一样。看似也很好地解决了我们的设计问题。接下来我们看看如何用策略模式解决这个问题,然后我们再对两种模式做对比。

1.2 策略模式实现电子地图

使用策略模式,需要增加一个策略上下文类(Context)。Context类持有策略实现的引用,并且对外提供计算方法。Context类根据持有策略的不同,实现不同的计算逻辑。客户端代码只需要调用 Context 类的计算方法即可。如果想切换策略实现,那么只需要改变Context类持有的策略实现即可。TravelStrategy 接口和实现的代码不变,请参照上面工厂模式中给出的代码。其他代码如下:StrategyContext 类:

public class StrategyContext {
    private TravelStrategy strategy;
    public StrategyContext(TravelStrategy strategy) {
        this.strategy = strategy;
    }
    public int calculateMinCost(){
        return strategy.calculateMinCost();
    }

StrategyContext 持有某种 TravelStrategy 的实现,它对外提供的calculateMinCost 方法,实际是对 TravelStrategy 做了一层代理。想切换不同算法的时候,只需更改 StrategyContext 持有的 TravelStrategy 实现。

TravelService 对外提供计算方法,代码如下:

public class TravelService {
    private StrategyContext strategyContext;
    public int calculateMinCost(String travelWay){
        if ("selfDriving".equals(travelWay)) {
            strategyContext = new StrategyContext(new SelfDrivingStrategy());
        } if ("bicycle".equals(travelWay)) {
            strategyContext = new StrategyContext(new BicycleStrategy());
        } else {
            strategyContext = new StrategyContext(new PublicTransportStrategy());
        }
        return strategyContext.calculateMinCost();
    }
}

可以看到 TravelService 中只会和 Context 打交道,初始化 Context 时,根据不同的出行方式,设置不同的策略。看到这里你是不是会有疑问,使用工厂模式消除了客户端代码的条件语句。怎么使用策略模式,条件语句又回来了?别急,我们继续向下看。

最后我们看一下策略模式的类图:

2. 策略模式优缺点

2.1 优点

  1. 使用策略模式,可以根据策略接口,定义一系列可供复用的算法或者行为;

  2. 调用方只需要持有Context的引用即可。而无需知道具体的策略实现。满足迪米特法则;

  3. Context 在策略的方法之外可以做一些通用的切面逻辑。

GOF的《设计模式》著作中认为策略模式可以消除一些条件语句,我对此持怀疑态度。正如上面的例子,虽然由于Context在初始化的时候已经指定了策略实现,在计算逻辑中不需要根据条件选择逻辑分支。但是,客户端代码在初始化Context的时候,如何判断应该传入哪个策略实现呢?其实在客户端代码或者别的地方还是缺少不了条件判断。所以这里消除条件语句,只是针对算法逻辑的条件判断。

第一个优点是策略模式解决的核心问题。但其实工厂模式也是可以做到的。第二点,我认为很重要,客户端代码只需要和 Context 打交道即可,避免了和不同策略类、工厂类的接触。工厂模式中,客户端代码需要知道工厂类和产品类,两个类。正好复习一下迪米特法则,如果两个类没有必要直接通信,那么两个类就没有必要相互作用。可以通过第三方来间接调用。

2.2 缺点

  1. 客户端代码需要知道不同的策略以及如何选择策略。因此可以看到上面的客户端代码有着丑陋的条件判断;

  2. 由于策略类实现同样的接口,所以参数列表要保持一致,但可能并不是所有的策略都需要全部参数。

3. 策略模式与工厂模式结合使用

针对第一个缺点。我们可以通过策略模式与工厂模式结合使用来改进。通过进一步封装,消除客户端代码的条件选择。

我们修改一下StrategyContext类,代码如下:

public class StrategyContext {
    private TravelStrategy strategy;
    public StrategyContext(String travelWay) {
        if ("selfDriving".equals(travelWay)) {
            strategy = new SelfDrivingStrategy();
        } if ("bicycle".equals(travelWay)) {
            strategy = new BicycleStrategy();
        } else {
            strategy = new PublicTransportStrategy();
        }
    }
    public int calculateMinCost(){
        return strategy.calculateMinCost();
    }
}

可以看到我们初始化的逻辑和工厂的逻辑很相似。这样条件判断就提炼到 Context 类中了。而客户端代码将会简洁很多,只需要在初始化 StrategyContext 时,传入相应的出行方式即可。代码如下:

public class TravelService {
    private StrategyContext strategyContext;
    public int calculateMinCost(String travelWay){
        strategyContext = new StrategyContext(travelWay);
        return strategyContext.calculateMinCost();
    }
}

改进后,客户端代码现在已经完全不知道策略对象的存在了。条件判断也被消除了。其实很多时候我们都是通过搭配不同设计模式来达到我们的设计目标的。

策略+工厂模式类图如下:

4. 策略模式适用场景

当存在多种逻辑不同,但属于同一类型的行为或者算法时,可以考虑使用策略模式。以此来消除你算法代码中的条件判断。同时让你的代码满足多种设计原则。

很多时候,工厂模式和策略模式都可以为你解决同类问题。但你要想清楚,你想要的是一个对象,还是仅仅想要一个计算结果。如果你需要的是一个对象,并且想用它做很多事情。那么请使用工厂模式。而你仅仅想要一个特定算法的计算结果,那么请使用策略模式。

策略模式属于对象行为模式,而工厂属于创建型模式。策略模式和工厂模式对比如下:

5. 小结

策略模式解决的问题是如何封装可供复用的算法或者行为。策略模式满足了单一职责、开闭、迪米特法则、依赖倒转等原则。我们一定想清楚策略模式的适用场景,否则某些时候你会搞不清到底用工厂模式还是策略模式。最后提醒大家,设计模式很多时候都是混合使用,我们不应该局限于使用某一种设计模式来解决问题。

4、单例模式

单例模式是设计模式中最简单的设计模式之一。他和工厂模式同属于创建型模式,都用于类的实例化。不过两者的区别很大,要解决的问题也不一样。

单例模式保证一个类只会被实例化一次,使用的时候通过单例提供的方法来获取实例。在确保线程安全的前提下,很多时候我们只需要同一个类的一个实例即可,而不是在任何使用的地方都实例化一个新对象。新对象创建是有成本的,不但要花时间,而且占用内存。另外有的时候我们需要一个全局唯一的实例,比如计数器,全局多个计数器就会计数混乱不准确,如下图所示。单例模式就是为了实现全局一个实例的需求。

1. 实现单例模

实现单例模式,其实我们需要实现如下需求:

  1. 提供获取实例的方法。此方法会控制全局仅有一个实例,而不会重复创建实例;

  2. 全局唯一的实例要有地方能存放起来;

  3. 不能随意通过new关键字创建实例。这样才能控制调用方只能用受控的方法来创建对象。

针对以上三点需求我们需要做如下事情:

  1. 编写一个获取实例的公有方法,已经创建过实例就直接返回实例,否则进行实例化;

  2. 实例化好的对象存哪里呢?存在类当中是最好的。这样不用引入新的类,而且也符合就近原则;

  3. 禁止通过new关键字初始化,只需要把无参构造方法私有化。此外不要添加任何有参数的构造方法。

我们按照上面的思路实现第一版单例模式,代码如下:

public class SingletonOne {
    private static SingletonOne singletonOne;
    private SingletonOne() {
    }
    public static SingletonOne getInstance() {
        if (singletonOne == null) {
            singletonOne = new SingletonOne();
        }
        return singletonOne;
    }
}

代码中使用静态变量,也称之为类变量保存SingletonOne的实例。无参构造方法私有化,并且不提供其他构造方法。getInstance() 对外提供获取实例的方法。方法内部也符合我们的需求,已经实例化,直接返回实例,如果还是null,去创建这个实例。这种方式称之为懒汉式,是因为类的实例化延迟到第一次getInstance的时候。

看起来上面的代码实现了我们提到的三点需求,无懈可击。没错,一般的场景采用上面的代码足以应付。但是在并发的时候,上面的代码是有问题的。并发时,两个线程对于 singletonOne == null 的判断可能都满足,那么接下来每个线程各自都创建了一个实例。这和单例模式的目标是相违背的。我们需要改造一下。

1.1 线程安全的懒汉单例模式

想要线程安全还不好说,加上 Synchronized 关键字就可以了。修改后代码如下:

public class SingletonTwo {
    private static SingletonTwo singletonTwo;
    private SingletonTwo() {
    }
    public static SingletonTwo getInstance() {
        if (singletonTwo == null) {
            synchronized (SingletonTwo.class) {
                if (singletonTwo == null) {
                    singletonTwo = new SingletonTwo();
                }
            }
        }
        return singletonTwo;
    }
}

实例化之前为了确保线程安全,我们加上了 synchronized 关键字。你肯定注意到 synchronized 代码块中,又判断了一次 singletonTwo 是否为 null。这是因为你在等待锁的这段时间,可能其他线程已经完成了实例化。所以此处加上 null 的判断,才能确保全局唯一!

看到这里你一定赞叹,这是多么严谨的程序,一定不会有错了!但是事实却不是这样。

如果你学习过多线程,一定对重排序有印象。CPU 为了提高运行效率,可能会对编译后代码的指令做优化,这些优化不能保证代码执行完全符合编写的顺序。但是一定能保证代码执行的结果和按照编写顺序执行的结果是一致的。重排序在单线程下没有任何问题,不过多线程就会出问题了。其实解决方法也很简单,只需要为

singletonTwo 声明时加上 volatile 关键字即可。volatile 修饰的变量是会保证读操作一定能读到写完的值。这种单例也叫做双重检查模式。

代码如下:

public class SingletonTwo {
    private volatile static SingletonTwo singletonTwo;
    private SingletonTwo() {
    }
    public static SingletonTwo getInstance() {
        if (singletonTwo == null) {
            synchronized (SingletonTwo.class) {
                if (singletonTwo == null) {
                    singletonTwo = new SingletonTwo();
                }
            }
        }
        return singletonTwo;
    }
}

1.2 饿汉式单例模式

有懒汉就有饿汉。饿汉式单例模式在类初实话的时候就会进行实例化。好处是不会有线程安全的问题。问题就是不管程序用不用,实例都早以创建好,这对内存是种浪费。代码如下:

public class SingletonThree {
    private static SingletonThree singletonOne = new SingletonThree();
    private SingletonThree() {
    }
    public static SingletonThree getInstance() {
        return singletonOne;
    }
}

1.3 内部静态类方式

这次我们先看代码:

public class SingletonFour {
    private SingletonFour() {
    }
    public static SingletonFour getInstance() {
        return SingletonHolder.singletonFour;
    }
    private static class SingletonHolder{
        private static final SingletonFour singletonFour = new SingletonFour();
    }
}

代码中增加了内部静态类 SingletonHolder,内部有一个SingletonFour的实例,并且也是类级别的。那这种方式是饿汉式还是懒汉式?看起来像是饿汉式,因为实例化也是在类初实话的时候进行的。但如果是饿汉式,为什么还要兜这个圈?

其实这是懒汉式。因为内部静态类是现在第一次使用的时候才会去初始化。所以SingletonHolder最初并未被初始化。当第一次执行 return SingletonHolder.singletonFour 时,才会去初始化SingletonHolder类,从而实例化SingletonFour。这种方式利用类加载的机制达到了双重检查模式的效果,而代码更为简洁。

2. 单例模式适用场景

  1. 必须保证全局一个实例。如计数器,多个实例计数就不准确了。再比如线程池,多个实例的话,管理就乱套了。

  2. 一个实例就能满足程序不同地方的使用,并且是线程安全的。比如我们使用 Spring 开发的 bean,绝大多数都可以用单例模式。例如某个 service 类,因为自己不维护状态,线程安全,其实全局只需要一个实例。

  3. 对象被频繁创建和销毁,可以考虑使用单例。

  4. 对象创建比较消耗资源。

3. 小结

我们本节学习了四种单例的实现方式:

  1. 饿汉式非线程安全;

  2. 懒汉式线程安全(双重检查模式);

  3. 饿汉式单例模式;

  4. 内部静态类方式。

单例模式虽然简单,但是想写的严谨,还是需要考虑周全。实际使用中,推荐使用双重检查模式和内部静态类方式。如果实例在你的程序初始化阶段就会被使用,也可以使用饿汉式。非线程安全的懒汉式只能用于非并发的场景,局限性比较大,并不推荐使用。

5、代理模式

代理模式又称为委托模式。代理模式从字面上很好理解,有些事情你可以不必亲自去做,而是通过更为专业的人或者机构去做。比如你开公司需要工商注册。你可以选择自己去了解相关的规章制度,亲自跑腿去办理。也可以通过专业机构办理。这样你就不需要去了解办理的细节,只需把材料交给机构。机构会为你完成注册工作。在这个过程中,机构不但会为你完成机构注册,而且还有可能为你办理一些你并不知道的增值服务。这就是典型的代理模式。

1. 实现代理模式

我们再看一个生活中的例子。房屋买卖中经常会出现代理的情况。当卖家不在房屋所在地时,可能会委托自己的亲人或者朋友进行交易。而买方会和代理人直接进行交易。交易中间的问题代理人会回答,手续代理人会办理。如下图:这个代理人不太老实,私自加了20万,想赚差价。所以不要以为代理人真的只是代理,在这个过程中他可以加入自己的逻辑处理。而客户和被代理人并不知道。

我们看看采用代理模式如何实现这个场景。

首先真正卖掉房子的还是房主,只不过和买房人直接进行买卖的是代理人。那么房主和代理人有一个公共的行为都是卖房。那么我们可以抽象出一个接口定义卖房的行为。房主和代理人都需要实现这个接口。真正的卖房逻辑在房主的实现中,代理人的卖房实现只是调用房主的实现而已。要达到这个目的,代理人需要持有房主的引用。而买方进行买卖的时候,仅和代理人打交道。不用知道房主是谁,也不用让房主到现场过户。甚至连房主身在何处都不知道。

上面其实就是这个例子的程序设计。代码如下:

房屋交易接口代码:

public interface RealEstate {
    void sell();
}
代码块123

房主代码:

public class Seller implements RealEstate {
    @Override
    public void sell() {
        System.out.println("卖了房子");
    }
}

代理人代码:

public class SellerProxy implements RealEstate{
    private Seller seller;
    @Override
    public void sell() {
        if(seller==null){
            seller = new Seller();
        }
        seller.sell();
        System.out.println("退税办理完毕");
    }
}

类图:代理人在这里有什么用处呢?没有代理人,直接和房主买就好了啊?试想下,假如现在有了新的买房政策,交易完成后可以退税,那么在不修改房主代码的前提下,我们只需要修改此代理人的代码即可。如果在其他地方卖房没有此政策,只需要定义另外一个地区的代理人即可,这里实现了开闭原则。其实代理模式还有很多好处和适用的场景。我们下面详细来看。

2. 代理模式优缺点

2.1 优点

代理模式在客户端代码和真正的逻辑代码中引入了一层代理,这样做有很多好处:代理模式

  1. 隐藏逻辑的真正实现对象。上面的例子中,如果卖房人身份特殊,那么通过代理人来卖房,可以不让买房人接触到自己;

  2. 隐藏委托类的某些行为,在代理类认为应该触发时再触发;

  3. 代理类可以为委托类的行为附加一些逻辑处理,例如上例中的退税。

2.2 缺点

  1. 代理类和委托类实现同一个接口,因此只能面向接口代理;

  2. 代理类和委托类实现同一个接口。即使代理类只想代理某个行为,也需要实现接口所有方法;

  3. 代理类和委托类需要一一对应。如果你有段逻辑需要对所有的方法都附加上,静态代理是无法实现的。

3. 代理模式适用场景

针对代理模式的三个优点,我们来看看有哪些适用场景。

  1. 远程调用代理:在分布式系统中,我们经常会调用其他系统的服务。通过代理模式,可以对客户端代码隐藏远程调用的细节;

  2. 虚代理:有一个典型的场景,加载一个包含大量大 size 图片的页面时,为了更好的用户体验,可以通过图片代理类先把图片的位置占好,保证排版的正确。当滚动到某个图片位置的时候才去加载图片;

  3. 保护代理:当委托对象需要访问权限控制时,可以通过代理类来控制权限进行保护;

  4. 智能指引:为委托对象增加一层控制。比如记录访问次数,当为 0 的时候,可以释放掉。第一次引用一个对对象时,把它装入内存。访问委托对象前,检查是否已经有其他访问已经锁定了它,以确保其他对象不能改变它。

4. 小结

代理模式由 Suject 接口,RealSubject 实现和 Proxy 类构成。Proxy 类同样要实现 Suject 接口。同时 Proxy 类依赖 RealSubject 类。代理模式对方的调用增加了间接性。利用间接性,可以加入额外的逻辑。这也是我们常说的 AOP,即面向切面编程。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多