在学习设计模式之前

前言

在正式介绍设计模式之前需要对如下内容了解:

  1. 面向对象编程基础(OOP),本文不详述
  2. 优先软件设计的特征、原则
  3. SOLID原则

本文着重回顾后两种。

1. 优秀设计的特征

总的来说,有两个特征:

  1. 方便复用
  2. 具有扩展性

其中,复用有三个层次:

  1. 底层复用类库与容器
  2. 中层是设计模式复用发挥作用的地方
  3. 最高层复用框架

2. 优秀设计的原则

  1. 封装变化的内容
  2. 面向接口开发,而不是面向实现
  3. 组合优于继承

2.1 封装变化的内容

本章节所有代码均改变自来源11.

修改前的方法:

def get_order_total(order):
    total = 0.0
    for item in order.item_lines:
        total += item.price * item.quantity
    
    # 美国营业税
    if order.country == 'US':
        total += total * 0.07
    # 欧盟营业税
    elif order.country == 'EU':
        total += total * 0.2
    
    return total

上面的示例中,如果营业税只计算美国和欧盟,代码还勉强可读。但如果全球190+国家和地区,如果日后需要再扩展,难道每次都要再加elif order.country == a_country一类的代码吗? 原始方法get_order_total并不关心它是哪里收的营业税,只关心最终总量。那么,获取营业税部分可以抽取到一个独立的方法中。下面是修改后的代码:

# 获取税率的方法单独抽出计算
def get_tax_rate(country):
    if country == 'US':
        return 0.07
    if  country == 'EU':
        return 0.2
    # 还扩展了中国的营业税,方便后续扩展
    if country == 'China':
        return 0.3

    return -0.01

# 修改后的方法
def get_order_total(order):
    total = 0.0
    for item in order.item_lines:
        total += item.price * item.quantity

    # 这样一来,方法是不是简洁了很多
    # get_order_total方法中,哪国的营业税不是主要关心项
    # 获取营业税额才是
    return total + total * get_tax_rate(order.country)

除了方法层面的封装外,类层面也可以实现类似的封装。例如,Order类随着订单数量的扩大,随着贸易越来越国际化,可能扩展的税金计算方法会很多,例如按州计算的美国税,按国家计算的欧盟税,按商品计算的中国税等等。这时,算税的方法就可以抽象到一个大类TaxCalculator中。UML图如下:

UML-1

2.2 面向接口而不是面向实现

可以分为如下几个步骤思考:

  1. 确定一个对象对另一对象的确切需求:它需执行哪些方法?
  2. 在一个新的接口或抽象类中描述这些方法
  3. 让被依赖的类实现该接口
  4. 现在让有需求的类依赖于这个接口, 而不依赖于具体的类

例如一个公司的架构,修改前:

uml-2

修改后:

uml-3

所有公司基于Company父类实现,所有员工实现Employee接口。

2.3 组合优于继承

继承问题清单:

  1. 子类不能减少父类的接口
  2. 重写方法时,需要确保新行为与旧行为兼容
  3. 继承可能打破超类的封装
  4. 子类可能与超类紧密耦合
  5. 可能会出现平行体系,使得代码非常难以维护

继承代表be的关系,而组合更像是have的关系。例如,汽车is交通工具,而汽车has发动机。汽车用这一原则可以抽象成如下图:

uml-4

3. SOLID原则

SOLID原则是面向对象编程和设计的五个基本原则,它们可以帮助我们理解设计模式和软件架构。这些原则是:

  1. 单一职责原则(Single Responsibility Principle, SRP):一个类应该只做一件事,一个类应该只有一个变化的原因。例如,如果一个类是一个数据容器,比如Book类或者Student类,只有当我们更改了数据定义时才能够修改这些字段。

  2. 开放封闭原则(Open Closed Principle, OCP):实体应该对扩展是开放的,对修改是封闭的。也就是说,我们应该能够在不修改现有代码的情况下,增加新的功能。

  3. 里氏替换原则(Liskov Substitution Principle, LSP):一个对象在其出现的任何地方,都可以用子类实例做替换,并且不会导致程序的错误。

  4. 接口隔离原则(Interface Segregation Principle, ISP):客户端不应该被强迫实现一些他们不会使用的接口。简单地说,就是使用多个专门的接口比使用单个接口要好很多。

  5. 依赖倒置原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对抽象(接口)编程,而不是针对实现细节编程。

3.1 单一职责原则 (SRP)

主要用于减少复杂度。核心思想是:一个类应该只负责一项职责。也就是说,一个类应该只有一个引起它变化的原因。换句话说,一个类要尽量留下只属于自己的部分,而可以和其他类共享的部分应该统统扔出去组成单独的类。

例如:

// 以上是日常业务代码
public interface IPhone {
    void dial(String phoneNumber);
    void chat(Object o);
    void hangup();
}

如果按照严格的SRP来修改,应该要这么改:

public interface IDataTransfer {
    void chat(Object o);
}

public interface IConnectionManager {
    void dial(String phoneNumber);
    void hangup();
}

// IPhone去实现两个接口的内容。因为两种行为都不是iPhone独有
public class IPhone implements IDataTransfer, IConnectionManager {
    //...
}

3.2 开放封闭原则 (OCP)

本原则的主要理念是在实现新功能时能保持已有代码不变。对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对已有代码进行任何修改。

通过代码示例如下:

// 图形接口, 可以扩展
interface Shape {
    double area();
}

// 矩形类,基于图形类
class Rectangle implements Shape {
    private double width;
    private double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

// 圆形类,基于图形类
class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * Math.pow(radius, 2);
    }
}

// 计算图形面积的类,对修改封闭
class AreaCalculator {
    public double calculate(Shape shape) {
        return shape.area();
    }
}

3.3 里氏替换原则 (LSP)

当你扩展一个类时, 记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。子类可以扩展父类的功能,但不能修改父类已有的功能, 子类必须保持与父类行为的兼容。因此,有如下几点要求:

  1. 子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象。例如,子类实现feed(投喂)方法时,喂动物好于喂猫,喂猫好于喂英短。
  2. 子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配,也就是说,子类应该返回一个更具体的类型。例如,子类实现birth(生娃)方法时,生猫比生只小动物好,生只串也比生只小猫好。
  3. 子类中的方法不应抛出基础方法预期之外的异常类型。
  4. 子类不应该加强其前置条件, 也不能削弱后置条件。换句话说,子类重写的时候,父类有一个int类型的参数,没有限制正负,子类也不能这么做;父类的逻辑子类尽量能不动就不动。
  5. 超类的常量必须保留。
  6. 子类不能修改超类中private值,尤其对于Python和js程序员来说很重要!

一句话,子类必须可以扩展超类的行为,并且不能覆盖超类的行为

示例:

// 四边形接口
interface Quadrangle {
    long getLength();
    long getWidth();
}

// 长方形类
class Rectangle implements Quadrangle {
    private long length;
    private long width;

    @Override
    public long getLength() {
        return this.length;
    }

    @Override
    public long getWidth() {
        return this.width;
    }

    public void setLength(long length) {
        this.length = length;
    }

    public void setWidth(long width) {
        this.width = width;
    }
}

// 正方形类
class Square implements Quadrangle {
    private long sideLength;

    @Override
    public long getLength() {
        return this.sideLength;
    }

    @Override
    public long getWidth() {
        return this.sideLength;
    }

    public long getSideLength() {
        return sideLength;
    }

    public void setSideLength(long sideLength) {
        this.sideLength = sideLength;
    }
}

假设我们有一个计算图形面积的程序,最初只需要处理矩形。后来,需求变更,需要处理正方形。按照里氏替换原则,我们应该在不修改原有“矩形”代码的情况下,添加新的“正方形”代码,以满足新的需求。在上述例子中,Rectangle类和Square类都实现了Quadrangle接口,这样就可以在不修改原有代码的情况下添加新的代码,从而遵守了里氏替换原则。

3.4 接口隔离原则 (ISP)

程序设计应该建立单一接口,而不要建立臃肿的大接口。这个原则的目的是降低类之间的耦合度,使类具有高内聚性和低耦合性,提高软件的可维护性和可扩展性。你必须将“臃肿”的方法拆分为多个颗粒度更小的具体方法。换句话说,代码拆的细一些。

UML

代码实例:

// 动物接口
interface Animal {
    void eat();
    void sleep();
}

// 狗类
class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating!");
    }

    @Override
    public void sleep() {
        System.out.println("Dog is sleeping!");
    }

    public void bark() {
        System.out.println("Dog is barking!");
    }
}

// 鸟类
class Bird implements Animal {
    @Override
    public void eat() {
        System.out.println("Bird is eating!");
    }

    @Override
    public void sleep() {
        System.out.println("Bird is sleeping!");
    }

    public void fly() {
        System.out.println("Bird is flying!");
    }
}

3.5 依赖倒置原则

高层次的类不应该依赖于低层次的类。 两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。反着说,就是抽象类中不要有具体实现,抽象类和interface仅作签名用,具体实现应该implement抽象类

示例:

// 课程接口
interface ICourse {
    void study();
}

// Java课程类
class JavaCourse implements ICourse {
    @Override
    public void study() {
        System.out.println("正在学习Java课程");
    }
}

// 设计模式课程类
class DesignPatternCourse implements ICourse {
    @Override
    public void study() {
        System.out.println("正在学习设计模式课程");
    }
}

// 学习者类
class Learner {
    public void study(ICourse course) {
        course.study();
    }
}

// 使用示例
public static void main(String[] args) {
    Learner learner = new Learner();
    learner.study(new JavaCourse());
    learner.study(new DesignPatternCourse());
}

Reference

  1. refactoring guru设计模式
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • 文章大纲 一、策略模式二、观察者模式三、工厂模式四、单例模式五、其他模式六、设计模式总结七、参考文章 一、策略模式...
    故事爱人c阅读 675评论 0 1
  • 设计模式分类 总体来说设计模式分为三大类:1、 创建型模式,共五种:工厂方法模式,抽象工厂模式,单例模式,建造者模...
    倔强青铜弟中弟阅读 307评论 0 0
  • 永不放弃的毅力,和对欲望的控制。目标:要能够理解相类似的设计模式之间的区别和不同。可以把类比列举出来,加深记忆。 ...
    卡斯特梅的雨伞阅读 579评论 0 1
  • 一个UML类图 类之间的关系 类的继承结构表现在UML中为:泛化(generalize)与实现(realize) ...
    僚机KK阅读 638评论 0 0
  • 编程是一门技术,更是一门艺术 面向对象:可维护 、可复用、可扩展、灵活性好 [TOC] 设计模式六大原则 单一职责...
    Snail127阅读 226评论 0 0