OO大原则
系统软件的常见问题
1:僵化:牵一发而动全身,系统不可修改或扩展。
2:复杂或重复。过分复杂,难于理解。
3:不可复用,过于僵化而不可服用,不能剥离出独立的服用组件。
4:不够稳定。常常出错而又无法解决问题,系统运行不够可靠。
设计的原则:
降低耦合,来实现软件的复用和扩展,这正是设计原则的最终奥义。
随着面向对象的发展,形成了以封装,继承,多态为主的完整体系。继承了以抽象来封装变化,降低耦合实现复用的精髓。
而设计模式是对经验的总结与提炼,是对重复发生问题进行的总结和最佳解决策略的探索。
经典的5个设计原则:
1:单一职责原则
一个类,应该仅有一个引起它变化的原因。不要将变化原因不同的职责封装在一起,而应该分离。
2:开放封闭原则
软件实体,应该对外修改关闭,对外扩展开放。
3:依赖倒置原则
依赖于抽象,而不要依赖于具体,因为抽象相对稳定。
4:Liskov替换原则
子类必须能够替换其基类
5:合成/聚合复用原则
在新对象中聚合已有对象,使之成为新对象的成员,从而通过操作这些对象达到复用的目的,合成方式较继承方式耦合更松散。所以应该少继承,多聚合。
6:迪米特法则
又叫最少知识原则,指软件实体应该尽可能少的和其他软件实体发生作用。
单一职责原则的核心思想:
一个类最好只做一件事,只有一个引起它变化的原因。
单一职责原则可以看作是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,引起他变化的原因就越多,这将导致职责依赖,相互之间产生影响。从而极大的损伤其内聚性和耦合度。单一职责,通常意味着单一的功能,因此不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。
因此,SRP原则的核心就是要求对类的改变只能是一个,对于违反这一原则的类应该进行重构。
在以上设计中,DBManager类将对数据库的操作和用户权限的判别封装在一个类中实现,已添加记录为例。
public class DBManager { public void Add() { if (GetPermission(id) == "CanAdd") { Console.WriteLine("管理员可以增加数据。"); } } }
这显然是一个充满僵化味道的实现,如果权限设置的规则发生改变,那么必须修改所有的数据库操作逻辑。
重新设计的思路:
以Proxy模式调整之后,有效实现了职责的分离,DBManager类只关注数据操作和逻辑,而不用关系权限判断逻辑。
public class DBManager:IDBAction { public void Add() { } }
而将权限的判断交给DBManagerProxy代理类来完成。
public class DBManagerProxy : IDBAction { private IDBACtion dbManager; public DBManagerProxy(IDBAction dbAction) { dbManager = dbAction; } //处理权限判断的逻辑 public string GetPermission(string id) { //处理权限判断 } public void Add() { if (GetPermission(id) == "CanAdd") { dbManager.Add(); } } }
通过代理,将数据操作和权限判断两个职责分离,而实际的数据操作由DBManager来执行,此时客户端的调用就变得非常简单。
public class DBClient { public static void Main() { IDBAction DBManager = new DBManagerProxy(new DBManager("CanAdd")); DBManager.Add(); } }
开放封闭原则:
开放封闭原则(OCP)是面向对象原则的核心,软件设计本身的目标就是封装变化,降低耦合。
核心思想:
软件实体应该是可拓展,而不可修改的。也就是说,对拓展是开放的,而对修改是封闭的。
因此开放封闭原则主要体现在2个方面:
对外拓展开放,意味着有新的需求或变化时,可以对现有代码进行拓展,以适应新的情况。
对修改封闭,意味着一旦设计完成,就可以完成独立完成其工作,而不要对类进行任何修改。
需求总是变化。对软件设计者来说,必须在不需要原有系统进行修改的情况下,实现灵活的系统拓展,只有依赖于抽象。实现开放封闭的原则就是对抽象编程,而不是对具体编程,因为抽象相对稳定,而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以对拓展就是开放的,这是实施开放封闭原则的基本思路。
应用反思:
思路:银行处理员
class BusyBankStaff { private BankProcess bankProc = new BankProcess(); //定义银行业务员工的业务操作 public void HandleProcess(Client client) { switch (client.ClientType) { case "存款用户": bankProce.Deposit(); break; case "转款用户": bankProce.Transfer(); break; case "取款用户": bankProce.DrawMoney(); break; } }
将业务功能抽象为接口,当业务员依赖于固定的抽象时,对于修改就是封闭的,而通过继承和多态机制,从抽象体派生出新的拓展思路,就是对拓展的思路。
按照上述设计实现,用细节体现为:
interface IBankProcess { void Process(); } //按银行按业务进行分类 class DespositProcess : IBankProcess { public void Process() { //办理存款业务 } } class TransferProcess : IBankProcess { public void Process() { //办理转账业务 } } class DrawMoneyProcess : IBankProcess { public void Process() { //办理取款业务 } }
思路的转变,会让复杂的问题变得简单,使系统各负其责,人人实惠。有了上述的重构,银行工作人员变成一个彻底的EasyBankStaff.
class EasyBankStaff { private IBankProcess bankProc = null; public void HandleProcesss(ClientCertificateOption client) { //业务处理 bankProc = client.CreateProcess(); bankProc.Process(); } }
银行业务可以像这样就自动的实现了。
class BankProcess { public static void Main() { EasyBankStaff bankStaff = new EasyBankStaff(); bankStaff.HandleProcesss(new Client("转账用户")); } }
当有新的业务增加时,银行经理不必为重新组织业务流程而担心,只需为新增的业务实现IBankProcess接口,系统的其他部分丝毫不受影响。
对应的实现为:
class FundProcess : IBankProcess { public void Process() { //办理基金业务 } }
依赖倒置原则:
依赖倒置原则核心思想:
依赖于抽象。
具体而言:
高层模块不应该依赖于底层模块,两者都应该依赖于抽象。抽象不应该依赖于具体,具体应该依赖于抽象。
依赖,一定会存在于类与类、模块与模块之间。面向对象设计在某种层次上,就是一个关于关系处理的哲学,而依赖倒置正是这种哲学思想在具体应用中的体现。当两个模块之间存在紧耦合关系时,最好的方法就是分离接口和实现,使得高层调用接口的方法,底层模块实现接口的定义。
同时,业务员EasyBankStaff、业务IBankProcess和客户Client之间,明显违背了依赖倒置原则,业务员和业务类依赖于具体的客户,而非抽象。
CreateProcess在创建业务类别时,是必须依托于ClientType为判断条件的,从而也决定了HandleProcess的执行也受制于ClientType的条件时,我们必须从HandleProcess的处理过程了解这一点。
public class Program { public static void Main(string[] args) { EasyBankStaff bankStaff = new EasyBankStaff(); bankStaff.HandleProcess(new Client("转账用户")); } }
bankStaff处理HandleProcess的过程依赖于具体的Client客户,而当有新的业务类别增加时,系统中必须增加对客户类别的依赖,对于完美的设计来说,这种机制是僵化的,应该实现更好的解决方案。
需要找到潜在的对象,使EasyBankStaff依赖于抽象,而抽象的办法就是为EasyBankStaff和Client之间增加一个抽象接口。
具体的实现为:
interface IClient { IBankProcess CreateProcess(); } class DepositClient : IClient { IBankProcess IClient.CreateProcess() { return new DepositProcess(); } } class TransferClient : IClient { IBankProcess IClinet.CreateProcess() { return new TransferProcess(); } } class DrawMoneyClient : IClient { IBankProcess IClinet.CreateProcess() { return new DrawMoneyProcess(); } }
在客户端调用,不需要进行任何类别的判断,可以实现用户自动找到窗口的需求。
public class BankProcess { public static void Main() { EasyBankStaff bankStaff = new EasyBankStaff(); bankStaff.HandleProcess(new Client("转账用户")); } }
HandleProcess自行受理其业务,通过依赖于抽象,实现了Client对象的依赖倒置,Client不被依赖,可以实现更多的灵活性。当有新的业务类别增加,只需要实现Client接口。
class FundClient : IClinet { IBankProcess IClient.CreateProcess() { return new FundProcesss(); } }
抽象的稳定性决定了系统的稳定性,因为抽象是保持不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心思想。
依赖于抽象是一个通用的规则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一成不变的。
依赖于抽象,就是要对接口编程,不要对实现编程。
接口隔离原则:
核心思想:
使用多个小的专门的接口,而不要使用一个大的总接口。
具体实现:
接口应该是内聚的,应该避免出现"胖"接口。
一个类对另一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的接口,这是一种接口的污染。
接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离原则强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口所有的方法、属性等。在设计上,这是一种浪费,而且在实施上会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,这是一种灾难。
将胖接口分解为多个特定的定制化方法,使得客户端仅仅依赖于他们实际调用的方法,从而解除了客户端不会依赖于他们不用的方法。因此,按照客户需求将客户分组,并依赖这种分组来实现接口,是接口隔离的重要方法。分离的主要手段有两种:
委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统开销。
多重继承分离,通过接口多继承来实现客户需求,值得推荐。
应用反思:
IComputerUser是一个典型的"胖"接口,对于Aduit来说,他既需要工作,又需要娱乐,对于学生,只需要学习就行了,工作对他来说是浪费。
重新设计:
interface IComputerLearn { void ToLearn(); } interface IComputerWork { void ToWork(); } interface IComputerBeFun { void ToBeFun(); } class Aduit { private IComputerWork myWork(); private IComputerBeFun myFun; public void UseComputer { //主要是工作 myWork.ToWork(); //还可以娱乐 myFun.ToBeFun(); } } class Child { private IComputerLearn myLearn; public void UseComputer() { myLearn.ToLearn(); } }
建议:
将功能接近的接口合并,可能造成接口污染,实现内聚的接口才是接口设计的基本原则。
接口隔离原则,能够保证系统拓展和修改的影响不会拓展到系统的其他部分,
Liskov替换原则:
Liskov替换原则是关于继承机制的应用原则,是实现开放封闭原则的具体应用规范。
只有子类能够替换其基类时,才能保证系统在运行时期内识别子类,这是保证继承复用的基础。