【程序如何正确撰写 ?】物件导向程序设计 - SOLID 设计原则 : SRP、OCP、LSP、ISP、DIP

程序设计的武功心法

目录

  • 前言 : 软件的价值
  • SRP : 单一职责原则
  • OCP : 开放 - 封闭原则
  • LSP : 里氏替换原则
  • ISP : 介面隔离原则
  • DIP : 依赖反向原则
  • 设计原则 : 分类排序

前言 : 软件的价值

软件提供的价值有两种 :

  • 第一种 : 让电脑的行为「符合需求」
  • 第二种 : 让电脑的行为可以「轻易改变」

001

软件 (Software) 
Soft - 软的、可以轻易改变的
Ware - 产品

第一种就是让程序能动,但第二种要如何实现 ?

轻易改变是一个抽象的概念,具体的描述可以理解为 :

如何让建构产品的「程序码」更容易的、阅读、维护与扩充。

SOLID 设计原则

实现需求的武功心法

由五种原则的字母 组成 SOLID (坚硬的) 单字的排序 :

  • SRP - 单一职责原则
  • OCP - 开放 - 封闭原则
  • LSP - 里氏替换原则
  • ISP - 介面隔离原则
  • DIP - 依赖反向原则

如果你在网路上搜寻过「软件 设计原则」还会发现有六大、七大或九大原则:

002
它们通常已经包含这五项。


用途

维基百科

存在的目的是为了建置清晰、可读与可延伸的开发指南,
并且还可以应用在 测试驱动开发、敏捷开发,以及自适应软件开发的基本原则。

003

我认为这五项,实际上就是很多软件设计方法论的根源点。

只要完全遵守,即便没有任何的架构,也能够将程序写得井然有序、条条有理。


SRP : 单一职责原则

误解

看到这个名字,直觉想到的是「一个函式只做一件事」或「一个类别只做一种事」。

004

这样的理解不太精确,因为这个是「重构的原则」并不是 SRP。

定义

一个模组应该有一个且只有一个理由会使其改变

更容易理解的描述 :

一个模组应该只对唯一的一个角色负责

005

  • 模组: 指的是原始档 或者 类别
  • 角色: 指的是特定群体的使用者
  • 理由: 指的则是这个群体的需求

合并起来重新描述

一个原始档案的类别只会对系统中特定角色的使用者负责,
只有当这个特定群体的需求改变,程序码才会改变。

从这一段描述可以知道 :

单一职责原则,实际上是一种「分类」的方法,依据的是「不同角色的使用者」(变化)。

006


为什麽 ?

一个简单的理解

程序之所以会修改,通常是使用者需求的改变。

假设 : 一个类别只做一种事,但这个事刚好被两个部门的角色使用到

当其中一个部门提出新的需求,调整时就会影响到另外一个

007

虽然该部门提升业务能力,但另外一个部门却得为他们的收益付出代价。
(被影响的一方,基本上都无法接受。)

更好的情况

建置时分别独立,各自对自己的使用者负责。

008


以开发者的角度来看:

虽然会导致程序码重复,但复制代码的成本绝对会比多个角色调整、验证 
与验证遗漏,导致错误的影响付出较少的代价。

这个原则出现的原因是由於「Conway 定律」的积极推论

Conway 定律说的是 :

软件产品的架构与专案团队的组织结构是互相影响的

Conway 定律,积极推论 :

软件系统的最佳结构 深深受到使用它的组织社会结构所影响

也就是说 :

组织架构通常也是软件架构的最佳参照。

除非组织的部门真的有共用到某些资源,否则我们开发的程序,就不应该将不同角色的程序模组重复使用。


怎麽做 ?

DDD 领域驱动设计的分层结构,可以很好的实现

007

  • 不同的角色 : 映射为领域层(Domain Layer),不同的业务。
    • 该业务的领域服务,各自使用自己的 实体(Entity) 与值对象(VO)
  • 跨部门协作 : 由处理「流程」的应用层(Application Layer)负责。
  • 共用的资源 : 放入到基础设施层(Infrastructure Layer)

在各个层级与区块中都有一个单一负责的对象,因此符合单一职责原则。


OCP : 开放 - 封闭原则

定义

一个软件制品应该对於扩展是开放的 但对於修改是封闭的

话句话说 : 如果今天在建一栋楼

一楼已经完成,追加新的设计,只能从二楼开始。
不应为了某种需求 在一楼的墙壁钻孔、打洞。
万一刚好是某个重要结构,可能导致倾斜或倒塌,这样肯定得不偿失。

软件设计的架构

与其说要遵循这个原则,不如说是要「设计」成符合这个原则的系统。

同样以建楼为例 :

一楼再搭建时,就设想还会添加哪些设备

  • 有采光的需求 -> 预留窗户的空间
  • 有冷气的需求 -> 规划阳台与室内机的动线

为什麽 ?

开头提到过的软件第二种价值 :「让电脑的行为可以轻易改变」。

或具体说法建构,更容易阅读、维护与扩充的产品程序码。

「开放 - 封闭原则」 就是一个实际的【指导方针】

理想状态,在扩展新功能时,修改旧程序的数量,无限趋近於零。

怎麽做 ?

原则的描述,它是一个「大原则」只指引方向。

具体的作法 :

  • 单一职责原则
  • 依赖反向原则 (之後提到)
将「重要」不可轻易改变的模组,保护起来避免外部修改。
将「动态」需要时常变更的模组,保留空间提供後续调整。

DDD 领域驱动设计的架构

领域层通常就是业务的核心逻辑是组织创造收益的根本原因,不可能经常更动。
(会变更的情况 通常都是组织经过重大调整。)

009

所以功能扩充会在「领域层」添加新的「业务区块」,然後才在「容易变动的应用层与使用者介面层」,调整服务的项目清单。

LSP : 里氏替换原则

定义

「里氏」是美国计算机科学家 - Barbarra Liskov

010

她於 1988 年,写下定义子型态的方式 :

这里需要如下的替换性质 :

若对於型态 S 每个物件 o1 都存在一个型态为 T 的物件 o2,
使得在所有针对 T 编写的程序 P 中,用 o1 替代 o2 後 ,程序 P 的行为功能不变,则 S 是 T 的子型态。

这一段描述,使用许多变数。

为了更好的理解,可以先关注「子型态」与「替换」这两个关键词。

  • 子型态 : 说的是物件导向中,继承的关系
  • 替换 : 则是继承关系的一种使用方式

例如 :

  • 应用程序呼叫「授权介面」中计算费用的方法
  • 授权介面分别由个人授权与企业授权「继承实作」

011

应用程序不需要依赖两个子型态类别的任何一种,两种子型态的授权又都可以替换成授权介面的物件。

该范例符合里氏替换原则

012

  • 型态 S : 个人授权与企业授权
  • 型态 T : 授权介面
  • 程序 P : 应用程序

用个人或企业授权的物件,替代授权介面的物件功能不变


为什麽 ?

首先,父类别-抽象介面,存在的目的是什麽 ?

维基百科,里氏替换原则的相关连结: 「契约式设计」

013


契约式设计

014
要求软件设计者必须为软件组件定义正式的、精确的并且可验证的介面。

也就是说:

介面存在的目的,就像是一种契约,用来验证实作提供的东西到底府不符合需求。

015

不验证没有契约,但拿到实际且正确的东西,当然没有问题。 (替代)

但假设 : 已经签好契约

厂商却发给我另外一种东西,还强迫必须接受

对应到程序码 : 负责的模组必须加上各种判断,来辨别这个东西到底是什麽

016

在系统中,额外机制就是混乱因子。
将会导致整体架构逐渐失序,使得系统难以维护与扩充更新。

ISP : 介面隔离原则

定义

不应强迫客户端依赖它不使用的方法

原则的由来

三个使用者,同时使用一个模组,但各自都只有使用其中一个方法。

017

对於任一使用者来说,模组中另外两个方法是他不需要的。

所以在使用者与模组之间,又各自新增一个介面 :

018

该介面只定义使用者会使用的方法,并且隔离彼此。(名称由来)

背後的罗辑

就是不强迫客户端依赖它不使用的方法。(客户端不一定是真实的使用者,有可能是上层模组与下层模组的使用关系。)

为什麽 ?

这个原则,做了两件事情:

  1. 将大的模组接口,拆分成许多小的且具体的接口
  2. 将客户端模组的依赖,转移到小的接口

第一个好处

维护客户端的工程师,可以清楚的知道模组需要的是什麽服务。
(大模组与我之间,关联并没有那麽的直接)

第二个好处

客户端对大模组的依赖解除

解除大模组依赖的好处 ?

大模组的存在是不太正常,但却又自然而然。
因为程序从一开始创建,并不会立马就想到未来会有多少功能。

在原本的模组,拓展新功能会是个省时省力的方法。
一次两次的叠加没啥问题,但当发现已经有点臃肿时,已经无法舍弃。

如何修复 ?

工程师看见这个问题,回头修改会牵连太广、成本太大,而且也会违反「开放 - 封闭原则」。

019


更好的做法是为未来做准备

如果有个全新且更精准的模组替代,对於客户端与依赖的小接口,基本上什麽都不用做。
因为是新模组依赖於我的小接口,而不是我客户端模组还要改动程序码去依赖新模组。

020


DIP : 依赖反向原则

定义

高层次的模组不应该依赖於低层次的模组,两者都应该依赖於抽象介面。
  • 高层次的模组 : 指的是核心的业务规则
  • 低层次的模组 : 指的是非核心的工具、介面或资料库

传统的应用程序架构是高层次透过低层次实现功能

例如 :

将分析报告保存在系统中,是分析的模组透过资料库的模组使用储存的功能。

021


依据原则

两者的依赖关系要调整成分析报告使用抽象介面的保存方法,然後资料库在依据抽象介面的定义,实作资料库的储存功能。

022


为什麽 ?

高层次为什麽是高层次 ?

因为它是企业「创造收益」的核心规则,即便没有系统,使用纸笔作业也依然成立。

因此,不能随意变动应该被保护起来

023

如果,依赖於低层次模组

 代表低层次模组变动会回头影响到高层次模组。严重一点出现异常,更可能导致高层次模组无法作业。

024


稳妥起见 : 解开依赖关系

即便低层次模组发生问题,最多也只是无法储存,不会导致高层次模组业务停摆。

025


为什麽依赖於抽象介面?

为了可以拓展新的功能

026

就像前面提到过的抽象介面是契约精神的展现,我要的东西规格已经定义清楚。
但如果有更好的方法,就是额外实作新的功能,将原本旧的模组替换掉即可。

设计原则 : 分类排序

个人理解 : 分成三部分

上述的 SOLID 设计原则的描述顺序,应该多少会感到有些混乱。

 这是因为 SOLID 只是单字字母的排序,并不是重要性或者因果推演的排序。

我认为可以分类成三个部分:

027

第一个部分 : 开放 - 封闭原则

它是一个大方向原则,总体目标就是将系统设计成「容易拓展新的,并且少量修改旧的」。

第二个部分 : 单一职责原则

讲的是「分类」的方法,可以运用在「开放 - 封闭原则」,「封闭」的部分,
透过需求根源点 - 「角色的分类」,将可能会修改的部分集中。

第三个部分 : 依赖反转原则、里氏替换原则、介面隔离原则

讲的是介面的使用方式,可以运用在「开放 - 封闭原则」,「开放」的部分。

依赖反转原则 :

告诉你使用抽象介面,可以在保证核心正常运作的情况下,还能够拓展新功能。

028


里氏替换原则 :

介面与类别 - 一对多关系
它可以帮助系统,在拓展功能时,保证子型态模组的可靠性。

029


介面隔离原则 :

介面与类别 - 多对一关系
它可以帮助系统,无法分割类别时,保证拓展功能的纯粹性,使得模组具有高内聚与可读性。

030


就像是「雅量」

我在搜寻相关资讯时,总觉得五项原则,就像是 :

一千个读者心中,有一千个哈姆雷特。

031

不是原则吗 ?

怎麽讲的都不太一样 !?

032

如果上述有错或者跟你想的有出入,都可以留言讨论。

参考资料

  • 书籍 : Clean Architecture - 无瑕的程序码 , 整洁的软件设计与架构篇

<<:  MySQL显示问题

>>:  可以代替GoogleChrome的4个浏览器

【第二三天 - Flutter iBeacon 官方范例讲解(上)】

前言 因为小弟有一些专案需求需要使用到 iBeacon,因此就有深入去了解 iBeacon 套件用法...

OpenStack Neutron 介绍 — OVS Self-service Networks

本系列文章同步发布於笔者网站 上篇介绍了 Open vSwitch with Provider Ne...

Day 6-单元测试 NUnit 更多常用的特性-1 (基础-5)

Setup 和 Teardown 在单元测试的艺术提到:进行单元测试时,很重要的一点是确保之前测试过...

压力平衡

早期运动Day10 - 对於压力,我们都需要平衡报导 今天在运动时,听着《自控力:和压力做朋友》 里...

04 你的专研不是你的专研

升上高中也有专题研究的学分。为了找到适合的题目,我和同个专研的同学一起到师大资工(和科学班合作的校系...