敏捷软件开发学习笔记(二)之敏捷设计基本原则

以下内容参考敏捷软件开发原则、模式与实践以及设计模式之面向对象与类基础特征概念设计模式之面向对象七大基本原则

面向对象基础概念

面向对象三大基本特征

封装

封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。

继承

继承是一种类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。

多态

多态性是指允许不同类的对象对同一消息作出响应。多态性包括编译时多态和运行时多态。
主要作用就是用来将接口和实现分离开,改善代码的组织结构,增强代码的可读性。
在某些很简单的情况下,或许我们不使用多态也能开发出满足我们需要的程序,但大多数情况,如果没有多态,就会觉得代码极其难以维护。

面向对象通过类和对象来实现抽象,实现时诞生了三个重要的特性,也就是由于这三个特性才衍生出了各种各样的设计模式。

面向对象类关系

通过大量代码和经验可以得知,类与类之间主要有6种关系模式,这六种模板写法导致了平时书写代码的不同耦合度。具体如下所列(耦合度依次增强排列):

  1. 依赖关系
  2. 关联关系
  3. 聚合关系
  4. 组合关系
  5. 继承关系
  6. 实现关系

为了记住类与类之间的关系,我自己总结了一句话方便记忆:《衣(依赖)冠(关联)剧(聚合)组(组合)纪(继承)实(实现)》。详细解释可以参考设计模式之面向对象与类基础特征概念,写的很有意思。

什么是敏捷设计

实际上满足工程设计标准的唯一软件文档,就是源代码清单。 —Jack Reeves

软件腐化

事实证明,随着软件开发的不断迭代,代码会变得越来越难以维护,更改代码越来越困难,当你的代码出现以下的特征时,就表明软件正在腐化。

  • 僵化性(Rigidity):很难对系统进行改动,因为每个改动都会迫使许多对系统其它部分的其他改动。
  • 脆弱性(Fraglity):对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现了问题。
  • 牢固性(Immobility):很难解开系统的纠结,使之成为一些可在其它系统中重用的组件
  • 粘滞性(Viscosity):做正确的事情比做错误的事情要困难(做错误的事情是容易的,但是做正确的事情却很难)
  • 不必要的复杂性(Needless Compxity):设计中包含有不具任何直接好处的基础结构
  • 不必要的重复(Needless Repetition):设计中包含重复的结构,而该重复的结构本可以使用单一的抽象进行统一
  • 晦涩性(Opacity):很难理解、阅读、没有很好的表现出意图

敏捷开发

敏捷设计是一个过程,不是一个事件。它是一个持续的应用原则、模式以及实践来改进软件的结构性和持续性的过程。它致力于保持系统设计在任何时间都尽可能的简单、干净以及富有表现力。

面向对象设计原则

以下所有的原则都是为了构建高内聚、低耦合的系统

单一职责原则(SRP)

就一个类来说,应该有一个引起它变化的原因

为什么需要SRP?

如果一个类承担的职责过多,就等于将这些类耦合到了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这样的耦合会导致脆弱(fragile)的设计,当变化发生的时候,设计会遭受到意想不到的破坏。

职责的意思是“变化的原因”

如何判断是否违反了SRP

如果你能够想到多余一个的动机去改变一个类,那么这个类就具有多于一个的职责。但是有时候这是比较困难的。如以下的这个例子,大多数人会认为这个接口是非常合理的。

interface Modem
{
	public void dial(String pro);
	public void hangup();
	public void send(char c);
	public void recv();
}

从正常逻辑来看,一个Modem就是具有这些功能,连接、断开、发送、接收。然而,该接口中却显示出两个职责。第一个职责是连接管理,第二个职责是数据通信

那么这两个职责该被分离吗?

这依赖于应用程序变化的方式。

  • 如果两个职责不是同时进行变化,就需要分离,不分离就具有僵化性的臭味。
  • 如果两个职责是同时变化,就不分离,分离了就具有不必要的复杂性的臭味。

结论

SRP是所有原则中最简单的之一,也是最难正确运用的之一。我们会自然而然地将职责结合在一起。软件设计真正要做的许多内容,就是发现职责并将那些职责相互分离。事实上,我们将要论述的其他原则都会以这样或者那样的方式回到这个问题上。

开放-封闭原则(OCP)

软件实体(类、模块、函数等等)应该是可以扩展的,但是不可修改的

为什么需要OCP?

OCP相当于一种策略来帮助我们构建健壮的系统,使系统在面对需求的改变可以保持相对稳定,从而使系统在第一个版本以后可以不断推出新的版本。

OCP描述

遵循开放-封闭原则设计出的模块具有两个主要的特征。

  • 对于扩展是开放的(Open for extension)

    这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具备满足那些变化的新行为。换句话说,我们可以改变模块的功能。

  • 对于更改是封闭的(Closed for modification)

    对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者Java的.jar文件,都无需改动

OCP设计

那么如何在不改动源代码的情况下去修改它的行为呢?

关键是抽象,可以使用STRATEGY模式和Template Method模式对代码进行重构,这个可以放在以后再学习,先大概了解一下。

OCP缺点及解决办法

事实证明,无论模块是多么的封闭,都会存在一些无法封闭的变化。解决这些变化的办法就是依靠经验(大多数情况下都是错误的)。那么如何解决呢?

  • 出现了变化就创建抽象来隔离以后的同类变化
  • 刺激变化(使用测试驱动开发可以帮助我们)

结论

在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处(也就是,灵活性、可重用性、以及可维护性)。然而,并不是说只要使用一种面向对象语言就是遵循了这个原则。对于应用程序中的每个部分都肆意地进行抽象同样不是一个好主意。正确的做法是,开发人员应该仅仅对程序中呈现频繁变化的那些部分作出抽象。拒绝不成熟的抽象和抽象本身一样重要。

里氏替换原则(LSP)

派生类型必须能够替换掉他们的基类型

为什么需要LSP?

在面向对象中,最主要的一个特征是继承,继承提供了机制让派生类可以使用基类中的属性和方法,这是通过在基类型里封装通用的数据和行为来实现的。那么如何更好的使用继承就是LSP所解决的。具体原则如下:

如果对于每个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P行为功能不变,则S是T的子类型。

举例(长方形和正方形)

首先定义一个长方形,

class Rectangle
{
	//属性
	private width;
	private height;
	
	//方法
	public function getWidth();
	public function setWidth();
	public function getHeight();
	public function setHeight();
} 

假如说这个程序运行在很多地方。假如有一天,要求添加操作正方形的功能。那么正方形和长方形之间满足IS-A关系,那么按照我们经常说的,继承就是IS-A的关系,那么正方形继承长方形。那么问题来了

  1. 正方形的宽和高是一样的,那么宽改变的时候,高也可以改变。 当然可以通过修改长方形中的代码来解决,但是这样就违反了OCP原则。

  2. 就算解决了上面那个问题,假如有下面这个函数

     void g(Rectangle& r)
     {
     	r.setWidth(4);	
     	r.setHeight(5);
     	assert(r.Area() == 20);
     }
    

    那么很明显是错的。编写g和Rectangle类的人都没错,错的是Square类从Rectangle类继承。

由以上的这个例子来讲,Square和Rectangle这个显然合理的模型为什么会有问题?

对不是g函数的编写者来说,正方形可以是长方形,但是从g函数的编写者非角度来说,这个是错误的。因为对于函数g的编写者来说,Square和Rectangle的行为方式不同。对象的行为方式才是软件真正关注的问题。

LSP让我们得出了一个非常重要的结论:一个模型,如果孤立地看,并不具有真正意义上的有效性。模型的有效性只能通过他的客户程序来表现。

如何更好的使用LSP

更多的假设

在考虑一个模型的设计是否恰当的时候,不能完全独立的来看这个解决方案,必须要根据该设计的使用者所做出的合理假设来审视它。(又多了一个使用单元测试的理由)。但是大多数这样的假设都很难预测。通常的做法是,只预测那些明显的违反LSP的情况,其他的直到出现相关的脆弱性的时候再去处理。

基于契约设计(DBC)

这项技术可以解决“合理假设”的问题。具体来说,使用DBC,类的编写者显示的规定对该类的契约。客户代码的编写者可以通过该契约获得可以依赖的行为方式。契约是通过为每个方法声明的前置条件和后置条件来指定的。要使一个方法得以执行,前置条件必须要为真。执行完毕后,该方法要保证后置条件为真。

上一个例子,setWidth(double w)的后置条件可以看做

  1. width == w
  2. height == old.height

很显然,Square类并不能满足这个后置条件。DBC的前置条件和后置条件如下所示:

再重新声明派生类中的程序时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。

总结

  • LSP并不是真正的和继承有关,而是行为兼容性。(能用组合就尽量别用继承)

  • OCP是OOD中很多说法的核心。如果这个原则应用的有效,应用程序就会具有更多的可维护性、可重用性以及健壮性。LSP是使OCP成为可能的主要原则之一。正是子类型的可替换性才使得使用基类类型的模块在无须修改的情况下就可以扩展。这种可替换性必须是开发人员可以隐形依赖的东西。因此,如果没有显式地强制基类类型的契约,那么代码就必须良好地并且明显地表达出这一点。

  • 术语“IS-A”的含义过于宽泛以至于不能作为子类型的定义,子类型的正确定义应该是“可替换的”,这里的可替换性可以通过显式地或者隐式的契约来定义

依赖倒置原则(DIP) 写的很好,请点击

a. 高层模块不应该依赖于底层模块,二者都应该依赖于抽象。
b. 抽象不应该依赖于细节,细节应该依赖于抽象。

为什么要使用DIP

在上文说到了面向对象类关系,依赖关系是耦合性最强的。使用该原则可以达到解耦的目的。

什么是抽象

一个简单但非常有效的对于DIP的解释,是一个简单的启发式规则:“依赖于抽象”。这是一个简单的陈述,该启发式规则建议不应该依赖于具体类——也就是说,程序中的所有的依赖关系都应该终止于抽象类或者接口。依据这个启发式,可得:

  • 任何变量都不应该持有一个指向具体类的指针或者引用
  • 任何类都不应该从具体类中派生
  • 任何方法都不应该覆盖他的任何基类中已经实现了的方法

但是如果是稳定类的话,就相当于抽象,如PHP中的大多数类,抽象并不一定是形式上是抽象的,而是内容是抽象的。

举个例子

 <?php

//测试依赖倒置原则

Interface readInterface{
	public function read();
}

class readPaper implements readInterface{
	public function read(){
		echo "this is paper reader";
	}
}

class readTest implements readInterface{
	public function read(){
		echo "this is test reader";
	}
}

class person{
	public $read;
	public function __construct(readInterface $read){
		$this->read = $read;
	} 

	public function read(){
		$this->read->read();
	}
}

$tom = new person(new readTest());
$tom->read();

在这个例子中person类(调用类)依赖于抽象readInterface接口readPaper类readTest类依赖于抽象readInterface接口

总结

依赖倒置原则是实现许多面向对象技术所宣称的好处的基本底层机制。它的正确应用对于创建可重用的框架来说是必须的。同时他对于构建在变化面前富有弹性的代码也是非常重要的。由于抽象和细节被彼此隔离,所以代码也非常容易维护。

接口隔离原则(ISP)

不应该强迫客户依赖于它们不用的方法。

为什么使用ISP

这个原则用来处理“胖”接口所具有的缺点。接口A要同时被类B类C继承,如果接口A对于类C类B来说不是最小接口,则类B类C必须去实现他们不需要的方法。此时接口A显得太“胖”。

解决办法

可以使用委托模式或者将接口A进行分离,类B和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

总结 参考

说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。采用接口隔离原则对接口进行约束时,要注意以下几点:

  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

原文地址:https://www.cnblogs.com/qiye5757/p/9872148.html