浅谈C#六大设计原则

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

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

单一职责原则

单一职责原则(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#六大设计原则的资料请关注我们其它相关文章!

(0)

相关推荐

  • C# 面向对象的基本原则

    C#面向对象的基本原则 一.面向接口编成而不是实现 [Code to an interface rather than to an implementation.] 二.优先使用组合而非继承 [Favor Composition Over Inheritance.] 三.SRP: The single responsibility principle 单一职责 系统中的每一个对象都应该只有一个单独的职责,而所有对象所关注的就是自身职责的完成.[Every object in your syste

  • 高效C#编码优化原则

    本文汇总了高效C#编码常见的优化原则,对于进行C#程序设计来说有很大的参考借鉴作用.具体列出如下: 1.foreach VS for 语句 Foreach 要比for具有更好的执行效率 Foreach的平均花费时间只有for的30%.通过测试结果在for和foreach都可以使用的情况下,我们推荐使用效率更高的foreach 另外,用for写入数据时间大约是读取数据时间的10倍左右. 2.避免使用ArrayList ArrayList的性能低下任何对象添加到ArrayList中都要封箱为Syst

  • 浅谈c#设计模式之单一原则

    单一原则: 程序设计时功能模块独立,功能单一更有助于维护和复用. 例如:个人计算机功能很多,如果想从中只拿出一个功能来制造一个新的东西是困难的.同时如果你的计算机开不机,同时你的计算器功能也不能用了. 在编程中如果一个类封装了太多功能和上面的结果是类似的. 单一职责原则 例1: 大家应该能看出来这个类图中的接口设计是有问题的,用户的属性和用户的行为没有分开.我们根据用户的属性和行为拆开这个接口. 重新拆分成两个接口,IUserBo 负责用户的属性,IUserBiz负责用户的行为.当我们实例化除U

  • 浅谈C#设计模式之开放封闭原则

    在软件设计模式证这种不能修改,但可以扩展的思想也是最重要的设计原则,他就是开放-封闭原则 (OCP) 对于程序设计而言,怎么的设计才能面对需求的改变却可以保持相对的稳定,从而可以使得系统可以再第一个版本的基础上不断的推出新版本呢? 答案是在程序设计的时候使用开放封闭原则.   但是设计的同时,绝对对修改的关闭是不可能的,无论模块是多么的封闭,都存在一些无法对之封闭的变化,既然不可以完全的封闭,设计人员必须对他设计的模块应该对哪种变换的封闭做出选择,他必须猜测出最有可能发生变换的种类,然后构造抽象

  • C#面向对象设计的七大原则

    本文我们要谈的七大原则,即:单一职责,里氏替换,迪米特法则,依赖倒转,接口隔离,合成/聚合原则,开放-封闭 . 1.   开闭原则(Open-Closed Principle, OCP) 定义:软件实体应当对扩展开放,对修改关闭.这句话说得有点专业,更通俗一点讲,也就是:软件系统中包含的各种组件,例如模块(Modules).类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,去扩展新功能.开闭原则中原有"开",是指对于组件功能的扩展是开放的,是允许对其

  • C# 自定义异常总结及严格遵循几个原则

    在C#中所有的异常类型都继承自System.Exception,也就是说,System.Exception是所有异常类的基类. 总起来说,其派生类分为两种: 1. SystemException类: 所有的CLR提供的异常类型都是由SystemException派生. 2. ApplicationException类: 由用户程序引发,用于派生自定义的异常类型,一般不直接进行实例化. 创建自定义异常类应严格遵循几个原则 1. 声明可序列化(用于进行系列化,当然如果你不需要序列化.那么可以不声明为

  • 浅谈C#六大设计原则

    笔者作为一个菜鸟,会尝试以简单的代码和容易理解的语句去解释这几种原则的特性和应用场景. 这六种原则分别为单一职责原则.接口隔离原则.里氏替换原则.迪米特法则.依赖倒置原则.开闭原则. 单一职责原则 单一职责原则(SRP:Single responsibility principle),规定一个类中应该只有一个原因引起类的变化. 单一职责原则的核心就是解耦和增强内聚性. 问题: // 假设此类是数据库上下文 public class DatabaseContext { } public class

  • C#实现六大设计原则之单一职责原则

    单一职责(SRP)定义: 不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责. 问题由来: 类T负责两个不同的职责:职责P1,职责P2.当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障. 解决方案: 遵循单一职责原则.分别建立两个类T1.T2,使T1完成职责P1功能,T2完成职责P2功能.这样,当修改类T1时,不会使职责P2发生故障风险:同理,当修改T2时,也不会使职责P1发生故障风险. ps: 说到单一职责原则,很多人都会不屑一顾.因为

  • C#实现六大设计原则之迪米特法则

    定义: 一个对象应该对其他对象保持最少的了解. 问题由来: 类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大. 解决方案: 尽量降低类与类之间的耦合. PS: 自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚. 无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率. 低耦合的优点不言而喻,但是怎么样编程才能做到低耦合呢?那正是迪米特法则要去完成的. 迪米特法则又叫最少知道原则,最早是在1987年由美国Northe

  • C#实现六大设计原则之接口隔离原则

    接口隔离原则(ISP)定义: 客户端不应该依赖它不需要的接口:一个类对另一个类的依赖应该建立在最小的接口上. 问题由来: 类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法. 解决方案: 将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系.也就是采用接口隔离原则. 举例来说明接口隔离原则: 类A依赖接口I中的方法1.方法2.方法3,类B是对类A依赖的实现. 类C依赖接口I中的方法1.方法4.

  • C#实现六大设计原则之依赖倒置原则

    依赖倒置原则(DIP)定义: 高层模块不应该依赖低层模块,二者都应该依赖其抽象:抽象不应该依赖细节:细节应该依赖抽象. 问题由来: 类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成.这种场景下,类A一般是高层模块, 负责复杂的业务逻辑:类B和类C是低层模块,负责基本的原子操作:假如修改类A,会给程序带来不必要的风险. 解决方案: 将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率. ps: 依赖倒置原则

  • C#实现六大设计原则之里氏替换原则

    定义: 1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子型. 2:所有引用基类的地方必须能透明地使用其子类的对象. 问题由来: 有一功能P1,由类A完成.现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成. 新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障. 解决

  • 六大设计原则之开闭原则

    定义: 一个软件实体如类.模块和函数应该对扩展开放,对修改关闭. 问题由来: 在软件的生命周期内,因为变化.升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试. 解决方案: 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化. 开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统. 开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们

  • 浅谈java中OO的概念和设计原则(必看)

    一.OO(面向对象)的设计基础 面向对象(OO):就是基于对象概念,以对象为中心,以类和继承为构造机制,充分利用接口和多态提供灵活性,来认识.理解.刻划客观世界和设计.构建相应的软件系统.面向对象的特征:虽然各种面向对象编程语言相互有别,但都能看到它们对面向对象基本特征的支持, 即 "抽象.封装.继承.多态" : – 抽象,先不考虑细节 – 封装,隐藏内部实现 – 继承,复用现有代码 – 多态,改写对象行为 面向对象设计模式:是"好的面向对象设计",所谓"

  • 浅谈mysql的索引设计原则以及常见索引的区别

    索引定义:是一个单独的,存储在磁盘上的数据库结构,其包含着对数据表里所有记录的引用指针. 数据库索引的设计原则: 为了使索引的使用效率更高,在创建索引时,必须考虑在哪些字段上创建索引和创建什么类型的索引. 那么索引设计原则又是怎样的? 1.选择唯一性索引 唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录. 例如,学生表中学号是具有唯一性的字段.为该字段建立唯一性索引可以很快的确定某个学生的信息. 如果使用姓名的话,可能存在同名现象,从而降低查询速度. 2.为经常需要排序.分组和联合操

  • 浅谈Java设计模式之七大设计原则

    前言 学习设计模式的方法:掌握理解七大原则以及其目的,学习相应的设计模式(带着设计目的,应用场景(解决什么样的问题),如何实现(编码实现一个小例子),优缺点是什么?等等) 一.单一职责原则(SingleResponsibilityPrinciple,SRP) 定义:一个类只负责一个功能领域中的相应职责 理解:该设计模式很好理解,就是一个类只实现某个领域的相应职责,这样有利于进行调用.就比如在Java开发时,设计controller.service.manager.dao层一样的道理,进行分层分工

随机推荐