Loading...
墨滴

楼仔

2021/12/13  阅读:86  主题:默认主题

设计模式,看了就忘,该怎么学

还在抱着书,或者对着教程啃设计模式么,这样你永远都不会用,本文告诉你方法。

最开始学习设计模式,是读研期间,买了本最经典的《设计模式:可复用面向对象软件的基础》,开始风风火火搞起来,第一遍没看懂,又看了一遍,23个设计模式,前后反复看了3遍,还做了笔记,后来。。。嗯嗯。。。好像全忘了

等到要去面试的时候,再翻一下,找几个熟悉的,当时又热乎了一阵,等下一次再跳槽的时候,我的设计模式呢?得再翻一下,只记得个工厂和单例,其它的都忘光了。

所以前几年学习设计模式,给我的感觉就是,看了忘,忘了看,看了继续忘。如果一直这样,那学习设计模式的意义在哪?问题又出在哪个地方呢?

其实问题的点,在于我们一次性看太多,还没有实操,那怎么学呢?其实很简单,只需要掌握几个重点的设计模式,然后到项目中去实操,或者把之前写的代码用设计模式重构一下,就能妥妥掌握。

所以设计模式不在于多,在于精,平时常用的就那么几个,你把23个都看完,浪费时间!有这时间,陪陪女票,打打游戏,不香么?那哪些是必须掌握的呢,我都给你列出来,省得你再去找,看下面的思维导图:

温馨提示:设计模式虽好,但不要迷恋,借鉴思想,好用最重要!

模板模式&策略模式

有个记者去南极采访一群企鹅,他问第一只企鹅:“你每天都干什么?”

企鹅说:“吃饭,睡觉,打豆豆!”

接着又问第2只企鹅,那只企鹅还是说:“吃饭,睡觉,打豆豆!”

记者带着困惑问其他的企鹅,答案都一样,就这样一直问了99只企鹅。

当走到第100只小企鹅旁边时,记者走过去问它:每天都做些什么啊?

那只小企鹅回答:"吃饭,睡觉."

记者惊奇的又问:"你怎么不打豆豆?"

小企鹅撇着嘴巴,瞪了记者一眼说:"我就是豆豆!"

最Low方式

假如现在有3只企鹅,都喜欢“吃饭,睡觉,打豆豆”:

public class littlePenguin {
    public void everyDay() {
        System.out.println("吃饭");
        System.out.println("睡觉");
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin {
    public void everyDay() {
        System.out.println("吃饭");
        System.out.println("睡觉");
        System.out.println("用圆圆的肚子打豆豆");
    }
}
public class bigPenguin {
    public void everyDay() {
        System.out.println("吃饭");
        System.out.println("睡觉");
        System.out.println("拿鸡毛掸子打豆豆");
    }
}
public class test {
    public static void main(String[] args) {
        System.out.println("littlePenguin:");
        littlePenguin penguin_1 = new littlePenguin();
        penguin_1.everyDay();
        
        System.out.println("middlePenguin:");
        middlePenguin penguin_2 = new middlePenguin();
        penguin_2.everyDay();
        
        System.out.println("bigPenguin:");
        bigPenguin penguin_3 = new bigPenguin();
        penguin_3.everyDay();
    }
}

看一下执行结果:

littlePenguin:
吃饭
睡觉
用小翅膀打豆豆
middlePenguin:
吃饭
睡觉
用圆圆的肚子打豆豆
bigPenguin:
吃饭
睡觉
拿鸡毛掸子打豆豆

这种方式是大家写代码时,最容易使用的方式,上手简单,也容易理解,目前看项目中陈旧的代码,经常能找到它们的影子,下面我们看怎么一步步将其进行重构。

常规方式

“吃饭,睡觉,打豆豆”其实都是独立的行为,为了不相互影响(比如吃饭时突然睡着了,或者睡觉时不好好睡,居然急着跑去打豆豆,开个玩笑哈~~),我们可以通过函数简单进行封装:

public class littlePenguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public void beating() {
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public void beating() {
        System.out.println("用圆圆的肚子打豆豆");
    }
}
// bigPenguin相同,省略...
public class test {
    public static void main(String[] args) {
        System.out.println("littlePenguin:");
        littlePenguin penguin_1 = new littlePenguin();
        penguin_1.eating();
        penguin_1.sleeping();
        penguin_1.beating();
        // 下同,省略...
    }
}

这样看起来,是不是要稍微清晰一些呢,工作过一段时间的同学,可能会采用这种实现方式,我们有没有更优雅的实现方式呢?

模板模式

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

这3只企鹅,由于每天吃的都一样,睡觉也都是站着睡,但是打豆豆的方式却不同,所以我们可以将“吃饭,睡觉,打豆豆”抽象出来,因为“吃饭,睡觉”都一样,所以我们可以直接实现出来,但是他们“打豆豆”的方式不同,所以封装成抽象方法,需要每个企鹅单独去实现“打豆豆”的方式。最后再新增一个方法everyDay(),固定每天的执行流程:

public abstract class penguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public abstract void beating();
    public void everyDay() {
        this.eating();
        this.sleeping();
        this.beating();
    }
}

每只企鹅单独实现自己“打豆豆”的方式:

public class littlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用圆圆的肚子打豆豆");
    }
}
public class bigPenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("拿鸡毛掸子打豆豆");
    }
}

最后看调用方式:

public class test {
    public static void main(String[] args) {
        System.out.println("littlePenguin:");
        littlePenguin penguin1 = new littlePenguin();
        penguin1.everyDay();
        System.out.println("middlePenguin:");
        middlePenguin penguin2 = new middlePenguin();
        penguin2.everyDay();
        System.out.println("bigPenguin:");
        bigPenguin penguin3 = new bigPenguin();
        penguin3.everyDay();
    }
}

“楼哥,你这代码看的费劲,能给我画一个UML图么”,“嗯,其实画图挺麻烦的,谁让楼哥是暖男呢,那我就学着给大家画一个”

策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。

我们还是先抽象出3个企鹅的行为:

public abstract class penguin {
    public void eating() {
        System.out.println("吃饭");
    }
    public void sleeping() {
        System.out.println("睡觉");
    }
    public abstract void beating();
}

每只企鹅单独实现自己“打豆豆”的方式:

public class littlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用圆圆的肚子打豆豆");
    }
}
public class bigPenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("拿鸡毛掸子打豆豆");
    }
}

这里就是策略模式的重点,我们再看一下策略模式的定义“我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的context对象”,那么该contex对象如下:

public class behaviorContext {
    private penguin _penguin;

    public behaviorContext(penguin newPenguin) {
        _penguin = newPenguin;
    }
    public void setPenguin(penguin newPenguin) {
        _penguin = newPenguin;
    }
    public void everyDay() {
        _penguin.eating();
        _penguin.sleeping();
        _penguin.beating();
    }
}

最后看调用方式:

public class test {
    public static void main(String[] args) {
        behaviorContext behavior = new behaviorContext(new littlePenguin());
        behavior.everyDay();

        behavior.setPenguin(new middlePenguin());
        behavior.everyDay();

        behavior.setPenguin(new bigPenguin());
        behavior.everyDay();
    }
}

我们可以通过给behaviorContext传递不同的对象,然后来约定everyDay()的调用方式。其实我这个示例,有点把策略模式讲复杂了,因为纯粹的策略模式,3个企鹅只有beating()方法不同,所以可以把beating()理解为不同的算法即可,之所以引入everyDay(),是因为实际的项目场景中,会经常这么使用,也就是把这个变化的算法beating(),包装到具体的执行流程里面,所以策略模式就看起来没有那么直观,但是核心思想是一样的。

再强调一下,下面的UML图是使用了策略模式,因为我想结合具体的业务场景去讲,如果大家想去看最简版的策略模式,那就没有everyDay的封装,只有对beating执行的策略变更,这个可以看一下菜鸟教程。

模板模式 vs 策略模式

我在选择模板模式和策略模式时,发现两者都可以完全满足我的需求,然后我到网上查阅了很多资料,希望能找到两种模式在技术选择时,能确定告诉我哪些情况需要选择哪种模式,说来惭愧,到现在我都没有找到,因为网上只告诉我两种实现姿势的区别,但是没有说明如何具体选型,下面我就把我收集的资料,觉得比较核心的部分列出来,给大家一些参考。

有人请向我解释模板方法模式和策略模式之间有什么区别?

据我可以告诉他们是99%相同 – 唯一的区别是模板方法模式具有抽象类作为基类,而战略类使用由每个具体战略类实现的接口。

然而,就客户而言,他们的消费方式完全一样 – 这是正确的吗?

两者的主要区别在于具体algorithm的select。

使用Template方法模式时,通过子类化模板在编译时发生。 每个子类通过实现模板的抽象方法提供了一个不同的具体algorithm。 当客户端调用模板的外部接口的方法时,模板根据需要调用其抽象方法(其内部接口)来调用algorithm。

相比之下, 策略模式允许在运行时通过遏制来selectalgorithm。 具体algorithm是通过单独的类或函数实现的,这些类或函数作为parameter passing给构造函数或构造方法。 为此参数select哪种algorithm会根据程序的状态或inputdynamic变化。

综上所述:

  • 模板方法模式:通过子类化 编译时间algorithmselect
  • 策略模式:通过遏制 运行时algorithmselect

上面是完全摘抄网上的区别说明,只看到实现姿势的区别,但是如果通过这个就能指导我去选型,我觉得还不够,下面这个可能会讲的更具体一点:

相似:

  • 策略和模板方法模式都可以用来满足开闭原则,使得软件模块在不改变代码的情况下易于扩展。

  • 两种模式都表示通用function与该function的详细实现的分离。不过,它们所提供的粒度有一些差异。

差异:

  • 在策略中,客户和策略之间的耦合更加松散,而在模板方法中,两个模块耦合得更紧密。

  • 在策略中,虽然抽象类也可以根据具体情况而使用,但大多使用一个接口,而不使用具体类,而在Template方法中大多使用抽象类或具体类,不使用接口。

  • 在Strategy模式中,类的整体行为一般用接口表示,另一方面,Template方法用于减less代码重复,样板代码在基本框架或抽象类中定义。 在Template Method中,甚至可以有一个具有默认实现的具体类。

  • 简而言之,您可以在策略模式中更改整个策略(algorithm),但是在Template模式中,只有一些事情发生变化(algorithm的一部分),而其余事件保持不变。 在Template Method中,不变步骤是在一个抽象基类中实现的,而变体步骤要么是默认的实现,要么根本就没有实现。 在Template方法中,组件devise器强制执行algorithm所需的步骤和步骤的sorting,但允许组件客户端扩展或replace某些步骤。

看到上面的总结,感觉还是没有解答我的疑问,最后再引用一段网上的区别解读:

模板模式:

  • 它基于inheritance。

  • 定义不能被子类改变的algorithm的骨架。只有某些操作可以在子类中重写。

  • 父类完全控制algorithm ,仅将具体的步骤与具体的类进行区分。

  • 绑定是在编译时完成的。

策略模式:

  • 它基于授权/组成。

  • 它通过修改方法的行为来改变对象的内容。

  • 它用于在algorithm族之间切换。

  • 它在运行时通过在运行时用其他algorithm完全replace一个algorithm来改变对象的行为。

  • 绑定在运行时完成。

对于有强迫症的我,没有找到问题的根源,总感觉哪里不对劲,我就说一下我对于两者区别的理解吧。说实话,两种设计模式,我也就看到在实现姿势上有所区别,至于说的策略模式要定义统一接口,模板模式不这样做等,我不太赞同,因为我有时也会给模板模式定义一个通用接口。然后也有人说,策略模式需要定义一堆对象,模板模式就不需要,如果有10个不同的企鹅,模板模式不也是需要定义10个不同的企鹅类,然后再专门针对特定的方法去实现么?

所以说,这两种设计模式,我感觉还没有到非此即彼的划分,我就是怎么爽就怎么用,比如我不需要固定的执行流程,比如只去打豆豆,只需要对一个方法做具体抽象,我愿意选择策略模式,因为这个我感觉会让我需要使用的对象,更清晰一些。如果我有固定的执行流程,比如“吃饭、睡觉、打豆豆”,我更愿意使用模板方法,可能是代码看多了,也看习惯了,更愿意用模板方法去规范代码固定的执行流程。

当然,我也可以将两者结合起来使用,比如我们可以用模板方法,去实现这3只企鹅,但是对于middlePenguin,可能有分为企鹅少年A、企鹅少年B、企鹅少年C,他们都喜欢隔壁的企鹅妹妹,但是喜欢的方式不同,有暗恋的,有直接表白的,还有霸道总裁的,我可以用策略模式,去指定他们对企鹅妹妹的表达方式。

哥就是这么任性,自己怎么用的爽,就怎么来~~

实际场景

任何模式,都需要结合实际的场景来讲,才能更清晰。这两个模式,可以在你之前做过的项目中,只要稍微留意一下,应该会发现它们其实是大量存在的,比如很多框架代码,里面有很多固定的执行流程,有些逻辑是可以采用默认处理的方式,有些逻辑需要下游自己去实现,然后有些逻辑还需要提前预留钩子,比如在执行process()流程时,可能需要进行preProcess()的操作,那么这个preProcess()就是你预留的钩子,下游可以实现,也可以不实现。

所以看完这篇文章,大家可以静下心来想想,自己之前做过的项目中,有哪些用到这两种模式,然后自己再结合具体的场景总结一下,我想你应该会对这两个模式,有更深入的理解。

工厂模式

前言

讲工厂模式,大家可能觉得会很Low,不就是搞个类,然后专门生成一个具体的对象嘛,这有什么难的。是的,工厂模式确实不难,但是问你一下,如果你的代码中有很多if...else,你知道怎么通过工厂模式,把这些if...else去掉么?“嗯,工厂模式我会,但是和去掉if...else好像没有关系吧?”

我举个例子,假如你遇到如下代码:

switch($taskInfo['type_id']) {
    //批量冻结订单
    case 1:
        $result = self::batchFrozen($row_key,1);
        break;
    //批量解冻订单
    case 2:
        $result = self::batchFrozen($row_key,0);
        break;
    //批量允许发货
    case 3:
        $result =self::batchReshipment($row_key);
        break;
    //批量取消发货
    case 4:
        $result = self::batchCancel($row_key);
        break;
    // 后面还有几十个case,省略...

既然你懂工厂模式,可以把if...else简单重构一下,那就开始你的表演吧。“什么?不会?!你刚才还是自己是会工厂模式,怎么突然就怂了呢?”,既然不会,那就静下心来,虚心学习一下。

工厂模式

工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

先直接上图,后面的示例主要通过该图展开:

其实设计模式一般不会单一使用,通常会和其它模式结合起来使用,这里我们就将上一篇文章讲到的模板模式和工厂模式结合起来。因为工厂模式,通常会给这些新创建的对象制定一个公共的接口,我们可以通过抽象类定义:

public abstract class penguin {
    public void eating() {
        System.out.println("吃饭");
    }

    public void sleeping() {
        System.out.println("睡觉");
    }

    public abstract void beating();

    public void everyDay() {
        this.eating();
        this.sleeping();
        this.beating();
    }
}

因为我们是结合了模板模式,所以这个抽象类中,可以看到模板模式的影子,如果你只关注抽象的接口,比如beating,那么这个就是一个抽象方法,也可以理解为下游需要实现的方法,其它的接口其实可以忽略。再看看每个企鹅具体的实现:

public class littlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用小翅膀打豆豆");
    }
}
public class middlePenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("用圆圆的肚子打豆豆");
    }
}
public class bigPenguin extends penguin {
    @Override
    public void beating() {
        System.out.println("拿鸡毛掸子打豆豆");
    }
}

这里就是工厂方法的重点了,需要构建一个工厂,专门用来拿企鹅:

public class penguinFactory {
    private static final Map<String, penguin> map = new HashMap<>();
    static {
        map.put("littlePenguin"new littlePenguin());
        map.put("middlePenguin"new middlePenguin());
        map.put("bigPenguin"new bigPenguin());
    }
    // 获取企鹅
    public static penguin getPenguin(String name) {
        return map.get(name);
    }
}

上面的逻辑很简单,就是通过一个map对象,放入所有的企鹅,这个工厂就可以通过企鹅的名字,拿到对应的企鹅对象,最后我们看使用方式:

public class test {
    public static void main(String[] args) {
        penguin penguin_1 = penguinFactory.getPenguin("littlePenguin");
        penguin_1.everyDay();
        penguin penguin_2 = penguinFactory.getPenguin("middlePenguin");
        penguin_2.everyDay();
        penguin penguin_3 = penguinFactory.getPenguin("bigPenguin");
        penguin_3.everyDay();
    }
}

输出如下:

吃饭
睡觉
用小翅膀打豆豆
吃饭
睡觉
用圆圆的肚子打豆豆
吃饭
睡觉
拿鸡毛掸子打豆豆

看到这里,大家应该知道怎么去使用工厂模式了吧,这里我是通过“工厂模式 + 模板模式”来讲的这个示例,这样刚好可以和我上一篇文章融会贯通。

“楼哥,你这个例子我看懂了,但是你最开始抛的那个问题,能给出答案么?”“不会吧,这个示例其实已经很清楚了,那我再讲述一下吧,谁让楼哥是暖男呢。”

问题解答

文章开头的这个示例,其实也是我最近需要重构项目中的一段代码,我就是用“工厂模式 + 模板模式”来重构的,我首先会对每个方法中的内容通过模板模式进行抽象(因为本章主要讲工厂模式,模板模式的代码,我就不贴了),然后通过工厂模式获取不同的对象,直接看重构后的代码(目前还是DEMO版):

public class TaskFactory {
    @Autowired
    public static List<AbstractTask> taskList;
    private static final Map<String, AbstractTask> map = new HashMap<>();
    static {
        // 存放任务映射关系
        map.put(AbstractTask.OPERATOR_TYPE_FROZEN, new BatchFrozenTask());
        map.put(AbstractTask.OPERATOR_TYPE_REJECT, new BatchRejectTask());
        map.put(AbstractTask.OPERATOR_TYPE_CANCEL, new BatchCancelTask());
    }

    public static void main(String[] args) {
        String operatorType = AbstractTask.OPERATOR_TYPE_REJECT;
        AbstractTask task = TaskFactory.map.get(operatorType);
        ParamWrapper<CancelParams> params = new ParamWrapper<CancelParams>();
        params.rowKey = 11111111;
        params.data = new CancelParams();
        OcApiServerResponse res =  task.execute(params);
        System.out.println(res.toString());
        return;
    }
}

实际场景

这个场景就太多了,刚才给大家讲解的是去掉if...else的场景,然后在小米商城的支付系统中,因为海外有几十种支付方式,也是通过这种方式去掉if...else的,不过支付类的封装不是用的模板方法,用的的策略模式,虽然感觉两者差不多。

如果你直接new一个对象就能解决的问题,就用不到工厂模式了。

builder模式

【设计模式系列3】builder模式

组合模式

【设计模式系列4】组合模式

单例模式

【设计模式系列5】单例模式

代理模式

【设计模式系列6】代理模式

装饰器模式

【设计模式系列7】装饰器模式

门面模式

【设计模式系列8】门面模式

责任链模式

【设计模式系列9】责任链模式

尽信书则不如无书,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激,个人联系方式:

  • 微信公众号:楼仔进阶之路
  • QQ:楼仔/514202273
  • 掘金:https://juejin.cn/user/3518877442254759

楼仔

2021/12/13  阅读:86  主题:默认主题

作者介绍

楼仔