面向对象设计模式与Go语言实现 – 面向对象设计原则

本系列文章是针对设计模式的系列文章,网上的很多设计模式的文章只讲了个大概和一部分纯理论,示例代码也是实际场景中根本遇不到,往往只会出现在课本中的无生产意义的代码模型。本系列文章则希望在讲述设计模式的同时,采用更贴合生产实践中的代码,在实战中学习设计模式的相关概念,理解设计模式的精髓。

面向对象设计原则

关于面向对象设计原则众说纷纭,有人说是6种有人说是7种,但无论有几种实际上内容都是一致的。

本文将基于 亚历山大 · 什韦茨(Alexander Shvets)的《深入设计模式》中的分类,介绍面软件设计原则。

1. 封装

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

可进行方法层面的封装与类层面的封装

2. 面向接口

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

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

当你需要两个类进行合作时,可以让其中一个类依赖于另一个类。但是,还有另外一种更灵活的方式来设置对象之间的合作关系。

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

3. 组合

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

不过,继承这件事通常只有在程序中已包含大量类,且修改任何东西都非常困难时才会引起关注。下面就是此类问题的清单。

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

好在Golang在语言层面就解决了这个问题,因为Go语言中没有继承 XD

4. SOLID原则

SOLID 的五条原则是在罗伯特·马丁的著作《敏捷软件开发:原则、模式与实践》中首次提出的。SOLID 是让软件设计更易于理解、更加灵活和更易于维护的五个原则的简称。

S:单一职责原则

单一职责原则(Single Responsibility Principle)

修改一个类的原因只能有一个。

尽量让每个类只负责软件中的一个功能,并将该功能完全封装(你也可称之为隐藏)在该类中。
这条原则的主要目的是减少复杂度。你不需要费尽心机地去构思如何仅用200 行代码来实现复杂设计,实际上完全可以使用十几个清晰的方法。
当程序规模不断扩大、变更不断增加后,真实问题才会逐渐显现出来。到了某个时候,类会变得过于庞大,以至于你无法记住其细节。查找代码将变得非常缓慢,你必须浏览整个类,甚至整个程序才能找到需要的东西。程序中实体的数量会让你的大脑堆栈过载,你会感觉自己对代码失去了控制。
还有一点:如果类负责的东西太多,那么当其中任何一件事发生改变时,你都必须对类进行修改。而在进行修改时,你就有可能改动类中自己并不希望改动的部分。

O:开闭原则

开闭原则(open/closed Principle)

对于扩展, 类应该是“开放”的; 对于修改, 类则应是“封闭”的。

本原则的主要理念是在实现新功能时能保持已有代码不变。

如果你可以对一个类进行扩展,可以创建它的子类并对其做任何事情(如新增方法或成员变量、重写基类行为等), 那么它就是开放的。有些编程语言允许你通过特殊关键字(例如final ) 来限制对于类的进一步扩展, 这样类就不再是“开放”的了。如果某个类已做好了充分的准备并可供其他类使用的话(即其接口已明确定义且以后不会修改),那么该类就是封闭(你可以称之为完整)的。

如果一个类已经完成开发、测试和审核工作,而且属于某个框架或者可被其他类的代码直接使用的话,对其代码进行修改就是有风险的。你可以创建一个子类并重写原始类的部分内容以完成不同的行为,而不是直接对原始类的代码进行修改。这样你既可以达成自己的目标,但同时又无需修改已有的原始类客户端。

这条原则并不能应用于所有对类进行的修改中。如果你发现类中存在缺陷,直接对其进行修复即可,不要为它创建子类。
子类不应该对其父类的问题负责。

L:里氏替换原则

里氏替换原则(Liskov Substitution Principle)

当你扩展一个类时, 记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。

这意味着子类必须保持与父类行为的兼容。在重写一个方法时,你要对基类行为进行扩展,而不是将其完全替换。
替换原则是用于预测子类是否与代码兼容,以及是否能与其超类对象协作的一组检查。这一概念在开发程序库和框架时
非常重要, 因为其中的类将会在他人的代码中使用——你是无法直接访问和修改这些代码的。
与有着多种解释方式的其他设计模式不同,替代原则包含一组对子类(特别是其方法)的形式要求。

  • 子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象。

    即设计子类方法时,尽可能的将入参类型设置为父类的类型

  • 子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配。

    即设计子类方法时,尽可能的将入参类型设置为子类的类型

  • 子类中的方法不应抛出基础方法预期之外的异常类型

    在C++、Java语言中,如果抛出基础方法预期之外的异常类型,编译时会发生错误,而Golang、Rust等使用ErrCode处理错误的语言则需要额外注意返回错误的处理

  • 子类不应该加强其前置条件

    例如,基类的方法有一个int类型的参数。如果子类重写该方法时,要求传递给该方法的参数值必须为正数(如果该值为负则抛出异常), 这就是加强了前置条件。客户端代码之前将负数传递给该方法时程序能够正常运行,但现在使用子类的对象时会使程序出错。

  • 子类不能削弱其后置条件

    假如你的某个类中有个方法需要使用数据库,该方法应该在接收到返回值后关闭所有活跃的数据库连接。
    你创建了一个子类并对其进行了修改,使得数据库保持连接以便重用。但客户端可能对你的意图一无所知。由于它认为
    该方法会关闭所有的连接,因此可能会在调用该方法后就马上关闭程序,使得无用的数据库连接对系统造成“污染”。

  • 超类的不变量必须保留

    不变量是让对象有意义的条件。因此,扩展一个类的最安全做法是引入新的成员变量和方法,而不要去招惹超类中已
    有的成员。当然在实际中,这并非总是可行。

  • 子类不能修改超类中私有成员变量的值

    有些编程语言允许通过反射机制来访问类的私有成员。还有一些语言(Python 和JavaScript)没有对私有成员进行任何保护。

其实总结一点就是,暴露给用户的总是父类/接口,所以在子类实现父类/接口的方法时,要以上层为准。

I:接口隔离原则

接口隔离原则(Interface Segregation Principle)

客户端不应被强迫依赖于其不使用的方法。

尽量缩小接口的范围,使得客户端的类不必实现其不需要的行为。

根据接口隔离原则,你必须将“臃肿”的方法拆分为多个颗粒度更小的具体方法。客户端必须仅实现其实际需要的方法。否则,对于“臃肿”接口的修改可能会导致程序出错,即使客户端根本没有使用修改后的方法。

继承只允许类拥有一个超类,但是它并不限制类可同时实现的接口的数量。因此,你不需要将大量无关的类塞进单个接口。你可将其拆分为更精细的接口,如有需要可在单个类中实现所有接口,某些类也可只实现其中的一个接口。

请过度使用这条原则。不要进一步划分已经非常具体的接口。记住,创建的接口越多,代码就越复杂。因此要保持平衡。

D:依赖倒置原则

依赖倒置原则(Dependency Inversion Principle)

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

通常在设计软件时,你可以辨别出不同层次的类。

  • 低层次的类实现基础操作(例如磁盘操作、传输网络数据和
    连接数据库等)。
  • 高层次类包含复杂业务逻辑以指导低层次类执行特定操作。

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

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

面向对象设计原则与设计模式

在1995 年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了 23 种设计模式,从此树立了软件设计模式领域的里程碑,人称「GoF设计模式」。
这23种设计模式包括了三大类型,即创建型设计模式、结构型设计模式、行为型设计模式,以此为分类依据,这23种设计模式的概览如下:

  • 创建型模式(Creational Patterns):

    1. 单例模式(Singleton)

    2. 工厂方法模式(Factory Method)

    3. 抽象工厂模式(Abstract Factory)

    4. 建造者模式(Builder)

    5. 原型模式(Prototype)

  • 结构型模式(Structural Patterns):

    1. 适配器模式(Adapter)

    2. 桥接模式(Bridge)

    3. 组合模式(Composite)

    4. 装饰者模式(Decorator)

    5. 外观模式(Facade)

    6. 享元模式(Flyweight)

    7. 代理模式(Proxy)

  • 行为型模式(Behavioral Patterns):

    1. 责任链模式(Chain of Responsibility)

    2. 命令模式(Command)

    3. 解释器模式(Interpreter)

    4. 迭代器模式(Iterator)

    5. 中介者模式(Mediator)

    6. 备忘录模式(Memento)

    7. 观察者模式(Observer)

    8. 状态模式(State)

    9. 策略模式(Strategy)

    10. 模板方法模式(Template Method)

    11. 访问者模式(Visitor)

设计模式是践行面向对象设计原则的良好案例,本系列文章将按照这三种分类,讲述这23种经典的设计模式,并适当扩展在此23种设计模式之外的常用设计模式

写在后面

面向对象设计原则与设计模式,不一定总是适用的,在一些复杂场景下,运用设计模式可以将系统解耦,增加系统的可维护性、可拓展性,降低系统的复杂度。但是过度运用设计模式反而会大大增加系统的复杂度和理解难度,所以请适当使用设计模式。
而且面向对象中的很多设计都过于理想化,实际生产过程中很大一部分都没有完全利用到面向对象理想中的最大的优势,因为将一个系统模块进行面向对象的抽象见面在一些复杂场景下往往是极具挑战性,或者说从实践角度上来说是意义不大的。正如如今在服务端开发领域用的最多的MVC模型,其本质上就是典型的面向过程的贫血模型,而真正面向对象充血模式的DDD,多数是难以落地的,多是进行进一步封装抽象,过度追求面向对象往往只会徒增系统复杂度。
所以说,学习设计模式的过程是一个则其善者而从之,其不善者而改之的过程,软件设计没有银弹,只有在合适的场合运用合适的设计。

暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇