OOD设计基本原则

OOD 设计基本原则

 

Ø       OCP 原则

Ø       里氏替换原则

Ø       依赖倒置原则

Ø       接口隔离原则

Ø       聚合与继承原则

Ø       单一职责原则

Ø       Separation of concerns Principle

Ø       Pareto Principle ( 帕雷多原则 80/20 原则 )

 

 

OOD 设计原则在提高一个系统可维护性的同时, 提高这个系统的可复用性. 他们是一些指导原则, 依照这些原则设计, 我们就可以有效的提高系统的复用性, 同时提高系统的可维护性.

OCP 原则

Open-Closed Principle

 

这些OOD 原则的一个基石就是"- 闭原则"(Open-Closed Principle OCP). 这个原则最早是由Bertrand Meyer 提出, 英文的原文是:Software entities should be open for extension, but closed for modification. 意思是说, 一个软件实体应当对扩展开放, 对修改关闭. 也就是说, 我们在设计一个模块的时候, 应当使这个模块可以在不被修改的前提下被扩展, 换句话说就是, 应当可以在不必修改源代码的情况下改变这个模块的行为.

满足OCP 的设计给系统带来两个无可比拟的优越性.

Ø       通过 扩展已有的软件系统, 可以提供新的行为, 以满足对软件的新需求, 使变化中的软件系统有一定的适应性和灵活性 .

Ø       已有的软件模块, 特别是最重要的抽象层模块不能再修改, 这就使变化中的软件系统有一定的稳定性和延续性 .

具有这两个优点的软件系统是一个高层次上实现了复用的系统, 也是一个易于维护的系统. 那么, 我们如何才能做到这个原则呢? 不能修改而可以扩展, 这个看起来是自相矛盾的. 其实这个是可以做到的, 按面向对象的说法, 这个就是不允许更改系统的抽象层, 而允许扩展的是系统的实现层 .

解决问题的关键在: 抽象化 . 我们让模块依赖于一个固定的抽象体, 这样它就是不可以修改的; 同时, 通过这个抽象体派生, 我们就可以扩展此模块的行为功能. 如此, 这样设计的程序只通过增加代码来变化而不是通过更改现有代码来变化, 前面提到的修改的副作用就没有了.

" -" 原则如果从另外一个角度讲述, 就是所谓的" 对可变性封装原则 "(Principle of Encapsulation of Variation, EVP ). 讲的是找到一个系统的可变因素, 将之封装起来. 在我们考虑一个系统的时候, 我们不要把关注的焦点放在什么会导致设计发生变化上, 而是考虑允许什么发生变化而不让这一变化导致重新设计. 也就是说, 我们要积极的面对变化, 积极的包容变化, 而不是逃避.

  

[SHALL01] 将这一思想用一句话总结为:" 找到一个系统的可变因素, 将它封装起来 ", 并将它命名为" 对可变性的封装原则"." 对可变性的封装原则" 意味者两点:

Ø       一种 可变性应当被封装到一个对象里面 , 而不应当散落到代码的很多角落里面 . 同一种可变性的不同 表象意味着同一个继承等级结构中的具体子类. 继承应当被看做是封装变化的方法 , 而不应当是被认为从一般的对象生成特殊的对象的方法( 继承经常被滥用).

Ø       一种可变性不应当与另外一种可变性混合在一起. 从具体的类图来看, 如果继承结构超过了两层, 那么就意味着将两种不同的可变性混合在了一起.

" 对可变性的封装原则" 从工程的角度说明了如何实现OCP. 如果按照这个原则来设计, 那么系统就应当是遵守OCP. 但是现实往往是残酷的, 我们不可能100% 的遵守OCP, 但是我们要向这个目标来靠近. 设计者要对设计的模块对何种变化封闭做出选择.

 

里氏替换原则

Liskov Substitution Principle

 

从上一篇的"-" 原则中可以看出, 面向对象设计的重要原则是创建抽象化, 并且从抽象化导出具体化 . 这个导出要使用继承关系和一个原则: 里氏替换原则(Liskov Substitution Principle, LSP).
  

那么什么是里氏替换原则呢? 有个严格的表述, 绕口, 不好记. 还是比较白话的这个好记. 说的是: 一个软件实体如果使用的是一个基类的话, 那么一定适用于其子类, 而且它察觉不出基类对象和子类对象的区别 . 也就是说, 在软件里面, 把基类都替换成它的子类, 程序的行为没有变化.
  

LSP 是继承复用的基石, 只有当衍生类可以替换掉基类, 软件单位的功能不受到影响时, 基类才能真正被复用, 而衍生类也能够在基类的基础上增加新的行为.
  

下面, 我们从代码重构的角度来对LSP 进行理解.LSP 讲的是基类和子类的关系. 只有当这种关系存在时, 里氏替换关系才存在. 如果两个具体的类A,B 之间的关系违反了LSP 的设计,( 假设是从BA 的继承关系) 那么根据具体的情况可以在下面的两种重构方案中选择一种.
  

Ø       创建一个新的抽象类C, 作为两个具体类的超类,A,B 的共同行为移动到C 中来解决问题.BA 的继承关系改为委派关系. 为了说明, 我们先用第一种方法来看一个例子。第二种办法在另外一个原则中说明. 我们就看那个著名的长方形和正方形的例子. 对于长方形的类, 如果它的长宽相等, 那么它就是一个正方形, 因此, 长方形类的对象中有一些正方形的对象. 对于一个正方形的类, 它的方法有个setSidegetSide, 它不是长方形的子类, 和长方形也不会符合LSP.
  

那么, 如果让正方形当做是长方形的子类, 会出现什么情况呢? 我们让正方形从长方形继承, 然后在它的内部设置width 等于height, 这样, 只要width 或者height 被赋值, 那么widthheight 会被同时赋值, 这样就保证了正方形类中,widthheight 总是相等的. 现在我们假设有个客户类, 其中有个方法, 规则是这样的, 测试传人的长方形的宽度是否大于高度, 如果满足就停止下来, 否则就增加宽度的值. 现在我们来看, 如果传人的是基类长方形, 这个运行的很好. 根据LSP, 我们把基类替换成它的子类, 结果应该也是一样的, 但是因为正方形类的widthheight 会同时赋值, 这个方法没有结束的时候, 条件总是不满足, 也就是说, 替换成子类后, 程序的行为发生了变化, 它不满足LSP.
  

那么我们用第一种方案进行重构, 我们构造一个抽象的四边形类, 把长方形和正方形共同的行为放到这个四边形类里面, 让长方形和正方形都是它的子类, 问题就OK. 对于长方形和正方形,widthheight 是它们共同的行为, 但是给widthheight 赋值, 两者行为不同, 因此, 这个抽象的四边形的类只有取值方法, 没有赋值方法. 上面的例子中那个方法只会适用于不同的子类,LSP 也就不会被破坏.
  

在进行设计的时候, 我们尽量从抽象类继承, 而不是从具体类继承. 如果从继承等级树来看, 所有叶子节点应当是具体类, 而所有的树枝节点应当是抽象类或者接口. 当然这个只是一个一般性的指导原则, 使用的时候还要具体情况具体分析.

 

依赖倒置原则

Dependency-Inversion Principles

" - " 原则是我们 OOD 的目标 , 达到这一目标的主要机制就是 " 依赖倒转原则 ". 这个原则的内容是 : 要依赖于抽象 , 不要依赖于具体 .

 

对于抽象层 次来说 , 它是一个系统的本质的概括 , 是系统的商务逻辑和宏观的 , 战略性的决定 , 是必然性的体现 ; 具体的层次 则是与实现有关的算法和逻辑 , 一些战术性的决定 , 带有相当大的偶然性 . 传统的过程性系统设计办法倾向于使高层次的模块依赖于低层次的模块 ; 抽象层次依赖于具体层次 . 这实际上就是微观决定宏观 , 战术决定战略 , 偶然决定必然 . 依赖倒转原则就是要把这种错误的依赖关系倒转过来 .

 

许多的建构设计模型 , 例如 COM, CORBA, JavaBean, EJB , 它们背后的基本原则就是 DIP.

     

对于软件设计的两个目标 , 复用和可维护性 来说 , 传统的设计侧重于具体层次模块的复用和可维护 , 比如算法 , 数据结构 , 函数库等等 . 但是 , 对系统的抽象是比较稳定的 , 它的复用是很重要的 , 同时 , 抽象层次的可维护性也应当是一个重点 . 就是说 DIP 也导致复用和可维护性的 " 倒转 ".

      

我们现在来看看依赖有几种 , 依赖也就是耦合 , 分为下面三种

Ø       零耦合 (Nil Coupling) 关系 , 两个类没有依赖关系 , 那就是零耦合

Ø       具体耦合 (Concrete Coupling) 关系 , 两个具体的类之间有依赖关系 , 那么就是具体耦合关系 , 如果一个具体类直接引用另外一个具体类 , 就会发生这种关系 .

Ø       抽象耦合 (Abstract Coupling) 关系 . 这种关系发生在一个具体类和一个抽象类之间 , 这样就使必须发生关系的类之间保持最大的灵活性 .

     

DIP 要求客户端依赖于抽象耦合 , 抽象不应当依赖于细节 , 细节应当依赖于抽象 (Abstractions should not depend upon details. Details should depend upon abstractions), 这个原则的另外一个表述就是 " 四人团 " 强调的那个 : 要针对接口编程 , 不要对实现编程 .(Program to an interface, not an implementation), 程序在需要引用一个对象时 , 应当尽可能的使用抽象类型作为变量的静态类型 , 这就是针对接口编程的含义 . DIP 是达到 " - " 原则的途径 .

 

要做到 DIP, 用抽象方式耦合是关键 . 由于一个抽象耦合总要涉及具体类从抽象类继承 . 并且需要保证在任何引用到某类的地方都可以改换成其子类 , 因此 ,LSP DIP 的基础 .DIP OOD 的核心原则 , 设计模式的研究和应用都是用它作为指导原则的 .DIP 虽然强大 , 但是也很难实现 . 另外 ,DIP 是假定所有的具体类都会变化 , 这也不是全对 , 有些具体类就相当稳定 . 使用这个类的客户端就完全可以依赖这个具体类而不用再弄一个抽象类 .

 

接口隔离原则

Interface Segregation Principle

 

接口隔离原则 (ISP): 使用多个专门的接口比使用单一的总接口要好 . 也就是说 , 一个类对另外一个类的依赖性应当是建立在最小的接口上的 .

这里的 " 接口 " 往往有两种不同的含义 :

Ø       一种是指一个类型所具有的方法特征的集合 , 仅仅是一种逻辑上的抽象 ;

Ø       另外一种是指某种语言具体的 " 接口 " 定义 , 有严 格的定义和结构 . 比如 Java 语言里面的 Interface 结构 .

对于这两种不同的含义 ,ISP 的表达方式以及含义都有所不同 .( 上面说的一个类型 , 可以理解成一个类 , 我们定义了一个类 , 也就是定义了一种新的类型 )

当我们把 " 接口 " 理解成一个类所提供的所有方法的特征集合的时候 , 这就是一种逻辑上的概念 . 接口的划分就直接带来类型的划分 . 这里 , 我们可以把接口理解成角色 , 一个接口就只是代表一个角色 , 每个角色都有它特定的一个接口 , 这里的这个原则可以叫做 " 角色隔离原则 ".

如果把 " 接口 " 理解成狭义的特定语言的接口 , 那么 ISP 表达的意思是说 , 对不同的客户端 , 同一个角色提供宽窄不同的接口 , 也就是定制服务 , 个性化服务 . 就是仅仅提供客户端需要的行为 , 客户端不需要的行为则隐藏起来 .

在我们进行 OOD 的时候 , 一个重要的工作就是恰当的划分角色和角色对应的接口 . 将没有关系的接口合并在一起 , 是对角色和接口的 污染 . 如果将一些看上去差不多的接口合并 , 并认为这是一种代码优化 , 这是错误的 . 不同的角色应该交给不同的接口 , 而不能都交给一个接口 .

 

对于定制服务 , 这样做最大的好处就是系统的可维护性 . 向客户端提供接口是一种承诺 ,public 接口后是不能改变的 , 因此不必要的承诺就不要做出 , 承诺越少越好 .

聚合与继承原则

 

合成 Composition )和聚合 Aggregation )都是关联 Association )的特殊种类。

聚合表示整体和部分的关系,表示 拥有 ;合成则是一种更强的 拥有 ,部分和整体的生命周期一样。合成的新的对象完全支配其组成部分,包括它们的创建和湮灭等。一个合成关系的成分对象是不能与另一个合成关系共享的。换句话说,合成是值的聚合( Aggregation by Value ),而一般说的聚合是引用的聚合( Aggregation by Reference )。

 

简短的说,合成-聚合复用原则( CARP )是指,尽量使用合成 / 聚合,而不是使用继承
OOD 中,有两种基本的办法可以实现复用,一种是通过合成 / 聚合,另外一种就是通过继承

 

通过合成 / 聚合 的好处是:

Ø       新对象存取成分对象的唯一方法是通过成分对象的接口。

Ø       这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。

Ø       这种复用支持包装。

Ø       这种复用所需的依赖较少。

Ø       每一个新的类可以将焦点集中在一个任务上。

Ø       这种复用可以在运行时间内动态进行,新对象可以动态的引用与成分对象类型相同的对象。

Ø       作为复用手段可以应用到几乎任何环境中去。

Ø       它的缺点 就是系统中会有较多的对象需要管理。
   

通过继承 来进行复用的优点是:

Ø       新的实现较为容易,因为超类的大部分功能可以通过继承的关系自动进入子类。

Ø       修改和扩展继承而来的实现较为容易。

缺点 是:

Ø       继承复用破坏封装,因为继承将超类的实现细节暴露给子类。由于超类的内部细节常常是对于子类透明的,所以这种复用是透明的复用,又称 白箱 复用。

Ø       如果超类发生改变,那么子类的实现也不得不发生改变。

Ø       从超类继承而来的实现是静态的,不可能在运行时间内发生改变,没有足够的灵活性。

Ø       继承只能在有限的环境中使用。

 

如何选择?

要正确的选择合成 / 复用和继承,必须透彻的理解里氏替换原则 Coad 法则 。里氏替换原则前面学习过, Coad 法则由 Peter Coad 提出,总结了一些什么时候使用继承作为复用工具的条件。只有当以下的 Coad 条件全部被满足时,才应当使用继承关系:

Ø       子类是超类的一个特殊种类,而不是超类的一个角色,也就是区分 “Has-A” “Is-A” 。只有 “Is-A” 关系才符合继承关系, “Has-A” 关系应当用聚合来描述。

Ø       永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。

Ø       子类具有扩展 超类的责任,而不是具有置换 调( override )或注销 掉( Nullify )超类的责任。如果一个子类需要大量的置换掉超类的行为,那么这个类就不应该是这个超类的子类。

Ø       只有在分类学角度上有意义时,才可以使用继承。不要从工具类继承。

 

错误的使用继承而不是合成 / 聚合的一个常见原因是错误的把 “Has-A” 当成了 “Is A” “Is A” 代表一个类是另外一个类的一种; “Has-A” 代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。
我们看一个例子。如果我们把 当成一个类,然后把 雇员 经理 学生 当成是 的子类。这个的错误在于把 角色 的等级结构和 的等级结构混淆了。 经理 雇员 学生 是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是雇员的话,就不可能是经理,也不可能是学生,这显然不合理。正确的设计是有个抽象类 角色 可以拥有多个 角色 (聚合), 雇员 经理 学生 角色 的子类。
另外一个就是只有两个类满足里氏替换原则的时候,才可能是 “Is A” 关系。也就是说,如果两个类是 “Has-A” 关系,但是设计成了继承,那么肯定违反里氏替换原则。

 

单一职责原则

Single Responsibility Principle (SRP)

 

就一个类而言,应该只专注于做一件事和仅有一个引起它变化的原因。

 

所谓职责,我们可以理解他为功能,就是设计的这个类功能应该只有一个,而不是两个或更多。也可以理解为引用变化的原因,当你发现有两个变化会要求我们修改这个类,那么你就要考虑撤分这个类了。因为职责是变化的一个轴线,当需求变化时,该变化会反映类的职责的变化。

“就像一个人身兼数职,而这些事情相互关联不大,,甚至有冲突,那他就无法很好的解决这些职责,应该分到不同的人身上去做才对。”

 

二、举例说明:

违反 SRP 原则代码 :

modem 接口明显具有两个职责:连接管理和数据通讯;

interface Modem

{

    public void dial(string pno);

    public void hangup();

    public void send(char c);

    public void recv();

}

 

如果应用程序变化影响连接函数,那么就需要重构:

interface DataChannel

{

    public void send(char c);

    public void recv();

}

interface Connection

{

    public void dial(string pno);

    public void hangup();

}

 

三、 SRP 优点:

消除耦合,减小因需求变化引起代码僵化性臭味

 

四、使用 SRP 注意点:

1 、一个合理的类,应该仅有一个引起它变化的原因,即单一职责;

2 、在没有变化征兆的情况下应用 SRP 或其他原则是不明智的;

3 、在需求实际发生变化时就应该应用 SRP 等原则来重构代码;

4 、使用测试驱动开发会迫使我们在设计出现臭味之前分离不合理代码;

5 、如果测试不能迫使职责分离,僵化性和脆弱性的臭味会变得很强烈,那就应该用 Facade Proxy 模式对代码重构;

 

Separation of concerns Principle

In computer science , separation of concerns (SoC ) is the process of breaking a computer program into distinct features that overlap in functionality as little as possible . A concern is any piece of interest or focus in a program. Typically, concerns are synonymous with features or behaviors. Progress towards SoC is traditionally achieved through modularity and encapsulation , with the help of information hiding . Layered designs in information systems are also often based on separation of concerns (e.g., presentation layer, business logic layer, data access layer, database layer).

All programming paradigms aid developers in the process of improving SoC. For example, object-oriented programming languages such as C++ , Java , and C# can separate concerns into objects , and a design pattern like MVC can separate content from presentation and data-processing (model) from content. Service-oriented design can separate concerns into services . Procedural programming languages such as C and Pascal can separate concerns into procedures . Aspect-oriented programming languages can separate concerns into aspects and objects .

Separation of concerns is an important design principle in many other areas as well, such as urban planning , architecture and information design . The goal is to design systems so that functions can be optimized independently of other functions , so that failure of one function does not cause other functions to fail, and in general to make it easier to understand, design and manage complex interdependent systems. Common examples include using corridors to connect rooms rather than having rooms open directly into each other, and keeping the stove on one circuit and the lights on another.

Pareto Principle ( 帕雷多原则 80/20 原则 )

 

1960 年意大利经济学家帕雷多建立了一个数学模型来描述国家不平等的财富分配,发现 20% 的人拥有了财富的 80%

在帕雷多经过观察并建立了模型之后,许多人都在他们各自的领域发现了同样的现象。

关于 重要的少数和普遍的多数 的发现,即 20% 因素往往决定事物 80% 的结果变成了有名的帕雷多定律或者 80/20 原则。

80/20 原则的含义是一切事物都是这样组成的: 20% 是至关重要的,而 80% 是平常的。

80/20 原则在软件开发领域的体现:

1 )软件开发中 20% 的功能是用户经常使用的, 80% 的功能其实没有那么重要;

2 )软件框架的设计能满足 80% 的应用开发, 20% 可能并不适用;在架构设计中要意思到不能满足所有的情况,同时考虑特殊情况的特殊处理;

3 )软件开发团队中非核心成员和核心成员也体现了 80/20 原则;

4 80% 的软件 Bug 集中在 20% 的软件模块中,体现了重点核心模块的重点开发和维护。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值