设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
借用并改编一下鲁迅老师《故乡》中的一句话,一句话概括设计模式: 希望本无所谓有,无所谓无.这正如coding的设计模式,其实coding本没有设计模式,用的人多了,也便成了设计模式
六大原则
设计模式(面向对象)有六大原则:
- 开闭原则(Open Closed Principle,OCP)
- 里氏代换原则(Liskov Substitution Principle,LSP)
- 依赖倒转原则(Dependency Inversion Principle,DIP)
- 接口隔离原则(Interface Segregation Principle,ISP)
- 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
- 最小知识原则(Principle of Least Knowledge,PLK,也叫迪米特法则)
开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。 设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。
C# 开闭原则
1.概念:
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原(是“原”,指原来的代码)代码的情况下进行扩展。
2.模拟场景:
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
3.Solution:
当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
4.注意事项:
- 通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法
- 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
- 抽象层尽量保持稳定,一旦确定即不允许修改
5.开闭原则的优点:
- 可复用性
- 可维护性
6.开闭原则图解:
C# 里氏代换原则
1.概述: 派生类(子类)对象能够替换其基类(父类)对象被调用
2.概念:
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。(源自百度百科)
3.子类为什么可以替换父类的位置?:
当满足继承的时候,父类肯定存在非私有成员,子类肯定是得到了父类的这些非私有成员(假设,父类的的成员全部是私有的,那么子类没办法从父类继承任何成员,也就不存在继承的概念了)。既然子类继承了父类的这些非私有成员,那么父类对象也就可以在子类对象中调用这些非私有成员。所以,子类对象可以替换父类对象的位置。
4.C# 里氏代换原则优点:
需求变化时,只须继承,而别的东西不会改变。由于里氏代换原则才使得开放封闭成为可能。这样使得子类在父类无需修改的话就可以扩展。
5.C# 里氏代换原则Demo:
代码正文:
namespace TestApp { using System; class Program { static void Main(string[] args) { Transportation transportation = new Transportation(); transportation.Say(); Transportation sedan = new Sedan(); sedan.Say(); Console.ReadKey(); } } class Transportation { public Transportation() { Console.WriteLine( "Transportation?" ); } public virtual void Say() { Console.WriteLine( "121" ); } } class Sedan:Transportation { public Sedan() { Console.WriteLine( "Transportation:Sedan" ); } public override void Say() { Console.WriteLine( "Sedan" ); } } class Bicycles : Transportation { public Bicycles() { Console.WriteLine( "Transportation:Bicycles" ); } public override void Say() { Console.WriteLine( "Bicycles" ); } } } |
代码效果:
6.里氏代换原则图解:
C# 依赖倒转原则
1.概念:
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
2.C# 依赖倒转原则用处:
有些时候为了代码复用,一般会把常用的代码写成函数或类库。这样开发新项目时,直接用就行了。比如做项目时大多要访问数据库,所以我们就把访问数据库的代码写成了函数。每次做项目去调用这些函数。那么我们的问题来了。我们要做新项目时,发现业务逻辑的高层模块都是一样的,但客户却希望使用不同的数据库或存储住处方式,这时就出现麻烦了。我们希望能再次利用这些高层模块,但高层模块都是与低层的访问数据库绑定在一起,没办法复用这些高层模块。所以不管是高层模块和低层模块都应该依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个更改都不用担心了。
3.注意事项:
- 高层模块不应该依赖低层模块。两个都应该依赖抽象。
- 抽象不应该依赖结节。细节应该依赖抽象。
4.模拟场景:
场景:
假设现在需要一个Monitor工具,去运行一些已有的APP,自动化来完成我们的工作。Monitor工具需要启动这些已有的APP,并且写下Log。
代码实现1:
namespace TestLibrary.ExtensionsClass { using System; public class AppOne { public bool Start() { Console.WriteLine( "1号APP开始启动" ); return true ; } public bool ExportLog() { Console.WriteLine( "1号APP输出日志" ); return true ; } } public class AppTwo { public bool Start() { Console.WriteLine( "2号APP开始启动" ); return true ; } public bool ExportLog() { Console.WriteLine( "2号APP输出日志" ); return true ; } } public class Monitor { public enum AppNumber { AppOne=1, AppTwo=2 } private AppOne appOne = new AppOne(); private AppTwo appTwo = new AppTwo(); private AppNumber number; public Monitor(AppNumber number) { this .number = number; } public bool StartApp() { return number == AppNumber.AppOne ? appOne.Start() : appTwo.Start(); } public bool ExportAppLog() { return number == AppNumber.AppOne ? appOne.ExportLog() : appTwo.ExportLog(); } } } |
代码解析1:
在代码实现1中我们已经轻松实现了Monitor去运行已有APP并且写下LOG的需求。并且代码已经上线了.
春…夏…秋…冬…
春…夏…秋…冬…
春…夏…秋…冬…
就这样,三年过去了。
一天客户找上门了,公司业务扩展了,现在需要新加3个APP用Monitor自动化。这样我们就必须得改Monitor。
代码实现2:
namespace TestLibrary.ExtensionsClass { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; public class Monitor { public enum AppNumber { AppOne = 1, AppTwo = 2, AppThree = 3, AppFour = 4, AppFive = 5 } private AppOne appOne = new AppOne(); private AppTwo appTwo = new AppTwo(); private AppThree appThree = new AppThree(); private AppFour appFour = new AppFour(); private AppFive appFive = new AppFive(); private AppNumber number; public Monitor(AppNumber number) { this .number = number; } public bool StartApp() { bool result = false ; if (number == AppNumber.AppOne) { result = appOne.Start(); } else if (number == AppNumber.AppTwo) { result = appTwo.Start(); } else if (number == AppNumber.AppThree) { result = appThree.Start(); } else if (number == AppNumber.AppFour) { result = appFour.Start(); } else if (number == AppNumber.AppFive) { result = appFive.Start(); } return result; } public bool ExportAppLog() { bool result = false ; if (number == AppNumber.AppOne) { result = appOne.ExportLog(); } else if (number == AppNumber.AppTwo) { result = appTwo.ExportLog(); } else if (number == AppNumber.AppThree) { result = appThree.ExportLog(); } else if (number == AppNumber.AppFour) { result = appFour.ExportLog(); } else if (number == AppNumber.AppFive) { result = appFive.ExportLog(); } return result; } } } |
代码解析2:
这样会给系统添加新的相互依赖。并且随着时间和需求的推移,会有更多的APP需要用Monitor来监测,这个Monitor工具也会被越来越对的if…else撑爆炸,而且代码随着APP越多,越难维护。最终会导致Monitor走向灭亡(下线)。
介于这种情况,可以用Monitor这个模块来生成其它的程序,使得系统能够用在需要的APP上。OOD给我们提供了一种机制来实现这种“依赖倒置”。
代码实现3:
namespace TestLibrary.ExtensionsClass { using System; public interface IApp { bool Start(); bool ExportLog(); } public class AppOne : IApp { public bool Start() { Console.WriteLine( "1号APP开始启动" ); return true ; } public bool ExportLog() { Console.WriteLine( "1号APP输出日志" ); return true ; } } public class AppTwo : IApp { public bool Start() { Console.WriteLine( "2号APP开始启动" ); return true ; } public bool ExportLog() { Console.WriteLine( "2号APP输出日志" ); return true ; } } public class Monitor { private IApp iapp; public Monitor(IApp iapp) { this .iapp = iapp; } public bool StartApp() { return iapp.Start(); } public bool ExportAppLog() { return iapp.ExportLog(); } } } |
代码解析3:
现在Monitor依赖于IApp这个接口,而与具体实现的APP类没有关系,所以无论再怎么添加APP都不会影响到Monitor本身,只需要去添加一个实现IApp接口的APP类就可以了。
C# 接口隔离原则
1.概念:
客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
2.含义:
接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则。
3.模拟场景:
一个OA系统,外部只负责提交和撤回工作流,内部负责审核和驳回工作流。
4.代码演示:
namespace TestLibrary.ExtensionsClass { using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; public interface IReview { void ReviewWorkFlow(); void RejectWorkFlow(); } public class Review : IReview { public void ReviewWorkFlow() { Console.WriteLine( "开始审核工作流" ); } public void RejectWorkFlow() { Console.WriteLine( "已经驳回工作流" ); } } public interface ISubmit { void SubmitWorkFlow(); void CancelWorkFlow(); } public class Submit : ISubmit { public void SubmitWorkFlow() { Console.WriteLine( "开始提交工作流" ); } public void CancelWorkFlow() { Console.WriteLine( "已经撤销工作流" ); } } } |
5.代码解析:
其实接口隔离原则很好理解,在上面的例子里可以看出来,如果把OA的外部和内部都定义一个接口的话,那这个接口会很大,而且实现接口的类也会变得臃肿。
C# 合成/聚合复用原则
1.概念:
合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)经常又叫做合成复用原则。合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承。
2.合成/聚合解析:
聚合概念:
聚合用来表示“拥有”关系或者整体与部分的关系。代表部分的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,部分的生命周期可以超越整体。例如,Iphone5和IOS,当Iphone5删除后,IOS还能存在,IOS可以被Iphone6引用。
聚合关系UML类图:
C# 合成/聚合复用原则
代码演示:
namespace TestLibrary.ExtensionsClass { class IOS { } class Iphone5 { private IOS ios; public Iphone5(IOS ios) { this .ios = ios; } } } |
合成概念:
合成用来表示一种强得多的“拥有”关系。在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。使用程序语言的术语来说,合成而成的新对象对组成部分的内存分配、内存释放有绝对的责任。一个合成关系中的成分对象是不能与另一个合成关系共享的。一个成分对象在同一个时间内只能属于一个合成关系。如果一个合成关系湮灭了,那么所有的成分对象要么自己湮灭所有的成分对象(这种情况较为普遍)要么就得将这一责任交给别人(较为罕见)。例如:水和鱼的关系,当水没了,鱼也不可能独立存在。
合成关系UML类图:
代码演示:
namespace TestLibrary.ExtensionsClass { using System; class Fish { public Fish CreateFish() { Console.WriteLine( "一条小鱼儿" ); return new Fish(); } } class Water { private Fish fish; public Water() { fish = new Fish(); } public void CreateWater() { // 当创建了一个水的地方,那这个地方也得放点鱼进去 fish.CreateFish(); } } } |
3.模拟场景:
比如说我们先摇到号(这个比较困难)了,需要为自己买一辆车,如果4S店里的车默认的配置都是一样的。那么我们只要买车就会有这些配置,这时使用了继承关系:
不可能所有汽车的配置都是一样的,所以就有SUV和小轿车两种(只列举两种比较热门的车型),并且使用机动车对它们进行聚合使用。这时采用了合成/聚合的原则:
C# 迪米特法则
1.概念:
一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。
2.模拟场景:
场景:公司财务总监发出指令,让财务部门的人去统计公司已发公司的人数。
一个常态的编程:(肯定是不符LoD的反例)
UML类图:
代码演示:
namespace TestLibrary.ExtensionsClass { using System; using System.Collections.Generic; /// <summary> /// 财务总监 /// </summary> public class CFO { /// <summary> /// 财务总监发出指令,让财务部门统计已发工资人数 /// </summary> public void Directive(Finance finance) { List<Employee> employeeList = new List<Employee>(); // 初始化已发工资人数 for ( int i = 0; i < 500; i++) { employeeList.Add( new Employee()); } // 转告财务部门开始统计已结算公司的员工 finance.SettlementSalary(employeeList); } } /// <summary> /// 财务部 /// </summary> public class Finance { /// <summary> /// 统计已结算公司的员工 /// </summary> public void SettlementSalary(List<Employee> employeeList) { Console.WriteLine( string .Format( "已结算工资人数:{0}" , employeeList.Count)); } } /// <summary> /// 员工 /// </summary> public class Employee { } /// <summary> /// 主程序 /// </summary> public class Runner { public static void main(String[] args) { CFO cfo = new CFO(); // 财务总监发出指令 cfo.Directive( new Finance()); } } } |
根据模拟的场景:财务总监让财务部门总结已发工资的人数。 财务总监和员工是陌生关系(即总监不需要对员工执行任何操作)。根据上述UML图和代码解决办法显然可以看出,上述做法违背了LoD法则。
依据LoD法则解耦:(符合LoD的例子)
UML类图:
代码演示:
namespace TestLibrary.ExtensionsClass { using System; using System.Collections.Generic; /// <summary> /// 财务总监 /// </summary> public class CFO { /// <summary> /// 财务总监发出指令,让财务部门统计已发工资人数 /// </summary> public void Directive(Finance finance) { // 通知财务部门开始统计已结算公司的员工 finance.SettlementSalary(); } } /// <summary> /// 财务部 /// </summary> public class Finance { private List<Employee> employeeList; //传递公司已工资的人 public Finance(List<Employee> _employeeList) { this .employeeList = _employeeList; } /// <summary> /// 统计已结算公司的员工 /// </summary> public void SettlementSalary() { Console.WriteLine( string .Format( "已结算工资人数:{0}" , employeeList.Count)); } } /// <summary> /// 员工 /// </summary> public class Employee { } /// <summary> /// 主程序 /// </summary> public class Runner { public static void main(String[] args) { List<Employee> employeeList = new List<Employee>(); // 初始化已发工资人数 for ( int i = 0; i < 500; i++) { employeeList.Add( new Employee()); } CFO cfo = new CFO(); // 财务总监发出指令 cfo.Directive( new Finance(employeeList)); } } } |
根据LoD原则我们需要让财务总监和员工之间没有之间的联系。这样才是遵守了迪米特法则。
博客总结
想搞懂设计模式,必须先知道设计模式遵循的六大原则,无论是哪种设计模式都会遵循一种或者多种原则。这是面向对象不变的法则。本文针对的是设计模式(面向对象)主要的六大原则展开的讲解,并尽量做到结合实例和UML类图,帮助大家理解。在后续的博文中还会跟进一些设计模式的实例。