设计模式之设计原则

一些经典的设计原则

其中包括,SOLID、KISS、YAGNI、DRY、LOD 等。
SOLID原则:由5个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则,依次对应SOLID中的S、O、L、I、D 这5个英文字母。
SRP单一职责原则 Single Responsibility Principle;
KISS保持简单 Keep It Simple and Stupid;
YAGNI不需要原则 You Ain’t Gonna Need It;
DRY不要重复原则 Don’t Repeat Yourself;
LOD迪米特法则 Law of Demeter

1.单一职责原则(SRP)

1. 1如何理解单一职责原则?

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

1.2 如何判断类职责是否单一?

  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

1.3类的职责是否设计越单一越好?

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

2.开闭原则(Open closed principle)

2.1 如何理解对扩展开放,对修改关闭?

添加一个新的功能时应该在已有代码基础上扩展代码(新增模块,类,方法等),而非修改已有代码(修改模块,类,方法等)。
实际上开闭原则可以理解为代码的扩展性问题,判断一段代码是否易扩展的“金标准”,即为某段代码在应对未来需求变化的时候,能够做到“对扩展开放,对修改关闭”,但是有时候扩展和代码的复杂性又息息相关,增加代码的扩展性往往会增加代码的复杂度,使得代码更加难以解读。
我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

2.2 如何做到“对扩展开放,对修改关闭“?

写代码的时候我们首先需要多花点时间来思考一下,这段代码在未来可能会有哪些需求变更,如何设计代码结构,以便事先留好扩展点,从而在未来需求变更的时候,在不改动代码整体结构的情况下,做到最小代码改动的情况下,将新的代码灵活的插入到扩展点上。
比如在Byteps的通信库的设计中,上层实现时不需要去考虑底层通信协议到底是TCP,还是RDMA,亦或是UCX。所以上层实现时通过定义一个抽象类,当底层具体通信协议实现时定义新的协议类继承该抽象类,从而实现如果出现一个新的协议,能够进一步写一个新的协议类来实现顶层的抽象类,从而做到对扩展开放,对修改关闭。

3.里氏替换原则(Liskou Substitution Principle)

3.1 如何理解里氏替换原则?

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
简单来说,里氏替换原则还有一个更加能落地,更具有指导意义的描述,那就是按照协议来设计,子类在设计的时候,要遵守父类的行为约定,父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定,这里的行为约定包括:函数声明要实现的功能;对输入,输出,异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类的关系,也可以替换成接口和实现类之间的关系。

3.2 哪些代码明显违背了LSP

  1. 子类违背了父类声明要实现的功能。
    假设父类中提供的是订单排序函数,按照金额从小到大来给订单排序,而子类重写这个订单排序函数时,是按照创建日期来给订单排序,那子类的设计就是违反了里氏替换原则。
    2)子类违背父类对输入,输出,异常的约定
    在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。那子类的设计就违背里式替换原则。
    在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
    在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
    3)子类违背父类注释中所罗列的任何特殊说明
    父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。

通常判断子类的设计实现是否违背里式替换原则那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。

3.3 多态和里氏替换原则的不同

虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

3.4 里氏替换原则存在的意义?

一、改进已有实现。例如程序最开始实现时采用了低效的排序算法,改进时使用LSP实现更高效的排序算法。
二、指导程序开发。告诉我们如何组织类和子类(subtype),子类的方法(非私有方法)要符合约定。
三、改进抽象设计。如果一个子类中的实现违反了LSP,那么是不是考虑抽象或者设计出了问题。
多态是一种能力,而里氏替换原则一种约定,想要实现这种约定,需要依靠多态这种能力。

4.接口隔离原则(Interface Segregation Principle)

4.1 如何理解接口隔离原则?

直译的话即为客户端不应该被强迫依赖它不需要的接口,其中的客户端可以理解为接口的调用者或者使用者。
理解接口隔离原则的关键即为理解其中的“接口”二字,在这条原则中,接口可以被理解为以下三种东西。

  • 一组API接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
  • 单个API接口或函数。部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的细粒度函数。
  • OOP中的接口概念,可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

4.2 接口隔离原则与单一职责原则的区别?

单一职责原则针对的是模块,类,接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接的判定。如果调用者只使用了部分接口或接口的部分功能,那接口的设计就不够职责单一。

5.依赖反转原则

5.1 控制反转

一般来说程序员控制代码的实现流程,控制反转即框架提供了一个可扩展的代码骨架,用来组装对象,管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。此处的控制指的是程序执行流程的控制,而反转指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员反转到了框架。

  • 控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。

5.2 依赖注入(Dependency lnjection)

依赖注入与控制反转恰恰相反,它是一种具体的编程技巧。
用一句话来概括就是:不通过new()的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数,函数参数等方式传递给类使用。

5.3 依赖注入框架(DI framework)

在实际的软件开发中,一些项目可能会涉及几十,上百,甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高,而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。
依赖注入框架,我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象,类与类之间的依赖关系,就可以实现由框架来自动创建对象,管理对象的生命周期,依赖注入等原本需要程序员来做的事情。

5.4 依赖反转原则(Dependency Inversion Principle)

依赖反转原则的大概意思即高层模块不要依赖低层模块,高层模块和底层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。
在调用链上,调用者属于高层,被调用者属于低层。
Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值