设计模式初探

什么是设计模式?

Q1、设计模式的产生背景?

其实,”设计模式“这个术语最早并非出现在软件设计中,而是被应用于建筑领域的设计中。

早在1977 年的时候,美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫·亚历山大(Christopher Alexander)在他的著作《建筑模式语言:城镇、建筑、构造(A Pattern Language: Towns Building Construction)中就提出了253种建筑领域设计的基本模式。

直到1987年,肯特·贝克(Kent Beck)和沃德·坎宁安(Ward Cunningham)率先将克里斯托夫·亚历山大的这些模式思想应用在 Smalltalk 中的图形用户接口的生成中,遗憾的是并没有引起软件界的关注。(Smalltalk:被公认为历史上第二个面向对象的程序设计语言,有"面向对象编程之母"的称号。)这里的对象和接口就好比建筑领域的墙壁和门窗。

3年后即1990年,软件界才开始研讨关于设计模式的话题,相关会议也是层出不穷的召开。

1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,在本教程中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4 位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)匿名著称。

GoF(Gang of Four,四人组)所提出的设计模式主要基于以下的面向对象的设计原则:

对接口编程而不是对实现编程 优先使用对象组合而不是继承

直到今天,狭义的设计模式还是指GoF(Gang of Four,四人组)的这23种经典设计模式。

Q2、设计模式(Design Pattern)是如何被定义的?

软件设计模式(Software Design Pattern),又称设计模式。是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。概括来讲,它是解决特定问题的一系列套路,具有程语言无关性。

这世上本没有什么设计模式,用的人多了,也便成了设计模式。

学习设计模式的目的就是提高代码的可重用性、可读性、可靠性、灵活性及可维护性。自然也可 以提高程序猿的思维能力、编程能力及设计能力。同时,学习并掌握设计模式是中级工程师进阶高开或者架构师的必经之路。

设计模式的基本要素?

总的来说,设计模式包含以下几个基本要素:模式名称、别名、动机、问题、解决方案、效果、结构、模式角色、合作关系、实现方法、适用性、已知应用、例程、模式拓展和相关模式等。

其实,设计模式的基本要素主要有以下四个,罗列如下:

设计模式提出之初就很明确地指定了设计模式的基本要素,但是实际多数软件开发者会重点关注第1和第3点要素(即重点关注设计模式及其实现)而忽略第2和第4点要素(即忽略设计模式的产生背景及设计目标),最终导致设计出的应用编码逻辑繁杂或得不到预期的效果。

设计模式的七大原则?

下面,我们用一张图清晰地记录下设计模式中的几大原则(部分文献中记录的为六大原则,就是没有合成复用原则):

Q1、开闭原则?

开闭原则是设计模式中的总原则,开闭原则就是说:对拓展开放、对修改关闭。模块应该在尽量不修改代码的前提下进行拓展,这就需要使用接口和抽象类来实现预期效果。

举个栗子,我们以书店销售书籍为例,类图如下: IBook定义了数据(书籍)的三个属性,分别是:名称、价格和作者。小说类NovelBook是一个具体的实现类,IBook接口代码如下:

public interface IBook {
    public String getName();

    public int getPrice();

    public String getAuthor();

}

小说类NovelBook实现了IBook接口,NovelBook类具体代码如下:

public class NovelBook implements IBook{
    // 书籍名称
    private String name;
    // 书籍价格
    private int price;
    // 书籍作者
    private String author;

    public NovelBook(String _name, int _price, String _author) {
        this.name = _name;
        this.price = _price;
        this.author = _author;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

}

然后我们打印下书籍价格等信息:

public class Main {

    public static void main(String[] args) {
        IBook novel = new NovelBook("笑傲江湖", 45, "金庸");
        System.out.println("书籍名字:" + novel.getName() + "\n书籍作者:" + novel.getAuthor() + "\n书籍价格:" + novel.getPrice());
    }

}

具体输出如下:

书籍名字:笑傲江湖 书籍作者:金庸 书籍价格:45

那么问题来了,倘若某一天市场环境不好,书籍需要打折销售的话(比如40元以上的书籍按9折销售,其它的8折销售),以上代码如何应对这种需求变化呢?一般来说,有如下三种方法来解决这个问题:

  • 修改接口,在IBook上新增一个getOffPrice()方法,专门用来获取折扣书价,所有的实现类包括NovelBook实现该方法。导致的问题,一是实现类要修改,二是main函数中的方法逻辑也要做修改,三就是IBook作为接口应该是稳定且可靠的,不应该频繁改动。这违反了开放原则,所以该方案,否定。
  • 修改实现类,即修改NovelBook的getPrice()中的处理逻辑,但是引来的问题是:假如某一天采购书籍的人员要查询书价的话,那么他看到的也是折扣后的价钱,显然是不合理的。因此,该方案也不是最优解决。
  • 通过拓展子类来实现变化,新建一个子类OffNovelBook,覆写getPrice()方法,通过拓展完成新增加的业务。

OffNovelBook类的代码示例如下:

public class OffNovelBook extends NovelBook{
    public OffNovelBook(String _name, int _price, String _author) {
        super(_name, _price, _author);
    }

    @Override
    public int getPrice() {
        // 原价
        int selfPrice = super.getPrice();
        // 折扣价
        int offPrice = 0;
        if (selfPrice >= 40) {
            offPrice = selfPrice * 90 / 100;
        } else {
            offPrice = selfPrice * 80 / 100;
        }
        return offPrice;
    }

}

同时,main中调用相应调整如下:

public class Main {

    public static void main(String[] args) {
        IBook novel = new NovelBook("笑傲江湖", 45, "金庸");
        System.out.println("书籍名字:" + novel.getName() + "\n书籍作者:" + novel.getAuthor() + "\n书籍价格:" + novel.getPrice());
        System.out.println("------------------------");
        IBook novel2 = new OffNovelBook("笑傲江湖", 45, "金庸");
        System.out.println("书籍名字:" + novel2.getName() + "\n书籍作者:" + novel2.getAuthor() + "\n书籍折扣后价格:" + novel2.getPrice());
    }

}

输出的书籍价格信息如下:

书籍名字:笑傲江湖 书籍作者:金庸 书籍价格:45 ------------------------ 书籍名字:笑傲江湖 书籍作者:金庸 书籍折扣后价格:40

Q2、单一职责原则?

何为单一指责原则,指的是一个类或者模块有且只有一个改变的原因。如果模块或类承担的指责过多,就等于这些指责耦合在一起,这样一个模块的变快可能会削弱或抑制其它模块的能力,这样的耦合是十分脆弱地。所以应该尽量保持单一职责原则,此原则的核心就是解耦和增强内聚性。

举个栗子,如下需求涉及到用户的相关模块操作,所以定义了IUserInfo接口,如下: 该接口设计有一个很糟糕的地方,就是将用户信息(Business Object,业务对象BO)和用户行为(Business Logic,业务逻辑Biz)的修改混为一谈了,根据单一职责原则,应当将指责解耦,处理过的接口设计如下: 这里解释下设计的目的,首先将一个接口拆分成两个接口,IUserBO负责用户属性信息,IUserBIz负责用户行为信息维护,根据面向接口编程的思想,所以产生了UserInfo对象之后,即可以将其当作IUserBO接口使用,又可以当做IUserBiz接口使用。当我们需要修改用户信息时调用IUserBO实现类,需要行为信息操作时当作IUserBiz实现类即可。

IUserInfo userInfo = new UserInfo();
// 操作BO时
IUserBO userBO = (IUserBO) userInfo;
userBO.setPassword("123456");
// 行为操作时
IUserBiz userBiz = (IUserBiz) userInfo;
userBiz.deleteUser();

单一职责原则,再举个栗子就是万能类:即在一个类(或方法)中完全可以实现系统所有功能。但是这样做往往模块严重耦合,牵一发而动全身。所以需要职责分离,MVC架构中该原则体现的就比较明显。

在现在流行的微服务架构体系中,最头疼的就是服务拆分,拆分的粒度也很有讲究,标准的应该是遵从单一原则,避免服务拆分时发生各种撕逼行为:”本应该在A服务中的被安排在了B服务中“,所以服务的职责划分尤为重要。

再有就是,做service层开发时,早期的开发人员会将数据库操作放在service中,比如getConnection,然后执行prepareStatement,再就是service逻辑处理等等。可是后来发现数据库要由原来的mysql变更为oracle,service层代码岂不是需要重写一遍,天了噜...直接崩溃跑路。

”我单纯,所以我快乐“用来形容单一指责原则再恰当不过了。

Q3、里氏替换原则?

里氏替换原则的解释是,所有引用基类的地方必须能透明地使用其子类的对象。通俗来讲的话,就是说,只要父类能出现的地方子类就可以出现,并且使用子类替换掉父类的话,不会产生任何异常或错误,使用者可能根本就不需要知道是父类还是子类。反过来就不行了,有子类的地方不一定能使用父类替换。

比如某个方法接受一个Map型参数,那么它一定可以接受HashMap、LinkedHashMap等参数,但是反过来的话,一个接受HashMap的方法不一定能接受所有Map类型参数。

里氏替换原则是开闭原则的实现基础,它告诉我们设计程序的时候尽可能使用基类进行对象的定义及引用,具体运行时再决定基类对应的具体子类型。

接下来举个栗子,我们定义一个抽象类AbstractAnimal对象,该对象声明内部方法”跳舞“,其中,Rabbit、Dog、Lion分别继承该对象,另外声明一个Person类,该类负责喂养各种动物,Client类负责逻辑调用,类图如下: 其中,Person类代码如下:

public class Person {
    private AbstractAnimal animal;

    public void feenAnimal(AbstractAnimal _animal) {
        this.animal = _animal;
    }

    public void walkAnimal(){
        System.out.println("人开始溜动物...");
        animal.dance();
    }

}

main函数调用的时候如下:

public class Main {

    public static void main(String[] args) {
        Person person = new Person();
        person.feenAnimal(new Rabbit());
        person.walkAnimal();
    }

}

打印输出:

人开始溜动物... 小白兔跳舞...

Q4、依赖倒置原则?

依赖倒置原则(Dependency Inversion Principle,DIP)的定义:程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。

依赖倒置原则,高层模块不应该依赖低层模块,都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。

举个栗子,拿顾客商店购物来说,定义顾客类如下,包含一个shopping方法:

public class Customer {
    public void shopping (YanTaShop shop) {
        System.out.println(shop.sell());
    }

}

以上表示顾客在"雁塔店"进行购物,假如再加入一个新的店铺"高新店",表示修改如下:

public class Customer {
    public void shopping (GaoXinShop shop) {
        System.out.println(shop.sell());
    }

}

这显然是设计不合理的,违背了开闭原则。同时,顾客类的设计和店铺类绑定了,违背了依赖倒置原则。解决办法很简单,将Shop抽象为具体接口,shopping入参使用接口形式,顾客类面向接口编程,如下:

public class Customer {
    public void shopping (Shop shop) {
        System.out.println(shop.sell());
    }
}

interface Shop{
    String sell();

}

类图关系如下:

Q5、接口隔离原则?

接口隔离原则(Interface Segregation Principle,ISP)的定义是客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。简单来说就是建立单一的接口,不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少,保持接口纯洁性。

我们所讲的接口主要分为两大类,一是实例接口,比如使用new关键字产生一种实例,被new的类就是实例类的接口。从这个角度出发的话,java中的类其实也是一种接口。二是类接口,java中常常使用interface关键字定义。

举个栗子来说,我们使用接口IPrettyGirl来描述美女,刚开始类图可能描述如下:

但是发现该接口中包含对美女的外观描述、内在美描述等,几乎将美女的所有特性全部纳入,这显然不是一个很好的设计规范,比如在唐朝,在那个以福为美的时代对美的理解就不同,就会出现单纯goodLooking过关就是美女的结果,所以这里我们需要将接口隔离拆分。将一个接口拓展为两个,增加系统灵活性及可维护性。

这里我们将美女接口拆分为内在美、外在美两个接口,系统灵活性提高了,另外接口间还能使用继承实现聚合,系统拓展性也得到了增强。

Q6、迪米特法则?

迪米特法则(Law of Demeter,LOD),有时候也叫做最少知识原则(Least Knowledge Principle,LKP),它的定义是:一个软件实体应当尽可能少地与其他实体发生相互作用。迪米特法则的初衷在于降低类之间的耦合。

举个栗子,拿教师点名来讲,体育老师需要清点班上学生人数,教师一般不是自己亲自去数,而是委托组长或班长等人去清点,即教师通过下达命令至班长要求清点人数:

public class Girl {

}

public class GroupLeader {

    private final List<Girl> girls;

    public GroupLeader(List<Girl> girls) {
        this.girls = girls;
    }

    public void countGirls() {
        System.out.println("The sum of girls is " + girls.size());
    }
}

public class Teacher {

    public void command(GroupLeader leader){
        leader.countGirls();
    }
}

public class Main {

    public static void main(String[] args) throws Exception {
        Teacher teacher = new Teacher();
        GroupLeader groupLeader = new GroupLeader(Arrays.asList(new Girl(), new Girl()));
        teacher.command(groupLeader);
    }

}

上述例子中,如果去掉GroupLeader这个中间人角色,教师就会直接去清点人数,这样做会违反迪米特法则。

Q7、合成/聚合复用原则?

合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。原则是尽量首先使用合成/聚合的方式,而不是使用继承。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券