1、合成/聚合复用原则(CARP)
1、定义
简而言之,对于合成/聚合复用原则的定义就是:要尽量使用合成和聚合,尽量不要使用继承。
2、释义
为什么“要尽量使用合成和聚合,尽量不要使用继承”呢?
这是因为:
第一,继承复用破坏包装,它把父类的实现细节直接暴露给了子类,这违背了信息隐藏的原则;
第二:如果父类发生了改变,那么子类也要发生相应的改变,这就直接导致了类与类之间的高耦合,不利于类的扩展、复用、维护等,也带来了系统僵硬和脆弱的设计。而用合成和聚合的时候新对象和已有对象的交互往往是通过接口或者抽象类进行的,就可以很好的避免上面的不足,而且这也可以让每一个新的类专注于实现自己的任务,符合单一职责原则。
3、何时使用合成/聚合、继承
两种判断方法:
1)、使用“Has-A”和“Is-A”来判断
“Is-A”代表一个类是另外一个类的一种,可以使用继承关系,如下图
而“Has-A”代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。如下图
2)、使用里氏代换原则来判断
里氏代换原则是继承复用的基础。
具体介绍:设计模式六大原则——里氏代换原则
2、迪米特法则(LoD)
摘要: 1、背景 在图书馆借书,刚开始的时候,直接跑到相应的楼层去,到里面去转,去找要借的书,在里面溜达半天才能找到;后来知道图书馆有一个电脑查询处,然后
3、里氏替换原则(LSP)
概述
里氏替换原则(LSP,Liskov Substitution Principle)是关于继承机制的原则,是实现开放封闭原则的具体规范,违反了里氏替换原则必然违反了开放封闭原则。
引经据典
约瑟夫.斯大林,苏联时期苏联共产党的最高领导人,对于斯大林有没有替身?有几个替身?有一种说法:斯大林有好几个替身,最著名的当属“第一替身”叶夫谢伊.卢比茨基——他“扮演”领袖斯大林长达15年之久,很多高官都被蒙在鼓里。
斯大林作为一国的领导人,肯定要不停的参加各种的聚会,进行各中慰问。。。找几个替身,忙里偷闲或者外出打掩护......
子类必须完全实现父类的方法
替身代替斯大林去参加各种活动时,他的相貌和一举一动都必须和真的斯大林一模一样,毕竟斯大林代表的是一个国家,如果让别人发现来见自己的是个替身,那不就让人寒心了嘛... 看一下代码:
//斯大林
public class SiDaLin
{
//斯大林讲话行为
public virtual void SpeakWay()
{
Console .WriteLine ("右手举过头顶、伸出两个手指、不断前后摆动,以表示强调或愤慨。")
}
}
//替身
public class TiShen : SiDaLin
{
//替身的讲话行为
public override void SpeakWay()
{
Console .WriteLine ("右手举过头顶、伸出两个手指、不断前后摆动,以表示强调或愤慨。")
}
}
不知道大家有没有注意到上面代码中父类中的virtual和子类中的override,为什么要用到它们?
在继承关系中,子类对父类的继承除了字段、属性,还有方法,而使同一方法在子类中表现出不同的行为是通过多态表现的,具体在语言上的操作上表现为父类提供虚函数,而在子类中覆写该虚函数,这是抽象机制的重要基础。
public static void DoSomething(Fatherclass f)
{
f.Method();
}
如果Method被实现为虚函数,并且在子类中被覆写,那么传入DoSomething中的实参既可以是父类,也可以是子类,子类完全可以替代父类在此调用自己的方法,这正是里氏替换原则强调的继承关系,代码如下:
class Fatherclass
{
//父类行为
public virtual void Method()
{
}
}
class Sonclass :Fatherclass
{
//子类重写父类方法
public override void Method()
{
}
}
(上面这个问题的回答参考《你必须知道的.NET》)
子类可以有自己的“个性”
采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。
就拿斯大林的替身来说,每一个替身都是一个人,都有自己的“个性”,但是他们是斯大林的替身,那么他们平时的表现就必须按照斯大林的表现来,如果他们加入自己的“个性”,那么他们替身的身份就会漏出破绽。
结论
实现开闭原则的关键步骤是抽象化,父类与子类之间的继承关系就是抽象化的体现,因此里氏替换原则是实现开闭原则的具体步骤规范,违反里氏替换原则一定违反开闭原则,反之未必。
4、依赖倒置原则(DIP)
定义
依赖倒置原则(Dependency Inversion Principle)
核心思想:依赖于抽象
具体体现:
体现一:高层模块不应该依赖低层模块。两个都应该依赖抽象。
体现二:抽象不应该依赖细节。细节应该依赖抽象。
依赖倒置原则告诉我们:细节是多变的,而抽象是相对稳定的。所以我们编程的时候要注重抽象的编程,而非细节编程。
实例
1、AGP插槽:主板和显卡之间的关系的抽象。主板和显卡通常是使用AGP插槽来连接的,这样,只要接口适配,不管是主板还是显卡更换,都不是问题。
2、驾照:司机和汽车之间关系的抽象。有驾照的司机可以驾驶各种汽车。
3、电源插座。
下面我们用一个灯的开关的实例来展示:
Switch(开关)、Light(灯),统一一个接口IDevices,让Switch和light都继承这个接口,而且这个接口有两个方法,即开/关,代码如下:
/// <summary>
/// 设备
/// </summary>
public interface IDevices
{
void TurnOn();
void TurnOff();
}
/// <summary>
/// 电灯
/// </summary>
public class Light : IDevices
{
public void TurnOn()
{
Console.WriteLine("灯泡亮了");
}
public void TurnOff()
{
Console.WriteLine("灯泡黑了");
}
}
/// <summary>
/// 开关
/// </summary>
public class Switch:IDevices
{
IDevices devices;
public Switch (IDevices devices)
{
this.devices =devices ;
}
public void TurnOn()
{
Console.WriteLine("打开开关");
devices.TurnOn();
}
public void TurnOff()
{
Console.WriteLine("关上开关");
devices.TurnOff();
}
}
class Program
{
//调用
static void Main(string[] args)
{
IDevices _switch = new Switch(new Light());
_switch.TurnOn();
_switch.TurnOff();
Console.Read();
}
}
建议
1、抽象的稳定性决定了系统的稳定性,因为抽象是保持不变的,依赖于抽象是面向对象程序设计的精髓,也是依赖倒置的核心思想。
2、依赖于抽象是一个通用的规则,而某些依赖于细节在所难免的,必须权衡抽象和具体的取舍,方法不是一成不变的。
3、依赖于抽象就是要对接口编程,不要对实现编程。
5、开放封闭原则(OCP)
什么是开闭原则?
定义:是说软件实体(类、模块、函数等等)应该可以扩展,但是不可修改。
开闭原则主要体现在两个方面:
1、对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
2、对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类尽任何修改。
怎么使用开闭原则?
实现开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以对修改就是封闭的;而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。
对于违反这一原则的类,必须通过重构来进行改善。常用于实现的设计模式主要有Template Method模式和Strategy 模式。而封装变化,是实现这一原则的重要手段,将经常变化的状态封装为一个类。
以银行业务员为例 :
没有实现OCP设计的:
public class BankProcess
{
public void Deposite(){} //存款
public void Withdraw(){} //取款
public void Transfer(){} //转账
}
public class BankStaff
{
private BankProcess bankpro = new BankProcess();
public void BankHandle(Client client)
{
switch (client .Type)
{
case "deposite": //存款
bankpro.Deposite();
break;
case "withdraw": //取款
bankpro.Withdraw();
break;
case "transfer": //转账
bankpro.Transfer();
break;
}
}
}
这种设计显然是存在问题的,目前设计中就只有存款,取款和转账三个功能,将来如果业务增加了,比如增加申购基金功能,理财功能等,就必须要修改BankProcess业务类。我们分析上述设计就能发现不能把业务封装在一个类里面,违反单一职责原则,而有新的需求发生,必须修改现有代码则违反了开放封闭原则。
如何才能实现耦合度和灵活性兼得呢?
那就是抽象,将业务功能抽象为接口,当业务员依赖于固定的抽象时,对修改就是封闭的,而通过继承和多态继承,从抽象体中扩展出新的实现,就是对扩展的开放。
一下是符合OCP的设计:
//首先声明一个业务处理接口
public interface IBankProcess
{
void Process();
}
public class DeposiProcess:IBankProcess
{
public void Process() //办理存款业务
{
Console.WriteLine("Process Deposit");
}
}
public class WithDrawProcess:IBankProcess
{
public void Process() //办理取款业务
{
Console.WriteLine("Process WithDraw");
}
}
public class TransferProcess:IBankProcess
{
public void Process() //办理转账业务
{
Console .WriteLine ("Process Transfer");
}
}
public class BankStaff
{
private IBankProcess bankpro = null ;
public void BankHandle(Client client)
{
switch (client .Type)
{
case "Deposite": //存款
userProc =new WithDrawUser();
break;
case "WithDraw": //取款
userProc =new WithDrawUser();
break;
case "Transfer": //转账
userProc =new WithDrawUser();
break;
}
userProc.Process();
}
}
这样当业务变更时,只需要修改对应的业务实现类就可以,其他不相干的业务就不必修改。当业务增加,只需要增加业务的实现就可以了。
6、单一职责原则(SRP)
定义
就一个类而言,应该仅有一个引起它变化的原因。通俗的说,一个类只负责一项职责。
问题的由来
手机的功能多,但是每一项的功能都不强:
拍摄功能——>专业的摄像机和照相机
手机游戏——>PSP
网络摄像头——>专业摄像头
GPS功能——>专业GPS导航系统
每一个职责都是一个变化的轴线,当需求变化时会反映为类的职责的变化,如果一个类的承担的职责多于一个,那么引起她变化的原因就有多个,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力,从而导致脆弱的设计。
解决方案
遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
示例
public interface Program
{
void draw(); //绘制图形
void area(); //计算面积
}
public class DrawGraph implements Program
{
public void draw()
{
System .Out.PrintIn("绘制图形");
}
public void area(){}
}
public class AreaCount implements Program
{
public void draw(){}
public void area()
{
System .out.printIn("计算面积");
}
}
}
以上 图形计算程序只使用了正方形的Area()方法,永远不会使用Draw()方法,而它却跟Draw方法关联了起来。这违反了单一原则,如果未来因为图形绘制程序导致Draw()方法产生了变化,那么就会影响到本来毫不关系的图形计算程序。
应该把接口改成2个,将不同的职责分配给不同的类,使单个类的职责尽量单一,就隔离了变化,这样他们也不会互相影响了。
public interface Draw
{
void draw(); //绘制图形
}
public interface Area
{
void area(); //计算面积
}
然后分别实现接口,如下: