【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则

系列文章目录


文章目录

  • 系列文章目录
  • 前言
  • 一、优秀设计的特征
      • 1. 代码复用
      • 2. 扩展性
  • 二、设计原则
      • 1. 封装变化的内容
      • 2. 面向接口开发,而不是面向实现
      • 3. 组合优于继承
        • 1.继承
        • 2.组合
  • 三、SOLID原则
      • 1. 单一职责原则(Single Responsibility Principle)
      • 2. 开闭原则(Open/closed Principle)
      • 3. 里氏替换原则(Liskov Substitution Principle)
      • 4. 接口隔离原则(Interface Segregation Principle)
      • 5. 依赖倒置原则(Dependency Inversion Principle)
  • 总结


前言


在这里插入图片描述

一、优秀设计的特征

1. 代码复用

代码复用是减少开发成本时最常用的方式之一。其意图非常明显:与其反复从头开发,不如在新对象中重用已有代码。

2. 扩展性

变化是程序员生命中唯一不变的事情。 因此在设计程序架构时,所有有经验的开发者会尽量选择支持未来任何可能变更的方式。

二、设计原则

1. 封装变化的内容

找到程序中的变化内容并将其与不变的内容区分开,该原则的主要目的是将变更造成的影响最小化。

  • 封装包括:
  • 方法层面的封装:将复杂的逻辑抽取到一个单独的方法中,并对原始方法隐藏该逻辑。
  • 类层面的封装:将一些相关联的变量和方法抽取到一个新类中会让程序更加清晰和简洁。

2. 面向接口开发,而不是面向实现

面向接口进行开发,而不是面向实现;依赖于抽象类型,而不是具体类。

如果无需修改已有代码就能轻松对类进行扩展,那就可以说这样的设计是灵活的。

让我们再来看一个关于猫的例子,看看这个说法是否正确:一只可以吃任何食物的猫(Cat)要比只吃香肠的猫更加灵活。无论如何你都可给第一只猫喂香肠,因为香肠是“任何食物”的一个子集;当然,你也可以喂这只猫任何食物。

  • 1. 确定一个对象对另一对象的确切需求:它需执行哪些方法?
  • 2. 在一个新的接口或抽象类中描述这些方法。
  • 3. 让被依赖的类实现该接口。
  • 4. 现在让有需求的类依赖于这个接口, 而不依赖于具体的类。你仍可与原始类中的对象进行互动,但现在其连接将会灵活得多。

从下图可以看出,右侧(面向接口开发)的代码比左侧(面向实现开发)的代码更加灵活,但也更加复杂。
【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第1张图片

3. 组合优于继承

继承可能是类之间最明显、最简便的代码复用方式。如果你有两个代码相同的类, 就可以为它们创建一个通用的基类,然后将相似的代码移动到其中。

  • 不过,继承这件事通常只有在程序中已包含大量类,且修改任何东西都非常困难时才会引起关注。下面就是此类问题的清单:
  • 子类不能减少超类的接口。你必须实现父类中所有的抽象方法,即使它们没什么用。
  • 在重写方法时,你需要确保新行为与其基类中的版本兼容。这一点很重要,因为子类的所有对象都可能被传递给以超类对象为参数的任何代码,相信你不会希望这些代码崩溃的。
  • 继承打破了超类的封装。因为子类拥有访问父类内部详细内容的权限。此外还可能会有相反的情况出现,那就是程序员为了进一步扩展的方便而让超类知晓子类的内部详细内容。
  • 子类与超类紧密耦合。超类中的任何修改都可能会破坏子类的功能。
  • 通过继承复用代码可能导致平行继承体系的产生。继承通常仅发生在一个维度中。只要出现了两个以上的维度,你就必须创建数量巨大的类组合,从而使类层次结构膨胀到不可思议的程度。

组合是代替继承的一种方法。继承代表类之间的“是”关系(汽车是交通工具),而组合则代表“有”关系(汽车有一个引擎)。

必须一提的是,这个原则也能应用于聚合(一种更松弛的组合变体,一个对象可引用另一个对象,但并不管理其生命周期)。例如:一辆汽车上有司机,但是司机也可能会使用另一辆汽车,或者选择步行而不使用汽车。

1.继承

继承在多个维度上扩展一个类(汽车类型 × 引擎类型 × 驾驶类型),可能导致子类组合的数量爆炸。
【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第2张图片

2.组合

组合将不同“维度”的功能抽取到各自的类层次结构中。
【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第3张图片

三、SOLID原则

1. 单一职责原则(Single Responsibility Principle)

  • 修改一个类的原因只能有一个:
  • 尽量让每个类只负责软件中的一个功能,并将该功能完全封装(你也可称之为隐藏)在该类中。
  • 这条原则的主要目的是减少复杂度。你不需要费尽心机地去构思如何仅用 200 行代码来实现复杂设计,实际上完全可以使用十几个清晰的方法。
  • 当程序规模不断扩大、变更不断增加后,真实问题才会逐渐显现出来。到了某个时候,类会变得过于庞大,以至于你无法记住其细节。查找代码将变得非常缓慢,你必须浏览整个类,甚至整个程序才能找到需要的东西。程序中实体的数量会让你的大脑堆栈过载,你会感觉自己对代码失去了控制。
  • 还有一点:如果类负责的东西太多,那么当其中任何一件事发生改变时,你都必须对类进行修改。而在进行修改时,你就有可能改动类中自己并不希望改动的部分。
  • 如果你开始感觉在同时关注程序特定方面的内容时有些困难的话,请回忆单一职责原则并考虑现在是否应将某些类分割为几个部分。
  • 我们有几个理由来对 雇员 Employee 类进行修改。第一个理由与该类的主要工作(管理雇员数据)有关。但还有另一个理由:时间表报告的格式可能会随着时间而改变,从而使你需要对类中的代码进行修改。

【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第4张图片

  • 解决该问题的方法是将与打印时间表报告相关的行为移动到一个单独的类中。这个改变让你能将其他与报告相关的内容移动到一个新的类中。
    【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第5张图片

2. 开闭原则(Open/closed Principle)

  • 对于扩展,类应该是“开放”的;对于修改,类则应是“封闭”的:
  • 本原则的主要理念是在实现新功能时能保持已有代码不变。如果你可以对一个类进行扩展,可以创建它的子类并对其做任何事情(如新增方法或成员变量、重写基类行为等),那么它就是开放的。有些编程语言允许你通过特殊关键字(例 如 final )来限制对于类的进一步扩展, 这样类就不再是“开放”的了。如果某个类已做好了充分的准备并可供其他类使用的话(即其接口已明确定义且以后不会修改),那么该类就是封闭(你可以称之为完整)的。
  • 如果一个类已经完成开发、测试和审核工作,而且属于某个框架或者可被其他类的代码直接使用的话,对其代码进行修改就是有风险的。你可以创建一个子类并重写原始类的部分内容以完成不同的行为,而不是直接对原始类的代码进行修改。这样你既可以达成自己的目标,但同时又无需修改已有的原始类客户端。
  • 这条原则并不能应用于所有对类进行的修改中。如果你发现类中存在缺陷,直接对其进行修复即可,不要为它创建子类。子类不应该对其父类的问题负责。
  • 你的电子商务程序中包含一个计算运输费用的订单(Order)类,该类中所有运输方法都以硬编码的方式实现。如果你需要添加一个新的运输方式,那就必须承担对 订单 类造成破坏的可能风险来对其进行修改。
    【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第6张图片
  • 你可以通过应用策略模式来解决这个问题。首先将运输方法抽取到拥有同样接口的不同类中。
    【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第7张图片

3. 里氏替换原则(Liskov Substitution Principle)

  • 当你扩展一个类时, 记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递:
  • 这意味着子类必须保持与父类行为的兼容。在重写一个方法时,你要对基类行为进行扩展,而不是将其完全替换。
  • 替换原则是用于预测子类是否与代码兼容,以及是否能与其超类对象协作的一组检查。这一概念在开发程序库和框架时非常重要,因为其中的类将会在他人的代码中使用——你是无法直接访问和修改这些代码的。
  • 与有着多种解释方式的其他设计模式不同,替代原则包含一组对子类(特别是其方法)的形式要求。让我们来仔细看看这些要求。
  • 子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象:
  • ○ 假设某个类有个方法用于给猫咪喂食: feed(Cat c) 。客户端代码总是会将“猫(cat)”对象传递给该方法。
  • ○ 好的方式:假如你创建了一个子类并重写了前面的方法,使其能够给任何“动物(animal,即‘猫’的超类)”喂 食: feed(Animal c) 。如果现在你将一个子类对象而非超类对象传递给客户端代码,程序仍将正常工作。该方法可用于给任何动物喂食,因此它仍然可以用于给传递给客户端的任何“猫”喂食。
  • ○ 不好的方式: 你创建了另一个子类且限制喂食方法仅接 受 “孟 加 拉 猫 (BengalCat, 一 个 ‘猫’ 的 子 类)”: feed(BengalCat c) 。如果你用它来替代链接在某个对象中的原始类,客户端中会发生什么呢?由于该方法只能对特殊种类的猫进行喂食,因此无法为传递给客户端的普通猫提供服务,从而将破坏所有相关的功能。
  • 子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配:
  • ○ 假如你的一个类中有一个方法 buyCat(): Cat 。 客户端代码执行该方法后的预期返回结果是任意类型的“猫”。
  • ○ 好的方式:子类将该方法重写为: buyCat(): BengalCat 。客户端将获得一只“孟加拉猫”,自然它也是一只“猫”,因此一切正常。
  • ○ 不好的方式: 子类将该方法重写为: buyCat(): Animal 。现在客户端代码将会出错,因为它获得的是自己未知的动物种类(短吻鳄?熊?),不适用于为一只“猫”而设计的结构。
  • 子类中的方法不应抛出基础方法预期之外的异常类型:
  • ○ 换句话说,异常类型必须与基础方法能抛出的异常或是其子类别相匹配。这条规则源于一个事实:客户端代码的 try-catch代码块针对的是基础方法可能抛出的异常类型。因此,预期之外的异常可能会穿透客户端的防御代码,从而使整个应用崩溃。
  • ○ 对于绝大部分现代编程语言, 特别是静态类型的编程语言(Java 和 C# 等等),这些规则已内置于其中。如果违反了这些规则,你将无法对程序进行编译。
  • 子类不应该加强其前置条件:
  • ○ 例如,基类的方法有一个 int类型的参数。如果子类重写该方法时,要求传递给该方法的参数值必须为正数(如果该值为负则抛出异常),这就是加强了前置条件。客户端代码之前将负数传递给该方法时程序能够正常运行,但现在使用子类的对象时会使程序出错。
  • 子类不能削弱其后置条件:
  • ○ 假如你的某个类中有个方法需要使用数据库,该方法应该在接收到返回值后关闭所有活跃的数据库连接。
  • ○ 你创建了一个子类并对其进行了修改,使得数据库保持连接以便重用。但客户端可能对你的意图一无所知。由于它认为该方法会关闭所有的连接,因此可能会在调用该方法后就马上关闭程序,使得无用的数据库连接对系统造成“污染”。
  • 超类的不变量必须保留:
  • ○ 这很可能是所有规则中最不“形式”的一条。不变量是让对象有意义的条件。例如,猫的不变量是有四条腿、一条尾巴和能够喵喵叫等。不变量让人疑惑的地方在于它们既可通过接口契约或方法内的一组断言来明确定义,又可暗含在特定的单元测试和客户代码预期中。
  • ○ 不变量的规则是最容易违反的,因为你可能会误解或没有意识到一个复杂类中的所有不变量。因此,扩展一个类的最安全做法是引入新的成员变量和方法,而不要去招惹超类中已有的成员。当然在实际中,这并非总是可行。
  • 子类不能修改超类中私有成员变量的值:
  • ○ 什么?这难道可能吗?原来有些编程语言允许通过反射机制来访问类的私有成员。还有一些语言(Python 和 JavaScript)没有对私有成员进行任何保护。

4. 接口隔离原则(Interface Segregation Principle)

  • 客户端不应被强迫依赖于其不使用的方法:
  • 尽量缩小接口的范围,使得客户端的类不必实现其不需要的行为。
  • 根据接口隔离原则,你必须将“臃肿”的方法拆分为多个颗粒度更小的具体方法。客户端必须仅实现其实际需要的方法。否则,对于“臃肿”接口的修改可能会导致程序出错,即使客户端根本没有使用修改后的方法。
  • 继承只允许类拥有一个超类,但是它并不限制类可同时实现的接口的数量。因此,你不需要将大量无关的类塞进单个接口。你可将其拆分为更精细的接口,如有需要可在单个类中实现所有接口,某些类也可只实现其中的一个接口。
  • 假如你创建了一个程序库,它能让程序方便地与多种云计算供应商进行整合。尽管最初版本仅支持阿里云服务,但它也覆盖了一套完整的云服务和功能。
  • 假设所有云服务供应商都与阿里云一样提供相同种类的功能。但当你着手为其他供应商提供支持时,程序库中绝大部分的接口会显得过于宽泛。其他云服务供应商没有提供部分方法所描述的功能。
    【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第8张图片
  • 尽管你仍然可以去实现这些方法并放入一些桩代码,但这绝不是优良的解决方案。更好的方法是将接口拆分为多个部分。能够实现原始接口的类现在只需改为实现多个精细的接口即可。其他类则可仅实现对自己有意义的接口。
    【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第9张图片
  • 与其他原则一样,你可能会过度使用这条原则。不要进一步划分已经非常具体的接口。记住,创建的接口越多,代码就越复杂。因此要保持平衡。

5. 依赖倒置原则(Dependency Inversion Principle)

高层次的类不应该依赖于低层次的类。 两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。

  • 通常在设计软件时,你可以辨别出不同层次的类:
  • ● 低层次的类实现基础操作(例如磁盘操作、传输网络数据和连接数据库等)。
  • ● 高层次类包含复杂业务逻辑以指导低层次类执行特定操作。

有时人们会先设计低层次的类, 然后才会开发高层次的类。当你在新系统上开发原型产品时,这种情况很常见。由于低层次的东西还没有实现或不确定,你甚至无法确定高层次类能实现哪些功能。如果采用这种方式,业务逻辑类可能会更依赖于低层原语类。

  • 依赖倒置原则建议改变这种依赖方式:
  • 1. 作为初学者, 你最好使用业务术语来对高层次类依赖的低层次操作接口进行描述。 例如, 业务逻辑应该调用 名 为 openReport(file) 的 方 法, 而 不 是 openFile(x) 、readBytes(n) 和 closeFile(x) 等一系列方法。 这些接口被视为是高层次的。
  • 2. 现在你可基于这些接口创建高层次类,而不是基于低层次的具体类。这要比原始的依赖关系灵活很多。
  • 3. 一旦低层次的类实现了这些接口,它们将依赖于业务逻辑层,从而倒置了原始的依赖关系。

依赖倒置原则通常和开闭原则共同发挥作用:你无需修改已有类就能用不同的业务逻辑类扩展低层次的类。

在本例中,高层次的预算报告类(BudgetReport)使用低层次的数据库类(MySQLDatabase)来读取和保存其数据。这意味着低层次类中的任何改变(例如当数据库服务器发布新版本时)都可能会影响到高层次的类,但高层次的类不应关注数据存储的细节。
【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第10张图片
要解决这个问题,你可以创建一个描述读写操作的高层接口,并让报告类使用该接口代替低层次的类。然后你可以修改或扩展低层次的原始类来实现业务逻辑声明的读写接口。
【设计模式从青铜到王者】第一篇:软件设计原则与SOLID原则_第11张图片
其结果是原始的依赖关系被倒置:现在低层次的类依赖于高层次的抽象。


总结

以上就是今天要讲的内容,本文详细介绍了设计模式中软件设计原则与SOLID原则的使用,设计模式提供了大量的方法供我们使用,非常的便捷,我们务必掌握。希望大家多多支持!另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。加油啊!

在这里插入图片描述

你可能感兴趣的:(设计模式,设计模式,开发语言,C++,设计模式原则,面向对象)