【学习笔记】设计模式六大原则之单一职责原则、里氏替换原则和迪米特法则
进入主题前我们来看下什么是设计模式
设计模式:面向对象语言开发过程中,遇到各种各样的场景和问题,提出的解决方案和思路,沉淀下来。设计模式是解决具体问题的套路。
设计模式六大原则:面向对象语言开发过程中,推荐的一些指导性原则。没有明确的招数,而且也经常会被忽视/违背。
一、单一职责原则(Single Responsibility Principle)
首先我们来看个示例:
/// <summary> /// 动物类 /// </summary> 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} 呼吸泥土"); } /// <summary> /// 运动 /// </summary> 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类呢,拆分后使每种动物都职责单一。下面来看下如何拆分。
/// <summary> /// 动物抽象类 /// </summary> public abstract class AbstractAnimal { protected string _name = null; public AbstractAnimal(string name) { this._name = name; } public abstract void Breath(); public abstract void Action(); } /// <summary> /// 鸡 /// </summary> public class Chicken : AbstractAnimal { public Chicken() : base("鸡") { } public override void Breath() { Console.WriteLine($"{base._name} 呼吸空气"); } public override void Action() { Console.WriteLine($"{base._name} flying"); } } /// <summary> /// 鱼 /// </summary> public class Fish : AbstractAnimal { public Fish() : base("鱼") { } public override void Breath() { Console.WriteLine($"{base._name} 呼吸水"); } public override void Action() { Console.WriteLine($"{base._name} swimming"); } }
经过这样拆分后我们的类简单了,职责也就单一了,简单意味着稳定。当然拆分后也会造成代码量的增加,类多了,使用成本也高(理解成本)。
那么我们究竟该什么时候使用单一职责原则呢?
如果类型足够简单,方法够少,是可以在类级别去违背单一职责。如果类型复杂了,方法多了,建议遵循单一职责原则。
小结:
1、单一职责原则:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
2、一个类只负责一件事,面向对象语言开发,类是一个基本单位,单一职责原则就是封装的粒度。
3、写分支判断,然后执行不同的逻辑,其实这就违背了单一职责原则,但是功能是可以实现的。
4、方法级别的单一职责原则:一个方法只负责一件事(职责分拆小方法,分支逻辑分拆)。
5、类级别的单一职责原则:一个类只负责一件事。
6、类库级别的单一职责原则:一个类库应该职责清晰。
7、项目级别的单一职责原则:一个项目应该职责清晰(客户端/管理后台/后台服务/定时任务/分布式引擎)。
8、系统级别的单一职责原则:为通用功能拆分系统(IP定位/日志/在线统计)。
二、里氏替换原则(Liskov Substitution Principle)
下面我们来看个示例:
public class People { public int Id { get; set; } public string Name { get; set; } //传统 public void Traditional() { Console.WriteLine("仁义礼智信 温良恭俭让 "); } } public class Chinese : People { public string Kuaizi { get; set; } public void SayHi() { Console.WriteLine("早上好,吃了吗?"); } } public class Hubei : Chinese { public string Majiang { get; set; } public new void SayHi() { Console.WriteLine("早上好,过早了么?"); } } //这就违背了里氏替换原则,因为父类中出现了子类中没有的行为,那么就应该断掉继承。 public class Japanese : People { //Traditional也会继承 但是Japanese又没有Traditional public void Ninja() { Console.WriteLine("忍者精神 "); } }
从示例中可以看出Japanese类继承People类,但是基类People中有子类Japanese类没有的行为Traditional,这就违背了里氏替换原则,此时就应该断掉继承关系,可以考虑重新去创建一个公共的父类。
小结:
1、继承:子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替。
2、里氏替换原则:任何使用基类的地方,都可以透明的使用其子类。继承+透明(透明:安全,不会出现行为不一致。)
3、父类有的,子类是必须有的;如果父类出现了子类没有的东西,那么就应该断掉继承;再来一个父类,只包含都有的东西。
4、子类可以有自己的属性和行为,子类出现的地方父类不一定能代替。
5、父类实现的东西,子类就不要再写了(就是不要new隐藏),因为有时候会出现意想不到的情况,把父类换成子类后,行为不一致。如果想修改父类的行为,通过abstract/virtual。
6、声明属性、字段、变量,尽量声明为父类(父类就能满足)。
三、迪米特法则(Law Of Demeter)
迪米特法则(最少知道原则):一个对象应该对其他对象保持最少的了解。只与直接的朋友通信。
示例1:
/// <summary> /// 学生 /// </summary> public class Student { public int Id { get; set; } public string StudentName { get; set; } public int Height { private get; set; } public int Salay; } /// <summary> /// 班级 /// </summary> public class Class { public int Id { get; set; } public string ClassName { get; set; } public List<Student> StudentList { get; set; } } /// <summary> /// 学校 /// </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); //管理班级 List<Student> studentList = c.StudentList; //违背了迪米特法则,出现了不知道的类型 foreach (Student s in studentList) { Console.WriteLine(" {0}Manage {1} ", s.GetType().Name, s.StudentName); //管理学生 } } } }
从示例1中可以看出School类的管理方法Manage中不仅出现了Class类(直接的朋友),而且也出现了Student类,此时就出现了依赖Student类,这就违背了迪米特法则。
下面我们针对这个问题对示例1做一下小调整以遵循迪米特法则。
示例2:
/// <summary> /// 学生 /// </summary> public class Student { public int Id { get; set; } public string StudentName { get; set; } public int Height { private get; set; } public int Salay; /// <summary> /// 管理学生自己 /// </summary> 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; } /// <summary> /// 管理班级自己 /// </summary> 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> /// 学校 /// </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) { //1 遵循了迪米特法则 c.ManageClass(); //2 违背了迪米特法则 //Console.WriteLine(" {0}Manage {1} ", c.GetType().Name, c.ClassName); //List<Student> studentList = c.StudentList; //foreach (Student s in studentList) //{ // Console.WriteLine(" {0}Manage {1} ", s.GetType().Name, s.StudentName); //} } } }
小结:
1、迪米特法则(最少知道原则):一个对象应该对其他对象保持最少的了解。只与直接的朋友通信。
2、类与类之间的关系
纵向:继承≈实现(最密切)
横向:聚合> 组合> 关联> 依赖(未知的类出现在方法内部)
3、高内聚低耦合,迪米特法则,降低类与类之间的耦合,只与直接的朋友通信就是要尽量避免依赖更多类型。
4、工作中为了更好的遵循迪米特法则,有时候会去造一个中介/中间层,例如:门面模式、中介者模式、分层封装等。
5、去掉内部依赖,降低访问修饰符权限。依赖别人更少,让别人了解更少(只有必要的才公开)。