设计模式六大原则
设计模式六大原则:
1、单一职责原则(single responsibility principle)
2、历史替换原则(liskov substitution principle)
3、依赖倒置原则(dependence inversion principle)
4、接口隔离原则(interface segregation principle)
5、迪米特法则(law of demeter)//最少知道原则
6、开闭原则(open closed principle)
设计模式:
面向对象语言开发过程中,遇到种种的场景和问题,提出的解决方案和思路,沉淀下来,设计模式是解决具体问题的套路
设计模式六大原则:
面向对象语言开发过程中,推荐的一些指导性原则;没有明确的招数,而且也会经常被忽视/违背;也是前辈总结,也是为了站在前辈的肩膀上。
实际上真实项目很难全部遵循,更多的时候会有一些侧重性。设计模式六大原则要能灵活运用,离不开时间的锤炼和思考,把这个真的融入到骨子里,设计确实会不一样。
1、单一职责(single responsibility principle)
类t负责两个不同的职责,职责p1和职责p2。由于职责p1需求发生改变需要修改t时,有可能会导致原本运行正常 职责p2功能发生故障。
一个类只负责一件事,面向对象语言开发,类是一个基本单位,单一职责原则就是封装的粒度。
下面我们来看一个动物类:
public class animal { private string _name = null; public animal(string name) { this._name = name; } /// <summary> /// 这个方法就挺不稳定,经常各种分支变化经常修改 /// </summary> public void breath() { if (this._name.equals("鸡")) console.writeline($"{this._name} 呼吸空气"); else if (this._name.equals("牛")) console.writeline($"{this._name} 呼吸空气"); else if (this._name.equals("鱼")) console.writeline($"{this._name} 呼吸水"); else if (this._name.equals("蚯蚓")) console.writeline($"{this._name} 呼吸泥土"); } //breathchicken breathfish //应该拆分了 public void action() { if (this._name.equals("鸡")) console.writeline($"{this._name} flying"); else if (this._name.equals("牛")) console.writeline($"{this._name} walking"); else if (this._name.equals("鱼")) console.writeline($"{this._name} swimming"); else if (this._name.equals("蚯蚓")) console.writeline($"{this._name} crawling"); } }
这样在不同动物过来时,只要在animal的构造函数中传递动物的名字即可:
{ animal animal = new animal("鸡");//呼吸空气 animal.breath(); animal.action(); } { animal animal = new animal("牛");//呼吸空气 animal.breath(); animal.action(); } { animal animal = new animal("鱼");//呼吸水 animal.breath(); animal.action(); }
但是这样不好,写了分支判断,然后执行不同的逻辑,其实这就违背了单一职责,但是功能是可以实现的。
需要拆分,拆分父类+子类,每个类很简单,意味着稳定,意味着强大。现在的东西没有以前警用,因为功能多了,这不坏就那坏了。
下面抽象出一个父类:
public abstract class abstractanimal { protected string _name = null; public abstractanimal(string name) { this._name = name; } public abstract void breath(); public abstract void action(); }
现在,有一种动物,只要继承这个父类,重写抽象类中的方法即可:
public class chicken : abstractanimal { public chicken(string name) : base(name) { } public chicken() : base("鸡") { } public override void breath() { console.writeline($"{base._name} 呼吸空气"); } public override void action() { console.writeline($"{base._name} flying"); } }
abstractanimal animal = new chicken(); animal.breath(); animal.action();
拆分之后,也会造成代码量的增加,类多了,使用成本(理解成本)也高了。
那么,什么时候使用单一职责呢?
如果类型足够简单,方法够少,是可以在类级别去违背单一职责的;如果类型复杂了,方法多了,建议遵循单一职责原则。
方法级别的单一职责原则:一个方法只负责一件事
类级别的单一职责原则:一个类只负责一件事
类库级别的单一职责原则:一个类库职责要清晰
项目级别的单一职责:一个项目应该职责要清晰(客户端/管理后台/后台服务/定时任务/分布式引擎)
系统级别的单一职责:为通用功能拆分系统(ip定位/日志/在线统计)
2、里氏替换原则(liskov substitution principle)
任何使用基类的地方,都可以透明(安全,不会出现行为不一致)的使用其子类
继承:子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替
a、父类有的,子类必须有,如果父类中出现了子类不应该有的东西,那么就应该断掉继承,再来一个父类,包含了都有的东西。
b、子类可以有自己的属性和行为,子类出现的地方,父类不一定能代替(白马非马)。
c、父类实现的东西,子类就不要再写了(就是不要new隐藏),有时候会出现意想不到的地方,行为不一致,如果想修改父类的行为,通过abstract或者virtual。声明属性、字段、变量,尽量声明为父类。
3、依赖倒置原则(dependence inversion principle)
高层模块不应该依赖于低层模块,二者应该是通过抽象依赖,依赖抽象,而不是依赖细节。面向对象,尽量使用抽象,80%的设计模式都是跟抽象有关(接口也可以)。属性、字段、方法参数、返回值。。。。尽量都是抽象
抽象:接口/抽象类---可以包含没有实现的元素
细节:普通类(子类)---一切都是确定的
student-------使用--------手机
高层--------------------------低层
就好像三层中:
bll---------------------------dal
如果在一开始,每个手机都定义通用的方法,学生在调用的时候,正常去调用,会出现高层依赖低层
//student-使用-手机 //高层---------底层 { iphone phone = new iphone(); student.playiphone(phone); student.playt(phone); student.play(phone); } { lumia phone = new lumia(); student.playlumia(phone); student.playt(phone); student.play(phone); } { honor phone = new honor(); student.playhonor(phone); student.playt(phone); student.play(phone); }
解决上面出现的问题,定义一个手机的抽象父类,里面定义手机通用的功能。
/// <summary> /// 定义一个手机抽象父类 /// </summary> public abstract class abstractphone { public int id { get; set; } public string branch { get; set; } public abstract void call(); public abstract void text(); }
/// <summary> /// 荣耀手机 /// </summary> public class honor : abstractphone { public override void call() { console.writeline("user {0} call", this.gettype().name); } public override void text() { console.writeline("user {0} call", this.gettype().name); } }
/// <summary> /// iphone /// </summary> public class iphone : abstractphone { public override void call() { console.writeline("user {0} call", this.gettype().name); } public override void text() { console.writeline("user {0} call", this.gettype().name); } }
/// <summary> /// 小米手机 /// </summary> public class mi : abstractphone { public override void call() { console.writeline("user {0} call", this.gettype().name); } public override void text() { console.writeline("user {0} text", this.gettype().name); } public void bracelet() { console.writeline("user {0} bracelet", this.gettype().name); } }
......只要是一部手机,只要继承了抽象父类,都可以重写父类中的抽象方法,实现通用的功能,也可以加上自己特有的功能(增加自己的功能,是不是很强大?后面就看出来了是否合理了)。
下面有两种方式去调用:泛型+父类约束=========》用抽象类,是等价的
public void playt<t>(t phone) where t : abstractphone { console.writeline("这里是{0}", this.name); phone.call(); phone.text(); }
public void play(abstractphone phone) { console.writeline("这里是{0}", this.name); phone.call(); phone.text(); //phone.bracelet(); }
面向抽象有啥好处?
一个方法既满足不同类型的参数,只要是实现了这个抽象类,都可以用;还支持扩展,不用修改student类;面向抽象后,不能使用子类特别的内容。
在调用的时候,studen.play(phone);如果,这个phone是一个小mi手机的子类呢?bracelet(手环功能)是有的,但是方法不能用,编译器决定了是不能用bracelet(dynamic/反射是可以调用的),不能常规调用,这个问题是解决不了的。
你这样想,如果小米手机都是自己特有的功能,那还用抽象父类干嘛?直接小米手机自己搞一个不就行了?因为面向对象不止一个类型,用的就是通用功能;非通用功能,那就不应该面向对象了。
面向对象语言开发,只要抽象不变,高层就不变。
面向对象语言开发,就是类与类之间进行交互,如果高层直接依赖低层的细节,细节是多变的,那么低层的变化就导致上层的变化;如果层数多了,低层的修改会直接水波效应传递到最上层,一点细微的改动,都会导致整个系统从下往上的修改(这就是大家经常加班的原因)。
面向抽象,如果高层和低层没有直接依赖,而是依赖于抽象,抽象一般是稳定的,低层细节的变化扩展就不会影响到高层,这样就能支持层内部的横向扩展,不会影响到其他地方,这样的程序架构就是稳定的。
依赖倒置原则(理论基础)------ioc控制反转(实践封装)-----di依赖注入(实现ioc的手段)。
4、接口隔离原则(interface segregation principle)
客户端不应该依赖它不需要的接口,一个类对另外一个类的依赖应该建立在最小的接口上,在工作中,80%都是用接口的。
abstractphone定义了id、branch、call()、text(),现在的智能手机,都有map、movie、online、game等功能,是否应该把这几个功能上升到abstractphone里面去?
答案是不应该的,oldmanphone也是手机,但是没有这些功能,abstractphone就只能放入任何手机都有的功能。
虽然这些东西不适合放在抽象类中,但是面向抽象编程,还有一个接口。抽象类 is a,接口 can do,面向对象编程中,是单继承多实现的。将这些东西定义在接口中,不局限产品。
接口是什么?
简单来说,接口就是,当一些东西都有相同的功能,但是有不能抽象出合理的父类的时候,这个时候就可以抽象为接口。
public interface iextend { void photo(); void online(); void game(); void record(); void movie(); void map();// void pay(); }//都拆成一个方法一个接口
camera能拍照,也能录像,既然面向抽象,那么有些功能的对象都能传递进来,那就让camera也去实现iextend接口?
不可以的,实现了iextend接口,camera出现了很多自己没有的功能,不应该使用这种大而全的接口,所以要把接口的功能进一步拆分,因为接口是可以多实现的。
public interface iextend { //void photo(); //void online(); //void game(); void record(); //void movie(); void map();// void pay(); }//都拆成一个方法一个接口 //电视--上网 玩游戏 public interface iextendhappy : iextendgame { void online(); //void game(); } //掌中游戏机:俄罗斯方块--玩游戏不能上网 public interface iextendgame { void game(); } public interface iextendvideo { void photo(); void movie();//打开相机--切换模式--start--suspend--end }
这样拆分下去,都拆成一个方法一个接口了,肯定也不好。
在.net中,有ilist<t>,是索引相关;icollection<t>,集合相关操作;ienumerable<t>,迭代器foreach....
接口到底该怎么定义?
a、既不能是大而全,会强迫实现没有的东西,也会依赖自己不需要的东西
b、也不能一个方法一个接口,这样,面向抽象也没意义了。
按照功能的密不可分可定义接口,而且应该是动态的,随着业务发展会有变化的,但是在设计的时候,要留好提前量,避免抽象变化。这里没有标准答案,随着业务来调整的。
c、接口合并,map----定位/导航/搜索,这种就属于固定步骤,业务细节,尽量的内聚,在接口也不要暴露太多细节。
5、迪米特法则(law of demeter)最少知道原则:高内聚低耦合
一个对象应该对其他对象保持最少的了解,只与直接的朋友通信。
面向对象----万物皆对象-----类与类交互才能产生功能,这不就是耦合了吗?要高内聚低耦合
类与类之间的关系:
纵向:继承≈实现(最密切)
横向:聚合>组合>包含>依赖(出现在方法内部)
迪米特法则:降低类与类之间的耦合,只与直接的朋友通信,就是要尽量避免依赖更多类型。
基类库(bcl-系统/框架内置)的类型除外
迪米特,也会增加一些成本
工作中,会去早一个中介(中间层)
上层ui下订单-----订单系统&支付&支付系统&仓储&物流
门面模式----上层交互门面----门面依赖子系统
三层架构中:ui----bll-----dal
去掉内部依赖
降低访问修饰符权限,private、protected、internal、protected internal、public
迪米特,依赖别人更少,让别人了解更少
/// <summary> /// 学生 /// </summary> public class student { public int id { get; set; } public string studentname { get; set; } public int height { private get; set; } public int salay; public void managestudent() { console.writeline(" {0}manage {1} ", this.gettype().name, this.studentname); } }
/// <summary> /// 班级 /// </summary> public class class { public int id { get; set; } public string classname { get; set; } public list<student> studentlist { get; set; } public void manageclass() { console.writeline(" {0}manage {1} ", this.gettype().name, this.classname); foreach (student s in this.studentlist) { s.managestudent(); //console.writeline(" {0}manage {1} ", s.gettype().name, s.studentname); } } }
/// 学校 /// </summary> public class school { public int id { get; set; } public string schoolname { get; set; } public list<class> classlist { get; set; } public void manage() { console.writeline("manage {0}", this.gettype().name); foreach (class c in this.classlist) { //console.writeline(" {0}manage {1} ", c.gettype().name, c.classname); c.manageclass();//1 遵循了迪米特 //list<student> studentlist = c.studentlist; //foreach (student s in studentlist) //{ // console.writeline(" {0}manage {1} ", s.gettype().name, s.studentname); //}//2 违背了迪米特法则 } } }
6、开闭原则(open closed principle)
对扩展开放,对修改关闭
修改:修改现有代码(类)
扩展:增加代码(类)
面向对象语言是一种静态语言,最害怕变化,会波及很多东西,全面测试。嘴里将就是新增类,对原有代码没有改动,原有的代码才是可信的。
开闭原则只是一个目标,并没有任何的手段,也别成为总则。其他5个原则的建议,就是为了更好的做到ocp。开闭原则也是面向对象语言开发的一个终极目标。
如果有功能增加/修改的需求,那么就修改现有的方法---增加方法---增加类----增加/替换类库。