浅谈C#六大设计原则

 更新时间:2020年6月25日 10:34  点击:2290

笔者作为一个菜鸟,会尝试以简单的代码和容易理解的语句去解释这几种原则的特性和应用场景。

这六种原则分别为单一职责原则、接口隔离原则、里氏替换原则、迪米特法则、依赖倒置原则、开闭原则。

单一职责原则

单一职责原则(SRP:Single responsibility principle),规定一个类中应该只有一个原因引起类的变化。

单一职责原则的核心就是解耦和增强内聚性。

问题:

 // 假设此类是数据库上下文
 public class DatabaseContext { }

 public class Test
 {
  private readonly DatabaseContext _context;
  public Test(DatabaseContext context)
  {
   _context = context;
  }

  // 用户登录
  public void UserLogin() { }

  // 用户注销
  public void UserLogout() { }

  // 新增一个用户
  public void AddUser() { }

  // 修改一个用户的信息
  public void UpdateUser() { }

  // 删除一个用户
  public void DeleteUser() { }
 }

Test 负责 职责 P1(用户登录和退出)和 P2(用户账号管理) 两个职责,当由于职责 P1 的需求发生变化而需要修改类时, 有可能会导致正常职责 P2 的功能发生故障。

上面的代码中,两个职责被耦合起来,担任了多种功能。

一个类中应该只有一个原因引起类的变化,也就要求一个类只应该负责一个功能,类中地代码是紧密联系的。

上面的示例代码非常简单,我们可以很自然地将一个个类分为两个部分。

 // 假设此类是数据库上下文
 public class DatabaseContext { }

 public class Test1
 {
  private readonly DatabaseContext _context;
  public Test1(DatabaseContext context)
  {
   _context = context;
  }

  // 用户登录
  public void UserLogin() { }

  // 用户注销
  public void UserLogout() { }

 }

 public class Test2
 {
  private readonly DatabaseContext _context;
  public Test2(DatabaseContext context)
  {
   _context = context;
  }
  // 新增一个用户
  public void AddUser() { }

  // 修改一个用户的信息
  public void UpdateUser() { }

  // 删除一个用户
  public void DeleteUser() { }
 }

因此,单一职责原则的解决方法,是将不同职责封装到不同的类或模块中。

接口隔离原则

接口隔离原则(ISP:Interface Segregation Principle) 要求对接口进行细分,类的继承建立在最小的粒度上,确保客户端继承的接口中,每一个方法都是它需要的。

笔者查阅了国外一些资料,大多将接口隔离原则定义为:

“Clients should not be forced to depend upon interfaces that they do not use.”

意思是不应强迫客户依赖于它不使用的方法

对于此原则的解释,这篇文章讲的非常透彻:

https://stackify.com/interface-segregation-principle/

这就要求我们拆分臃肿的接口成为更小的和更具体的接口,使得接口负责的功能更加单一。

目的:通过将软件分为多个独立的部分来减少所需更改的副作用和频率。

笔者想到从两方面论述:

其一,在描述多种动物时,我们可能会将不同种类的动物分类。但是这还不够,例如在鸟类中,我们印象中鸟的特征是鸟会飞,但是企鹅不会飞~。

那么还要对物种的特征进行细分,例如血液是什么颜色的、有没有脊椎等。

其二,我们可以通过下面代码表达:

 // 与登录有关
 public interface IUserLogin
 {
  // 登录
  void Login();

  // 注销
  void Logout();
 }

 // 与用户账号有关
 public interface IUserInfo
 {
  // 新增一个用户
  void AddUser();

  // 修改一个用户的信息
  void UpdateUser();

  // 删除一个用户
  void DeleteUser();
 }

上面的两个接口,各种实现不同的功能,彼此没有交叉,完美。

接下来我们看看两个继承了 IUserLogin 接口的代码

 // 对用户登录注销进行管理,资源准备和释放
 public class Test1 : IUserLogin
 {
  public void Login(){}

  public void Logout(){}
 }

 public class Test2 : IUserLogin
 {
  public void Login()
  {
   // 获取用户未读消息
  }

  public void Logout()
  {
  }
 }

对于 Test1 ,根据登录和注销两个状态,进行不同操作。

但是,对于 Test2,它只需要登录这个状态,其它情况不关它事。那么 Logout() 对他来说,完全没有用,这就是接口污染。

上面的代码就违法了接口隔离原则。

但是,接口隔离原则有个缺点,就是容易过多地将细分接口。一个项目中,出现成千上万个接口,将是维护地灾难。

因此接口隔离原则要灵活使用,就 Test2 来说,多继承一个方法无伤大碍,不用就是了。ASP.NET Core 中就存在很多这样的实现。

  public void Function()
  {
   throw new NotImplementedException();
  }

《设计模式之禅》第四章中,作者对接口隔离原则总结了四个要求:

1  接口尽量小:不出现臃肿(Fat)的接口。

2  接口要高内聚:提高接口、类、模块的处理能力。

3  定制服务:小粒度的接口可以组成大接口,灵活定制新的功能。

4  接口的设计有限度:难以有固定的标准去衡量接口的粒度是否合理。

另外还有关于单一职责原则和接口隔离原则的关系和对比。

单一职责原则是从服务提供者的角度去看,提供一个高内聚的、单一职责的功能;

接口隔离原则是从使用者角度去看,也是实现高内聚和低耦合。

接口隔离原则的粒度可能更小,通过多个接口灵活组成一个符合单一职责原则的类。

我们也看到了,单一职责原则更多是围绕类来讨论;接口隔离原则是对接口来讨论,即对抽象进行讨论。

开闭原则

开闭原则(Open/Closed Principle)规定 :

“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”

--《Object-Oriented Software Construction》作者 Bertrand Meyer
开闭原则意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。类的改动是通过增加代码实现,而不是修改源代码。

开闭原则 有 梅耶开闭原则、多态开闭原则。

梅耶开闭原则

​       代码一旦完成,一个类的实现只应该因错误而修改,新的或者改变的特性应该通过新建不同的类实现。

​       特点:继承,子类继承父类,拥有其所有的方法,并且拓展。

多态开闭原则

​       此原则使用接口而不是父类来允许不同的实现,您可以在不更改它们的代码的情况下轻松替换它们。

现在大多数情况下,开闭原则指的是多态开闭原则。

多态开闭原则笔者在查阅资料是,发现这个接口指的不是 Interface ,指的是抽象方法、虚方法。

问:面向对象的三大特性是什么?答:封装、继承、多态。

对,多态开闭原则就是指这个多态。不过,原则要求不应对方法进行重载(重写)、隐藏。

这是一个示例:

 // 实现登录注销
 public class UserLogin
 {
  public void Login() { }
  public void Logout() { }
  public virtual void A() {/* 做了一些事*/}
  public virtual void B() {/* 也做了一些事*/ }
 }
 public class UserLogin1 : UserLogin
 {
  public void Login(string userName) { }  // 应不应该对父类的方法进行重载?
  public override void A() { }    // √
  public override void B() { }    // √
  public new void Logout() { }    // 也许行?
 }

多态开闭原则的好处是,引入了抽象,使得两个类松耦合,而且可以使得在不修改代码的前提下,使用子类替换父类(里氏替换原则)。

有时,会看到这样的题目:接口和抽象类的区别?

笔者隐约记得有过一条这样的解释:接口是为了实现共同的标准;抽象是为了代码的复用。

当然,接口和抽象,都可以实现里氏替换。

通过开闭原则,我们可以了解到多态,也了解接口和抽象的应用场景。

还有一个问题是,开闭原则要求是要修改或添加功能时,通过子类来实现,而不是修改原有代码。那么是否可以和应该对父类的代码进行重载和隐藏?

而开闭原则的核心是构造抽象,从而通过子类派生来实现拓展。貌似没有说到这方面。

笔者觉得不太应该。。。

先结合下面的里氏替换原则,我们再讨论这个问题?

里氏替换原则

里氏替换原则(LSP:Liskov Substitution Principle)要求:凡是父类出现的地方,子类都可以出现。

这就要求了子类必须与父类具有相同的行为。只有当子类能够替换任何父类的实例时,才会符合里氏替换原则。

里氏替换原则的约束:

1  子类必须实现父类的抽象方法,但不能重写父类中已实现的方法。

2  子类中可以增加方法拓展功能。

3  当子类覆盖或实现(虚拟方法/抽象方法)父类的方法时,方法的输入参数限制更加宽松并且返回值要比父类方法更加严格。

所以,我们看到开闭原则中的示例,子类应不应该重载父类的方法?应不应该使用 new 关键字隐藏父类的方法?为了确保子类继承后,还具有跟父类一致的特性,不建议这样做呢,亲。

实现了开闭原则,自然可以使用里氏替换原则。

依赖倒置原则

依赖倒置原则(Dependence Inversion Principle)要求程序要依赖于抽象接口,不要依赖于具体实现。

我们可以从代码中,慢慢演进和推导理论。

 // 实现登录注销
 public class UserLogin
 {
  public void Login(){}
  public void Logout(){}
 }

 public class Test1 : UserLogin { }

 public class Test2
 {
  private readonly UserLogin userLogin = new UserLogin();
 }

 public class Test3
 {
  private readonly UserLogin _userLogin;
  public Test3(UserLogin userLogin)
  {
   _userLogin = userLogin;
  }
 }

上面代码中,Test1、Test2、Test3 都依赖 UserLogin 。先不说上面代码有什么毛病,根据依赖倒置原则,应该是这样编写代码的

 // 与登录有关
 public interface IUserLogin
 {
  void Login();  // 登录
  void Logout();  // 注销
 }

 // 实现登录注销
 public class UserLogin1 : IUserLogin
 {
  public void Login(){}
  public void Logout(){}
 }
 
 // 实现登录注销
 public class UserLogin2 : IUserLogin
 {
  public void Login(){}
  public void Logout(){}
 }

 public class Test4
 {
  private readonly IUserLogin _userLogin;
  public Test4(IUserLogin userLogin)
  {
   _userLogin = userLogin;
  }
 }

依赖倒置原则,在于引入一种抽象,这种抽象将高级模块和底层模块彼此分离。高层模块和底层模块松耦合,底层模块的变动不需要高层模块也要变动。

依赖导致原则有两个思想:

1  高层模块不应该依赖于底层模块,两者都应该依赖于抽象。

2  抽象不应该依赖细节,细节应该依赖于抽象。

因为依赖于抽象,底层模块可以任意替换一个实现了抽象的模块。

里氏替换原则是要求子类父类行为一致,子类可以替换父类。

依赖倒置原则,每个方法的行为是可以完全不一样的。

迪米特法则

迪米特法则(Law of Demeter)要求两个类之间尽可能保持最小的联系。

例如 对象A 不应该直接调用 对象B,而是应该通过 中间对象C 来保持通讯。

请参考 https://en.wikipedia.org/wiki/Law_of_Demeter

优势:松耦合,较少了依赖。

缺点:要编写许多包装代码,增加复杂读,模块之间的通讯效率变低。

笔者找了很多资料,发现都是 java 的。。。

一般来说,较少会提到迪米特原则,代码符合依赖倒置原则和里氏替换原则等,也就算是符合迪米特法则了。

以上就是浅谈C#六大设计原则的详细内容,更多关于C#六大设计原则的资料请关注猪先飞其它相关文章!

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • photoshop设计一幅大鱼海棠动画片海报制作实例教程

    今天小编在这里就来给各位photoshop的这一款软件的使用者们来说一说设计一幅大鱼海棠动画片海报制作的实例教程,各位想知道具体制作步骤的使用者们,那么各位就快来看看...2016-09-14
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25