设计模式原则
参考:https://zhuanlan.zhihu.com/p/54147707
分类
- 开放封闭原则——自己相关的属性封装在自己类里,对外暴露功能
- 职能单一性原则——一个类只负责一个功能领域的相应职责,一个类只负责做一类事
- 里氏替换原则——所有引用基类的地方都能透明地使用子类代替
- 迪米特原则——一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少关联,降低对其他类的依赖。
- 接口隔离原则——接口尽量细化,功能单一,同时接口中的方法尽量少
- 依赖倒置原则——依赖抽象,而不是依赖细节
相关介绍
单一职责原则
- 一个类只负责一个功能领域的相应职责,一个类只负责做一类事
适用场景:
-
当一个类里需要多个类型分支判断,影响类的稳定性的时候,需要拆分成多个类
此处Animal就该单独作为一个类,然后新增Dog,Cat的类继承Animal
class Animal { public string Type { get; set; } public void Eat() { if (Type == "Cat") { Console.WriteLine("吃鱼"); } else if (Type == "Dig") { Console.WriteLine("吃骨头"); } } public void dothing() { if (Type == "Cat") { Console.WriteLine("抓老鼠"); } else if (Type == "Dig") { Console.WriteLine("看家护院"); } } }
-
类里涉及的职责涉及太多层面,整个类很臃肿,很难理解这个类负责哪些职责
比如银行客户端,里面有货币转换、存钱、取钱等等操作;整个类显然太庞杂了。我们可以把里面的业务提取出来,比如提取一个货币转换类专门负责各种货币转换,我们可以抽取一个利率维护类,专门负责管理不同的业务的利率换算;抽取一个业务类,负责存钱、取钱。。。
class BankClient { public double TransLateRMBToDollars(int RMBCount) { Console.WriteLine("人民币转美元"); //double count = func(RMBCount) return 0; } public double TransLateDollarsToRMB(int RMBCount) { Console.WriteLine("美元转人民币"); //double count = func(RMBCount) return 0; } //存钱 public void StorageMoney(double money) { } //取钱 public void GetMoney(double money) { } }
可违背场景
-
如果类型足够简单,可以在类级别去违背单一职责
比如上例里业务就只有猫和狗两种类型,就可以写进一个类里
-
如果方法足够简单,可以在方法级别去违背单一职责
比如余额查询,余额查询涉及利息计算、账目显示;如果利息计算比较简单,只有一个利率和年限就可计算,这一步就可以写在在余额查询方法里。
如果利息计算比较复杂(比如要用到个人存款年限、是否为金卡用户、信誉等级等数据进行复杂计算),则应该把利息计算抽取单独定义成一个方法
相关建议
- 如果类型复杂了,方法逻辑多了,建议遵循单一职责原则
- 一个方法,不超过50行(编码建议)
- 一个功能类,不超过300行(编码建议)
优缺点
优点:
- 降低类的复杂度,类的职责单一,逻辑简单清晰
- 提高类的可读性,提高了类的稳定性(变逻辑拆分为新增)。
- 降低了与其他类的耦合性,因为功能单一,修改对其他功能的影响变小
缺点:
- 拆多了会更零碎,不好管理,使用成本高。
拓展
方法级别的单一职责原则:一个方法只做好一件事儿—职责拆分成小方法、接受输入-业务计算-数据
操作-日志、分支逻辑拆分--源码:父类方法DoS(写校验)-DoSCore(核心业务)
类级别的单一/WebCore
项目级别的单一职责原则:项目的职责要清晰—Client、Manager、职责原则:一个类只做好一件事
儿—源码:scheme-schemeprovider-handler
类库级别的单一职责原则:一个类库做好一件事儿—DAL/BLL/Core/PayCoreBackgroudJob
系统级别的单一职责原则:通用系统拆分---IP库/日志中心/在线统计
里氏替换原则
定义一:存在两个不同类(
T1
、T2
)的对象o1
,o2
,若一个程序里的所有对象o1
都能使用o2
代替,对程序地行为没有任何影响,则T2
是T1
的子类
定义二:所有引用基类的地方都能透明地使用子类代替。(透明代表对程序没有任何影响)
这是与继承地区别,因为继承存在重写,如果存在重写,这样替换肯定会有影响的
插播:抽象方法(abstract)、虚方法(virtual)及接口(interface) - 海岸线summer - 博客园 (cnblogs.com)
插播:is 与 as 的区别
is : 相当于判断,A is B A是不是B或者A是不是B的子类?
as :先判断,在转换。(它比传统的强制转换相对来说要安全一点,因为传统的强制转换, 一旦转换失败的话,程序就会崩溃,那么使用as关键字,如果转换不成功,就转换成空类型)
潜在规则:
- 尽量将公共属性、字段、变量及不涉及重写的方法抽象到父类
- 避免出现父类有,子类里没有的情况
- 父类实现的东西,子类就不要再写了
迪米特法则
迪米特法则(Law of Demeter)又叫作最少知识原则(The Least Knowledge Principle)
原则:
- 一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少关联,降低对其他类的依赖。
- 如果需要建立依赖,尽量把关联建立在第三方功能实现类里,不要直接建立联系
目的:
- 迪米特法则的初衷在于降低类之间的耦合。避免一个类的更改影响太多其他类。
- 由于耦合度降低,从而提高了类的可复用率和系统的扩展性。
- 由于每个类尽量减少对其他类的依赖,但是因为要实现功能类的相互调用是很常见的,为了降低更改影响同时实现功能,这个时候创建第三方功能类去间接建立联系。如果某一个类有变化,只需要改动这个类和关联类。
过度使用缺陷:
- 过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。
结论:
在采用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
迪米特法则不希望类之间建立直接的联系。
具体应用
参照:.NET(C#) 设计模式六大原则 迪米特法则-CJavaPy
错误例子:
public class LODPatternError
{
static void Main(string[] args)
{
Phone phone = new Phone();
phone.read("三国演义");
}
}
class Phone{
App app = new App();
public void read(string title)
{
Book book = new Book(title);
app.readBook(book);
}
}
class App
{
//app里的书单
public List<Book> books = new List<Book>();
public void readBook(Book book)
{
if (books.Find(s=>s.Title == book.Title) == null)
{
DownloadBook(book);
}
Console.WriteLine($"开始读{book.Title}");
}
public void DownloadBook(Book book)
{
Console.WriteLine($"开始下载{book.Title}");
books.Add(book);
}
}
class Book
{
public string Title { get; set; }
public Book(string title)
{
Title = title;
}
}
三个类:手机——App——书籍
正常的设计是:手机里面有阅读软件,阅读软件里面有书籍
看书的流程是:
- 我想看XXX书(告诉手机我想看书的标题)
- 我进去app根据书籍名称找这本书,没有就在app里下载
- 然后在app里阅读这本书
App相当于手机和书籍类的一个第三方中介类;手机不应该直接和书籍类建立联系。
手机的变化不应该对书籍有影响;书籍的变化不应该对手机有影响。显然在当前手机类的阅读方法里就有书籍实例的创建。就是说如果书籍的创建方式变化,我还需要修改手机类里的这个方法,这显然是不合理的。
正确例子:
internal class LODPattern
{
static void Main(string[] args)
{
Phone phone = new Phone();
phone.read("三国意义");
}
}
class Phone
{
App app = new App();
public void read(string title)
{
app.readBook(title);
}
}
class App
{
//app里的书单
public List<Book> books = new List<Book>();
public void readBook(string bookTitle)
{
Book book = books.Find(s=>s.Title == bookTitle);
if (book == null)
{
DownloadBook(bookTitle);
}
Console.WriteLine($"开始读{book.Title}");
}
public void DownloadBook(string title)
{
Console.WriteLine($"开始下载title");
Console.WriteLine($"下载完成");
books.Add(new Book(title));
}
}
class Book
{
public string Title { get; set; }
public string Conyent { get; set; }
public string Author { get; set; }
public Book(string title)
{
Title = title;
}
}
依赖倒置原则(Dependence Inversion Principle)
定义
设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。
简单说:依赖抽象,而不是依赖细节
抽象——抽象类/接口——包含没有实现的
细节:具体的类——所有的元素都是确定的
要有【面向接口】编程的思维
目的
- 减少类与类之间的耦合性,提高系统的稳定性
- 提高代码的可读性和可维护性
- 够降低修改程序所造成的风险
实践
-
变量的表面类型尽量是接口或者是抽象类
-
任何类都不应该从具体类派生
-
尽量不要覆写基类的方法
-
结合里氏替换原则使用
错误例子:
public class Student
{
public string Name { get; set; }
//玩Iphone手机
public void PlayIPhone(IPhone phone)
{
Console.WriteLine($"这里是{this.Name} {nameof(phone)}");
phone.Call();
phone.Text();
}
//玩小米手机
public void PlayMiPhone(MiPhone phone)
{
Console.WriteLine($"这里是{this.Name} {nameof(phone)}");
phone.Call();
phone.Text();
}
//那其他手机呢
}
public class IPhone
{
public string Name { get; set; }
public void Call()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
public void Text()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
}
public class MiPhone
{
public string Name { get; set; }
public void Call()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
public void Text()
{
Console.WriteLine("User {0} Call", this.GetType().Name);
}
}
这里定义几个类:学生、IPhone手机、小米手机
学生类里玩手机
PlayPhone
这个方法依赖于具体的手机类型,比如玩PlayIPhone
、PlayMiPhone
会诞生几个问题:
如果换了华为荣耀手机,是不是还要在Student类里添加一个
PlayHonnorPhone
?有一种新的手机机型出现就要修改一次Student类,这合理吗?从学生玩手机可以看出学生Student是依赖手机这个类型,但是学生一般也就是用手机打电话、上网等,换一个品牌、配置其实实际使用的还是这些功能。也就是说学生依赖的是一个可以打电话、上网、看视频的东西,这是不是就是一个抽象的手机概念
就如第一条所说,手机实际上就是打电话、上网等功能,除了操作系统、芯片这些少数区别,大多数手机的大部分功能都是一样的,有必要新出一个手机类型,我就把手机的属性或者功能重复写一遍吗?
正确的例子:
public class Student
{
public string Name { get; set; }
//玩手机
public void PlayPhone(AbstractPhone phone)
{
Console.WriteLine($"这里是{this.Name} {nameof(phone)}");
phone.Call();
phone.Text();
}
}
public abstract class AbstractPhone
{
public string Name { get; set; }
public abstract void Call();
public abstract void Text();
}
public class MiPhone: 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);
}
}
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);
}
}
这样手机种类的增加不会影响学生类Student的变化,而且抽象手机AbstractPhone发送变化的情况比具体的手机变化的频率小太多。所以Student类很稳定。
总结:
-
层级多了,依赖细节,一个改动会影响一连串---水波效应
-
依赖抽象,类的改动影响就不会扩散
-
相对于细节的多变性,抽象的东西要稳定的多
-
抽象指的是接口或者抽象类,细节就是具体的实现类。
使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去
实践建议
- 低层模块尽量都要有抽象类或接口,或者两者都有
- 变量的声明类型尽量是抽象类或接口(比如玩手机的参数变量)
- 使用继承时遵循里氏替换原则(避免埋雷)
依赖倒置原则的核心就是要我们面向抽象/接口编程
理解了面向抽象/接口编程,也就理解了依赖倒置。
现实应用
- 各种工厂的面向抽象---80%的设计模式都跟面向抽象有关
- 现代化开发,IOC已内置—无处不在的IOC---就得有抽象
接口隔离原则(Interface Segregation Principle)
定义
- 客户端不应该依赖它不需要的接口
- 类间的依赖关系应该建立在最小的接口上
使用规则
-
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是事实,但
是如果过小,则会造成接口数量过多,使设计复杂化,所以一定要适度。
-
接口细节要屏蔽(一个功能接口可能有一个串行化的动作,比如获取食物的接口,接口实现里需要
备料——烹饪——上菜等诸多步骤但没必要体现细节)
-
通过接口继承来组合接口
例子:
有以下几个类,对应功能如下
手机:打电话 发短信 看电影 上网 玩游戏 拍照 拍视频 导航 支付
平板: 看电影 上网 玩游戏 拍照 拍视频
电视: 看电影 上网 玩游戏
相机: 拍照 拍视频
- 将每个功能拆分成一个接口——接口太多不利管理书写
- 将这些功能统一成一个接口——没有的功能也得实现(比如电视没有支付功能)这样也不合理(违反了定义第一条)
- 接口组合及合并——通过功能的统一性(比如拍照、拍视频基本上是成对存在)、及统一的目的性(比如提供食物:可以将备料—烹饪—上菜接口组合成一个接口)等逻辑通过接口继承的方式去组合成新的接口
接口隔离与单一职责的区别:
-
单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。
-
单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和
细节;而接口隔离原则主要约束接口,主要针对抽象,针对程序整体框架的构建。
开闭原则(Open Closed Principle)
定义
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
修改:修改现有代码(类-方法)
扩展:增加代码(类-方法)
为什么要尽量遵循开闭原则
面向对象语言是一种静态语言,最害怕变化,会波及很多东西,需要全面测试。
如果有变化,最好的方式就是利用新增去替代修改实现新功能,能够保障原有的功能稳定可靠
其实其他几个原则也是为了更好实现开闭原则
实现方式
- 新增一个类实现新的方法
- 新增一个类继承原有的父类,再在类里定义新的功能