C# 设计模式——设计原则
1、前言
最近在搭建项目的的时候才会想设计原则问题,之前也看过设计模式,没有写博客很快就忘了也没有起到什么作用。现在在项目上遇到了你才会发现它的美。博客园也有很多前辈写的很好,对于我来说好记性不如烂笔头嘛。别人写的在好你看了之后终究是别人的。只有自己写下来会用了才是自己的。
2、定义
个人理解设计原则其实就是一个规范一样,为啥要用设计原则?就是为了写出适应变化、提高复用率、可维护性、可扩展性的代码。在进行设计的时候,我们需要遵循单一职责原则、开闭原则、里氏替代原则、依赖倒置原则、接口隔离原则、合成复用原则和迪米特法则。
3、单一职责原则
自己的事情自己干,一个类只弄它单一职责的模块。比如说Login类就只负责登录相关的业务,User类只负责用户相关的业务。如果说Login里面又夹杂着User的功能这样User牵连着其他的类是不是Login类也牵连进去了,这样下来耦合度就很好了。单一职责原则优点就是降低耦合,提高代码的复用率使得模块看起来有目的性,结构简单,修改当前模块对于其他模块的影响很低。缺点就是如果过度的单一,过度的细分,就会产生出很多模块,无形之中增加了系统的复杂程度。
比如Login能登录 但是里面又写了个User吃饭的方法,登录跟吃饭八杆子打不到一块去。生物分类学是研究生物分类的方法和原理的生物学分支。分类就是遵循分类学原理和方法,对生物的各种类群进行命名和等级划分:界门纲目科属种一样。程序里面类也是一样的。
Login login=new(); login.Sign("登录"); public class Login { /// <summary> /// 登录 /// </summary> public void Sign(string name) { Console.WriteLine($"账号密码{name}"); } }
现在在弄进去一个Eat(吃饭)login.Sign("吃饭");输出 账号密码吃饭。感觉就不合适了。如果要兼顾两个职责这时候要把程序做一点改变
第一种:这样就让Sign兼顾了两个职责:登录跟吃饭
public void Sign2(string name) { if(name=="登录") Console.WriteLine($"账号密码{name}"); else if(name=="吃饭") Console.WriteLine($"用户{name}"); }
第二种:此时Sign跟Eat职责都是单一的,但是我们设计之初就是Login是用来登录的
public class Login { /// <summary> /// 登录 /// </summary> public void Sign(string name) { Console.WriteLine($"账号密码{name}"); } /// <summary> /// 吃饭 /// </summary> /// <param name="name"></param> public void Eat(string name) { Console.WriteLine($"用户{name}"); } public void Sign2(string name) { if(name=="登录") Console.WriteLine($"账号密码{name}"); else if(name=="吃饭") Console.WriteLine($"用户{name}"); } }
第三种:此时Login 的Sign 跟User的Eat职责都是单一的,只做一件事。
public class Login { /// <summary> /// 登录 /// </summary> public void Sign(string name) { Console.WriteLine($"账号密码{name}"); } } public class User { /// <summary> /// 吃饭 /// </summary> /// <param name="name"></param> public void Eat(string name) { Console.WriteLine($"用户{name}"); } }
4、开闭原则(OCP)
强调的是:一个软件实体(指的类、函数、模块等)应该对扩展开放,对修改关闭。即每次发生变化时,要通过添加新的代码来增强现有类型的行为,而不是修改原有的代码。修改本省的代码破坏现有的程序可能稍微不注意就引起很大的连锁反应。通过扩展来实现就是本可能出现的结果抽象出来。
栗子:狗的叫声 Dog(狗)、Show(实现)、Voice(声音)。根据实现传入动物的声音
public class Dog { public void Call(DogVoice dogVoice) { Console.WriteLine($"狗的叫声是{dogVoice.Value}"); } } public class DogVoice { public string Value { get { return "汪汪汪"; } } } public class Show { public void ShowVoice(Dog dog,DogVoice dogVoice) { dog.Call(dogVoice); } }
调用
Dog d = new Dog(); DogVoice dv = new DogVoice(); Show s = new(); s.ShowVoice(d,dv);
如果有一天我们引入了新的类小猫类Cat跟声音CatVoice,这个时候就要修改Show 显示类了,这就违反了开闭原则。如果要符合开闭原则 我们就要对Dog跟Voice类做一个抽象,抽象出Call跟Voice的类或者接口。这里我们抽象两个接口ICall 跟IVoice
public interface ICall { void Call(IVoice voice); } public interface IVoice { string Value { get; } } public class Gog2 : ICall { public void Call(IVoice voice) { Console.WriteLine($"狗的叫声是{voice.Value}"); } } public class DogVoice2 : IVoice { public string Value => "汪汪汪"; } public class Show2 { public void ShowVoice(ICall dog, IVoice dogVoice) { dog.Call(dogVoice); } }
调用
ICall d = new Dog2(); IVoice dv = new DogVoice2(); Show2 s = new(); s.ShowVoice(d, dv);
这样你要新加锚的叫声不是就不用修改show了;新添加两个类就可以了Cat跟CatVoice。
public class Cat : ICall { public void Call(IVoice voice) { Console.WriteLine($"猫的叫声是{voice.Value}"); } } public class CatVoice : IVoice { public string Value => "喵喵喵"; }
调用
ICall d = new Dog2(); IVoice dv = new DogVoice2(); Show2 s = new(); s.ShowVoice(d, dv); ICall c = new Cat(); IVoice v = new CatVoice(); s.ShowVoice(c, v);
5、里氏替换原则(LSP)
一个程序中的子类如果继承了父类那么他将满足父类所有的方法与属性。也就是说在程序中,把父类都替换成它的子类,程序的行为没有任何变化。子类可以有自己的特点也可以重写父类的方法,但是父类有的方法子类一定要实现,这里举一个简单的列子,父类动物(Adimal),子类 母鸡(Hen)
/// <summary> /// 抽象类父类 动物 /// </summary> public abstract class Animal { /// <summary> /// 抽象方法 下蛋量 /// </summary> /// <returns></returns> public abstract double LayEgg(); /// <summary> /// 一年平均每天下蛋 /// </summary> /// <returns></returns> public abstract double LayEggAvg(Animal s); } public class Hen : Animal { public override double LayEgg() { return 100; } public override double LayEggAvg(Animal s) { return 365 / s.LayEgg(); } }
这时候如果在加一个类 羊 Sheep 羊也属于动物类 但是它不会下蛋呀,这个时候计算平均下蛋量的时候程序就要出错;0作为被除数程序编译都通过不了。
public class Sheep : Animal { public override double LayEgg() { return 0; } public override double LayEggAvg(Animal s) { return 365 / s.LayEgg();//0 } }
要运行也可以直接判断是不是绵羊类。是的话返回0,这样又不符合开闭原则了呀。因为Sheep就完全没有继承Animal类,它实现不了LayEggAve的方法。所以现实中绵羊属于动物类,但是程序中不属于,不要强行继承,如果继承就要完全实现。
public class Sheep2 : Animal { public override double LayEgg() { return 0; } public override double LayEggAvg(Animal s) { //传入的类型如果是绵羊类 返回0 if (s.GetType().Equals(typeof(Sheep2))) { return 0; } else { return 365 / s.LayEgg(); } } }
6、合成复用原则
如果程序一个对象A包含了另一个对象B,那么A就可以委托B来使用B的功能。 这里学生表李有班级 我们就可以通过学生直接看到班级的信息。
public class Student { public Class Class { get; set; } } public class Class { public string GetName { get { return "计科一班"; } 、 } }
调用
Student student = new(); var name=student.Class.GetName;
7、接口隔离原则
原则上使用多个接口,应用程序端不依赖不用多余的接口,这样也可以保证程序的安全性。比如说系统内部用的接口我们都要实现增删查改,而对于第三方我们只提供查询的接口就可以了。
public interface ICardServiceOut { void Query();//查 } /// <summary> /// 系统内部就继承两个接口 /// </summary> public class OurCard : ICardService, ICardServiceOut { public void Add() { throw new NotImplementedException(); } public void Query() { throw new NotImplementedException(); } public void Remove() { throw new NotImplementedException(); } public void Update() { throw new NotImplementedException(); } } /// <summary> /// 第三方就只能继承外部接口 只能查询操作 /// </summary> public class ThirdCard : ICardServiceOut { public void Query() { throw new NotImplementedException(); } }
8、依赖倒置原则(DIP)
抽象不应该依赖细节,而细节应该依赖抽象,高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口。一般来讲,就是高层模块定义接口,低层模块负责具体的实现。针对接口编程而不是针对细节编程。详情查看Asp.Net Core 3.1学习-依赖注入、服务生命周期(6)
9、迪米特法则(LoD)(最少知识原则(LKP))
指的是一个对象应当对其他对象有尽可能少的了解。也就是说,一个模块或对象应尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立,这样当一个模块修改时,影响的模块就会越少,扩展起来更加容易。如果想要满足迪米特法则,就要尽可能少的写public方法和变量,不需要让别的对象知道的方法或者字段就不要公开,好朋友之间都有自己的秘密一个的意思。
其实用不用设计原则程序都能运行出来,没有多大影响,但是后面要增加模块或者修改功能的时候就看你的程序能不能经得起折腾咯?
PS:学习向日葵,做一个积极吸收正能量的人。人生多数时候都是自寻烦恼。就是吸收的负能量太多。要学习向日葵,哪里有阳光就朝向哪里。多接触优秀的人,多谈论健康向上的话题,多想想有利于人生发展的问题。心里若是充满阳光,人生即便下雨,也会变成春雨。
版权声明:本文为 魏杨杨 原创文章并发布到博客园, 除了【萬仟网】外, 其他平台欢迎转载,但必须在文章页面明显位置写明作者和出处,非常感谢。技术交流QQ群 99210270
微信扫一扫关注我公众号
一起学习,一起进步