设计模式六大原则(1):单一职责原则
定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。
举个例子:有个动物呼吸类
public class Animal { private string _Name = null; public Animal(string name) { this._Name = name; } public void Breath() { Console.WriteLine("{0} 呼吸空气", this._Name); } }
public class Client{ public static void main(String[] args){ Animal animal = new Animal(); animal.Breath("鸡"); animal.Breath("鱼"); } }
运行结果:
鸡呼吸空气
鱼呼吸空气
程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,代码如下:
public class Fish
{
public void BreathFish() { Console.WriteLine("{0} 呼吸水", this._Name); } }
public class Chicken {
public void BreathChicken() { Console.WriteLine("{0} 呼吸空气", this._Name); }
}
这样做增加了类的成本,
public class Animal {
private string _Name = null; public Animal(string name) { this._Name = name; }
public void BreathFish() { Console.WriteLine("{0} 呼吸水", this._Name); } public void BreathChicken() { Console.WriteLine("{0} 呼吸空气", this._Name); } }
类的角度来说,违背单一职责
如果类的方法不多,逻辑简单,类级别可以违背单一职责
可以看到,这种修改方式要简单的多。但是却存在着隐患,以后如果增加了一些其他动物,则又要去增加一些方法,和一些类,其实我们很大一部分会这样操作
public void Breath() { if (this._Name.Equals("鱼")) { Console.WriteLine("{0} 呼吸水", this._Name); } else if (this._Name.Equals("鸡")) { Console.WriteLine("{0} 呼吸空气", this._Name); } else { throw new Exception(); } }
虽然这样违背了单一原则,但是如果逻辑够简单,方法级别可以违背单一职责。
例如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。
遵循单一职责原的优点有:
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则。
设计模式六大原则(2):里氏替换原则
肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑。其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。
定义:所有引用基类的地方必须能透明地使用其子类的对象。
其实关于里氏替换原则,在前面的章节中就有提及过,在这里就重温一遍。
#region abstract public abstract class ParentClass { /// <summary> /// CommonMethod /// </summary> public void CommonMethod() { Console.WriteLine("ParentClass CommonMethod"); } /// <summary> /// virtual 虚方法 必须包含实现 但是可以被重载 /// </summary> public virtual void VirtualMethod() { Console.WriteLine("ParentClass VirtualMethod"); } } public class ChildClass : ParentClass { /// <summary> /// new 隐藏 /// </summary> public new void CommonMethod() { Console.WriteLine("ChildClass CommonMethod"); } public override void VirtualMethod() { Console.WriteLine("ChildClass VirtualMethod"); } } #endregion abstract
public static void Test() { Console.WriteLine("*******************************************"); ParentClass instance = new ChildClass(); Console.WriteLine("下面是instance.CommonMethod()"); instance.CommonMethod(); Console.WriteLine("下面是instance.VirtualMethod()"); instance.VirtualMethod(); Console.WriteLine("*******************************************"); }
通过调用结果发现,普通方法调用的是父类,虚方法调用子类。
有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的
设计模式六大原则(3):依赖倒置原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
例子:假设张三一开始会使用Nokia手机,手机功能又打电话发短信。
public class Nokia { public void Call() { Console.WriteLine("User {0} Call", this.GetType().Name); } public void Text() { Console.WriteLine("User {0} Call", this.GetType().Name); } }
public class Student { public int Id { get; set; } public string Name { get; set; } public void PlayNokia(Nokia phone) { Console.WriteLine("这里是{0}", this.Name); phone.Call(); phone.Text(); } }
但是后面手机品种增加,出现了iPhone,honor等手机,张三都会使用。那我们是不是又要新增几个方法。
public void PlayiPhone(iPhone phone) { Console.WriteLine("这里是{0}", this.Name); phone.Call(); phone.Text(); } public void PlayHonor(Honor phone) { Console.WriteLine("这里是{0}", this.Name); phone.Call(); phone.Text(); }
这样是不是他麻烦了,没增加一款手机都会增加一个方法,对于程序来说这样做太繁冗了。
这个时候增加一个抽象类。使这些手机款式依赖于抽象类
public abstract class AbstractPhone { public abstract void Call(); public abstract void Text(); }
这样不管增加多少款手机,方法都不会增加
public void PlayPhone(AbstractPhone phone) { Console.WriteLine("这里是{0}", this.Name); phone.Call(); phone.Text(); }
上端程序只要调用这一个方法就可以代替其他方法,只有手机类依赖了这个抽象类。
在实际编程中,我们一般需要做到如下3点:
- 低层模块尽量都要有抽象类或接口,或者两者都有。
- 变量的声明类型尽量是抽象类或接口。
- 使用继承时遵循里氏替换原则。
依赖倒置原则的核心就是要我们面向接口、抽象编程,理解了面向接口、抽象编程,也就理解了依赖倒置
设计模式六大原则(4):接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
例子,还是手机这个类,定义了一个接口,一款手机又许多功能
public interface IExtend //: IExtendAdvanced { void Photo(); void Online(); void Game(); void Record(); void Movie(); void Map(); void Pay(); }
public class iPhone : IExtend public override void Call() { Console.WriteLine("User {0} Call", this.GetType().Name); } public override void Text() { Console.WriteLine("User {0} Call", this.GetType().Name); } public void Photo() { Console.WriteLine("User {0} Photo", this.GetType().Name); } public void Online() { Console.WriteLine("User {0} Online", this.GetType().Name); } public void Game() { Console.WriteLine("User {0} Game", this.GetType().Name); } public void Map() { Console.WriteLine("User {0} Map", this.GetType().Name); } public void Pay() { Console.WriteLine("User {0} Pay", this.GetType().Name); } public void Record() { Console.WriteLine("User {0} Record", this.GetType().Name); } public void Movie() { Console.WriteLine("User {0} Movie", this.GetType().Name); } }
但是像以前的Nokia就不支持支付功能,但是他也继承了这个功能,这就会出现问题了,
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口进行拆分。在这里我们将原有的接口拆分
public interface IExtend { void Photo(); void Online(); void Game(); void Record(); void Movie(); } public interface IExtendAdvanced { void Map(); void Pay(); }
接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。
采用接口隔离原则对接口进行约束时,要注意以下几点:
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
未完待续。。。