前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计模式的十八般武艺

设计模式的十八般武艺

作者头像
ClericYi
发布2020-06-23 15:23:55
3330
发布2020-06-23 15:23:55
举报
文章被收录于专栏:ClericYi's Blog

六大原则

单一职责原则

定义:就一个类而言,应该仅有一个引起它变化的原因。

其实字面意思就已经表达的比较明确,单一,也就是干尽量少的事情。在HDU中可以对耦合和内聚程度的评判有一定的了解。

什么叫做少,其实很难有一个标准。但是在Android的MVC框架中,Activity既作为View,又起着Controller的作用的时候是否显得会很臃肿呢?他需要进行页面的刷新,网络处理,消息处理等等,写起来容易,但是在我们进行维护的时候,是不是会很头疼呢,这就是单一职责原则的对应所在了。

开放封闭原则

定义:类,模块,函数等应该是可以扩展的,但是不可以修改。

在日常的项目开发中,需求一直是处于一个变动的状态,但是这同样也会成为项目开发的壁垒,如果你的Bean今天是一只猫,明天就需要是一只狗呢?重新打补丁吗?显然是一个很不合适的做法。而开放封闭原则,解决的就是这一类问题。不论是猫,还是狗,他们总会有相同的特征,抽象化也就是这个原则实现的基础。

代码语言:javascript
复制
// 定义一个动物抽象类
public abstract class Animal {
    abstract void do();
}
 
// 猫实现抽象方法
class Cat extends Animal {
    @Override
    void do() {
        System.out.println("喵");
    }
}
 
// 狗实现抽象方法
class Dog extends Animal {
    @Override
    void do() {
        System.out.println("汪");
    }
}

里氏替换原则

定义:所有引用基类(一般来说都是抽象类或接口)的地方必须能透明的使用其子类的对象。

定义的具体含义就是将一个基类对象替换成其子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。

想来直接看定义可能有点难以理解,接下来就用代码来具体实现里氏替换原则了。

代码语言:javascript
复制
/**
 * 基于里氏替换原则实现
 */
class Manage1 {
    private Animal animal;
 
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
 
    void getSound() {
        animal.do();
    }
}

/**
 * 使用子类实现
 */
class Manage2 {
    private Cat cat;
 
    public void setAnimal(Cat cat) {
        this.cat = cat;
    }
 
    void getSound() {
        cat.do();
    }
}

以上基于两种写法,给予读者评判,如果使用Manage2,如果我们希望获得Dog的声音,那么就需要重新实现Manager3,然后差异就是只是把Cat置换成Dog。而Manage1很好的解决了这个问题,因为不论是Cat还是Dog都是Animal的子类。

依赖倒置原则

定义:高层模块不应该依赖底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

两个概念:

  • 抽象/高层模块:接口或者抽象类,一般只定义了有什么操作,但是没有具体实现。
  • 细节/底层模块:实现接口或者继承抽象类而产生的,一般来说就是我们通过new产生出来的对象。

这里的代码与开闭原则、里氏替换原则一致,详细见于上文。

DogCat作为Animal的子类,对于do()函数,父类只是一个抽象的方法,而子类完成了具体的实现。而当类Manage1希望与类Cat发生关联时,就是通过类Animal来完成,好处就是Manage1不再关注了类CatDog的具体实现。

迪米特原则

定义:一个软件实体应当少的与其他实体发生相互作用。

其实和上面的内容差不多,同样都是为了降低耦合程度,直接用Demo证明就清楚明了了。

电话网络形式:

  • 打电话的人 --> 接电话的人
  • 打电话的人 --> 中间服务商转接 --> 接电话的人

第一种已经被时代抛弃了,虽然我们并不知觉,但是第一种电话网络模式,如果用在现代社会,那么带来的后果就是你再也看不见太阳了,头顶密密麻麻的电话线,更何况那是座机互联的时代,能够靠电话线来解决,但是这个时代呢?移动互联的时代呢,不能再靠着电话线来解决问题,而是中间商的介入就改变了这个现状。

(1)第一种电话网络模式

代码语言:javascript
复制
/**
 * 打电话的人
 */
class Call {
    private Receiver receiver;
 
    public void setReceicer(Receiver receiver) {
        this.receiver = receiver;
    }
 
    public void call() {
        receiver.receive();
    }
}

/**
 * 接电话的人
 */
class  Receiver{
    
    private String number;
    Receiver(String number){
         this.number = number;
    }
    
    public void receive() {
        System.out.println("接通电话");
    }
}

class Main{
    public static void main(String[] args) {
        Call call = new Call();
        call.setReceicer(new Receiver("电话号码"));
        call.call();
    }
}

代码虽然看着很轻松,但是折射到现实情况的时候,每一个拨打电话和接收电话的人之间都等于连接了一条电话线。

(2)第二种电话网络模式

代码语言:javascript
复制
/**
 * 转接
 */
public abstract class Manager {
    abstract void link();
}
 
/**
 * 打电话的人
 */
class Call {
    private Manager manager;
    
    public void setManager(Manager manager) {
        this.manager = manager;
    }
 
    public void call() {
        manager.link();
    }
}
 
/**
 * 接电话的人
 */
class  Receiver{
    
    private String number;
    Receiver(String number){
         this.number = number;
    }
    
    public void receive() {
        System.out.println("接通电话");
    }
}

class CMCC extends Manager {
    private String number;
  
    CMCC(String number){
      this.number = number;
    }

    public void link() {
        System.out.println("连接接电话的人");
        Receiver receiver = new Receiver();
        receiver.receive();
    }
}

class Main{
    public static void main(String[] args) {
        Call call = new Call();
        call.setManager(new CMCC("电话号码"));
        call.call();
    }
}

这个时候两个实体通过加入中间商的形式降低了耦合度。也就像我们生活中的各个电话厂商,你不会因为厂商不同而担心不能拨通电话,因为中间的处理过程厂商会帮你解决。

接口隔离原则

定义:一个类对另一个类的依赖应该建立在最小的接口上

一个接口内要实现的函数数量可控,有那么一点像数据库里的第一范式。让我们从Demo看看使用原则的与否的不同之处。

正常实现

代码语言:javascript
复制
// 特征
interface Character {
    void look();
    void nature();
}

class Dog implements Character{
    @Override
    void look() {
        System.out.println("能看");
    }
    @Override
    void nature() {
        System.out.println("淘气");
    }
}

基于接口隔离原则实现

代码语言:javascript
复制
// 外观
interface Facade {
     void look();
}
// 内在
interface Inherent {
    void nature();
}

class Dog implements Inherent{
    @Override
    void nature() {
        System.out.println("淘气");
    }
}

几个常用的设计模式

单例模式

定义:保证一个类仅有一个实例,并提供用于一个访问它的全局访问点。

四种写法及其优缺点

(1) 饿汉模式

代码语言:javascript
复制
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;
    }
}

在类加载时就已经完成了初始化。

优点:1. 保障了线程同步的问题;2. 获取对象的效率高。

缺点:1. 降低了类加载时速度;2. 如果一直不使用,会内存的浪费。

(2) 懒汉模式

  1. 线程不安全
代码语言:javascript
复制
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null) instance = new Singleton();
        return instance;
    }
}

缺点:存在线程同步问题

  1. 线程安全
代码语言:javascript
复制
public class Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if(instance == null) instance = new Singleton();
        return instance;
    }
}

缺点:每一次都需要同步,存在一定的开销问题。

懒汉模式相较于饿汉模式,不会存在不使用的问题。虽然不再在加载时消耗资源,但是实例化时同样会有一定的时间开销。

(3) 双重检查模式/DCL

代码语言:javascript
复制
public class Singleton {
    private volatile static Singleton instance;
    private Singleton(){}
    public static  Singleton getInstance(){
        if(instance == null) {
            synchronized (Singleton.class){
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

使用volatile关键词是对正确性的一种保障。

相较于懒汉模式而言,这又是一种升级。因为不在将synchronized套在了函数上,也就不会每次调用都对整个函数同步了,提高了资源的利用率。但是同样存在失效的情况。

存在失效的原因(对使用volatile的解释) 因为JVM的加载顺序是一个无序状态,他可能进行过指令优化的重排操作,那这种情况就是我们不可控制的,而volatile起着不被忽略的作用,保证了我们的instance不被指令重排。这也就是他的优化方法。

(4) 静态内部类单例模式

代码语言:javascript
复制
public class Singleton {
    private Singleton(){}
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    private static class SingletonHolder {
        private volatile static Singleton instance = new Singleton();
    }
}

这是最常用的方法,也是对DCL的一种升级。

优缺点和前面都差不多,就不再复述了。

模式分析

使用这个模式时说明系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。

缺点:

  • 单例类的扩展有很大的困难
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  • 如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失

工厂模式

工厂模式分为三种:

(1)简单工厂模式

(2)工厂方法模式

(3)抽象工厂模式

所以接下来将从这三种模式中进行分析和比较。

简单工厂模式

写法
代码语言:javascript
复制
/**
 * 工厂类
 */
public class Factory {
    public static Product createProduct(String type){
        Product product = null;
        switch (type){
            case "鸡翅":
                product = new ChickenWing();
                break;
            case "汉堡":
                product = new Hamburger();
                break;
        }
        return product;
    }
}

/**
 * 抽象产品类
 */
public abstract class Product {
    public abstract void use();
}

/**
 * 具体产品类
 */
public class Hamburger extends Product {
    @Override
    public void use() {
        System.out.println("汉堡制作完成");
    }
}

public class ChickenWing extends Product {
    @Override
    public void use() {
        System.out.println("鸡翅制作完成");
    }
}
模式分析

将类的创建细节与使用者隔离,使用者只需要知道对应的参数,将其送入工厂中即可完成创建。就比如我想吃汉堡了,那我就告诉工厂,汉堡这个关键参数,那么工厂就会将汉堡这个好吃的家伙送给我。但是这个工厂要注意,他可以制造鞋子、袜子、零食。。。你所能想到的想要找他做的他都得学会。

缺点:

  • 工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。

工厂方法模式

工厂方法

写法
代码语言:javascript
复制
/**
 * 抽象工厂类
 */
public abstract class Factory {
    public abstract <T extends Product> T  createProduct(Class<T> clazz);
}

/**
 * 具体工厂类
 */
public class KFC extends Factory {
    @Override
    public <T extends Product> T createProduct(Class<T> clazz) {
        Product product = null;
        try{
            product = (Product) Class.forName(clazz.getName()).newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
        return (T) product;
    }
}
模式分析

与简单工厂模式不一样的地方在于我们创建了专门的工厂,也就是说比如我们今天想吃汉堡,但是同时有肯德基、麦当劳、汉堡王等好几家公司可以生产,就轮到了我们选择谁来进行制作的问题了。但是这样的模式依旧存在一个问题,那就是我们需要专门跑到肯德基、麦当劳又或者是汉堡王的门店去,我们才能点餐。

缺点:

在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。

抽象工厂模式

抽象工厂

写法
代码语言:javascript
复制
public class FactoryProducer {
   public static AbstractFactory getFactory(String factory){
      if(factory.equalsIgnoreCase("KFC")){
         return new KFC();
      } 
      return null;
   }
}
模式分析

再次与上述的工厂方法模式进行比较,这次我们不需要再到肯德基门店去就可以买汉堡了,为什么呢?因为我们现在手上有饿了么,有美团了。我在搜索栏中输入了肯德基,他就告诉我了有这样的一个工厂,这样我们就能远程遥控获得这样的一个我们想要的油炸食品了。

缺点:

在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改

观察者模式

写法

代码语言:javascript
复制
/**
 * 抽象观察者类
 */
public interface IObserver {
    void update(String message);
}

/**
 * 抽象主题类
 */
public interface ISubject {
    void add(IObserver observer);
    void remove(IObserver observer);
    void notify(String message);
}

/**
 * 具体观察者类
 */
public class Observer implements IObserver {
    @Override
    public void update(String message) {
        System.out.println(message);
    }
}

/**
 * 具体主题类
 */
public class Subject implements ISubject {
    List<IObserver> list = new ArrayList<>();

    @Override
    public void add(IObserver observer) {
        list.add(observer);
    }

    @Override
    public void remove(IObserver observer) {
        list.remove(observer);
    }

    @Override
    public void notify(String message) {
        for(IObserver observer: list){
            observer.update(message);
        }
    }
}

模式分析

抛去模式中的接口类,就剩下了主题和观察者,这个模式的发生就是基于主题的变更与对观察者的通知。

这个模式在我的 helper 工具包中也有使用,就是基于对系统服务的监听,发现变化后,对订阅此变化的观察者们发出通知,并由观察者自己作出相应的动作。

缺点:

  1. 一个主题存在多个观察者,而通知的方式是通过轮询,这样的通知会有一定的时间消耗。
  2. 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。

代理模式

写法

静态代理

代码语言:javascript
复制
/**
 * 抽象主题类
 * 中心主题,买东西。
 */
public interface IShop {
    void buy();
}

/**
 * 真实主题类
 * 也就是我们购买者
 */
public class Person implements IShop {
    @Override
    public void buy() {
        System.out.println("购买");
    }
}

/**
 * 代理类
 * 持有被代理者
 */
public class StaticPurchase implements IShop {
    private IShop shop;

    WhoBuy(IShop shop){
        this.shop = shop;
    }
    @Override
    public void buy() {
        shop.buy();
    }
}

动态代理

代码语言:javascript
复制
/**
 * 动态代理类
 */
public class DynamicPurchase implements InvocationHandler {
    private Object object;
    DynamicPurchase(Object object){
        this.object = object;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(object, args);
        if(method.getName().equals("buy")){
            System.out.println("买上");
        }
        return result;
    }
}

/**
 * 客户端类
 */
public class User {
    public static void main(String[] args) {
        Shop person = new Person();
        DynamicPurchase dynamicPurchase = new DynamicPurchase(person);
        ClassLoader loader = person.getClass().getClassLoader();
        Shop purchase = (Shop) Proxy.newProxyInstance(loader, new Class[]{Shop.class}, dynamicPurchase);
        purchase.buy();
    }
}

模式分析

你可以看到上述文中和之前出现了不一样的地方,就是分为了两份代码,也就是动态代理和静态代理。那么就出现了这样的一个问题,何为静,何为动?

我们拿身边的代购举例,分为两种:

  1. 静就相当于我们需要找到专门的代理商,就是你微信里加的好友,你专门找到了他,然后跟他说想买Dior745,这样等他出国时就会帮你去买了。
  2. 动就是你只需要知道自己想要买Dior745,但是你这个时候手头没有静中的代理商,那你就找了淘宝,你只用知道自己想要的,不用再去思考代理商的问题了。也就是我们上文中所使用到的Proxy的代理技术。

缺点:

  1. 由于在客户端和真实主题之间增加了代理对象,因此 有些类型的代理模式可能会造成请求的处理速度变慢。
  2. 实现代理模式需要额外的工作,有些代理模式的实现 非常复杂。

适配器模式

类适配器:

对象适配器:

写法

类适配器

代码语言:javascript
复制
public interface MP4{
    void play();
}

public class MP4Player implements MP4{
    public void play(){
        // doSomething
    }
}

public interface Player{
      void action();
}

public class Adapter extends MP4Player implements Player{
    public void action(){
        play();
    }
}

对象适配器

代码语言:javascript
复制
public class PlayerAdapter implements Player{
    public MP4 mp4;
    
    public PlayerAdapter (MP4 mp4){
        this.mp4 = mp4;
    }     

    public void action(){
        if(mp4!= null){
             mp4.play();
        }
    }
}

模式分析

适配器模式简单来说就是为两个互不兼容的两者提供了合作的桥梁。

想一想我们在Android中使用的RecyclerView中为了进行数据的适配是不是都会加上一个Adapter,因为我们从网络获取的数据Bean是无法直接和XML文件中的每个View需要填充的数据项进行对应的。而适配器就是将两者进行了沟通协作。

类适配器模式和对象适配器模式的区别是什么?

类适配器使用了继承的方式来完成、对象适配器使用了依赖的关系来完成任务。拿代码来说的话就是类适配器继承了MP4Player,而对象适配器依赖就是MP4这个接口类的使用。

缺点

  • 类适配器模式对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
  • 对象适配器模式与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

策略模式

写法

代码语言:javascript
复制
/**
 * 上下文
 * 根据传入策略给出解决方法
 */
public class Context {
    private Strategy strategy;
    Context(Strategy strategy){
        this.strategy = strategy;
    }

    void solve(){
        strategy.solve();
    }
}

/**
 * 两种策略方法
 * 1. 普通用户
 * 2. Vip用户
 */
public class CustomStrategy implements Strategy {
    @Override
    public void solve() {
        System.out.println("普通用户");
    }
}

public class VipCustomStrategy implements Strategy {
    @Override
    public void solve() {
        System.out.println("Vip用户");
    }
}

/**
 * 抽象策略角色
 */
public interface Strategy {
    void solve();
}

模式分析

在掘金平台上看到最多的模式,没有之一。那我们是如何通过策略模式来干掉我们日常开发中的if-else的呢?

就拿上面的代码来说好了,如果使用if-else来完成任务。

代码语言:javascript
复制
if (level == "普通用户") {
    // ...
}else if (level == "Vip用户") {
    // ...
}

显然的很麻烦,而且会随着逻辑的复杂化而繁琐起来,就比如我今天Vip用户要分等级了,白银应该9折,黄金8折。。。。还ok,在Vip用户里继续加入if-else,第二天产品经理说要白银有一、二、三段了。你是不是想砍死你的产品经理了??

而策略模式去完成的时候只是多了一个或者多个类,虽然类增多了,但是从至少让我们的代码从屎山变成了一堆堆屎。

缺点:

  1. 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  2. 策略模式将造成产生很多策略类。

参考文献

  • 看懂UML类图和时序图
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DevGW 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 六大原则
    • 单一职责原则
      • 开放封闭原则
        • 里氏替换原则
          • 依赖倒置原则
            • 迪米特原则
              • 接口隔离原则
              • 几个常用的设计模式
                • 单例模式
                  • 四种写法及其优缺点
                  • 模式分析
                • 工厂模式
                  • 简单工厂模式
                  • 工厂方法模式
                  • 抽象工厂模式
                • 观察者模式
                  • 写法
                  • 模式分析
                • 代理模式
                  • 写法
                  • 模式分析
                • 适配器模式
                  • 写法
                  • 模式分析
                • 策略模式
                  • 写法
                  • 模式分析
              • 参考文献
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档