面向对象的设计原则

面向对象的设计原则

目录

单一职责原则(SRP)

开闭原则(OCP)

里式替换原则(LSP)

接口隔离原则(ISP)

依赖反转原则(DIP)

KISS原则

YAGNI原则

DRY 原则

迪米特法则(LOD)


单一职责原则(SRP)

单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single responsibility。王争老师描述是这样的:一个类或者模块只负责完成一个职责(或者功能)。

我们举个例子,几乎每个系统都有一个用户模块(user-module),用户模块下根据架构基本会有用户实体类(User)、用户访问类(UserRepository)、用户服务类(UserService)、用户控制类(UserController);

现在我们来分析一下用户模块职责。

用户模块(user-module),职责是提供与用户有关的信息和操作的功能接口(有些还会包含界面);

用户实体类(User),职责是做为数据库用户表的实体映射;

用户访问类(UserRepository),职责是访问数据用户表,通过User为载体对数据库表进行增删改查操作;

用户服务类(UserService),职责是提供与用户有关的业务逻辑处理及事务管理;

用户控制类(UserController),职责是向前端或客户端提供接口;

通过这个例子,可以了解到根据职责来划分模块和定义类。只要有些经验的程序员都知道这样划分,根据现实世界的对象或物种来划分,如人、车、用户、订单、老师、学生等。但如果是在现实不常用到或是要经过科学研究计算才可以认知和划分的东西,人脑并没有那么形象,要划分就没那么容易。还有就是从业务领域划分也是常用的方法,也比较容易。但如果跟业务没有关系是从复用的功能或解决某些技术问题的为出发而设计的类要划分就不容易。

如何理解单一职责原则?

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

如何判断类的职责是否足够单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:类中的代码行数、函数或者属性过多;类依赖的其他类过多,或者依赖类的其他类过多;私有方法过多;比较难给类起一个合适的名字;类中大量的方法都是集中操作类中的某几个属性。

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

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

此原则的目的是实现代码高内聚、低耦合,提高系统的稳定性。

开闭原则(OCP)

开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。王争老师描述是这样的:添加一个新的功能,应该是在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

1. 如何理解“对扩展开放、对修改关闭”?

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

2. 如何做到“对扩展开放、修改关闭”?

我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

此原则的目的就是:不修改保护了稳定性,可扩展提高扩展性。

里式替换原则(LSP)

里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则,英文原话是这样的:Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。王争老师描述是这样的:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。(不得不说还是中文好理解啊!!!)

举几个违反里式替换原则的例子

1. 子类违背父类声明要实现的功能

父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。

2. 子类违背父类对输入、输出、异常的约定

在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。那子类的设计就违背里式替换原则。在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。

3. 子类违背父类注释中所罗列的任何特殊说明

父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。以上便是三种典型的违背里式替换原则的情况。除此之外,判断子类的设计实现是否违背里式替换原则,还有一个小窍门,那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全地遵守父类的约定,子类有可能违背了里式替换原则。实际上,你有没有发现,里式替换这个原则是非常宽松的。一般情况下,我们写的代码都不怎么会违背它。所以,只要你能看懂我今天讲的这些,这个原则就不难掌握,也不难应用。

此原则目的就是不要让扩展破坏了稳定性。

接口隔离原则(ISP)

接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use”。王争老师描述是这样的:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

接口隔离原则,重点目的在隔离,接口是隔离的一种手段。通过接口的特性隔离客户端不需要的接口方法。

那如何隔离呢?

假设有一个接口类提供增、删、改、查四个方法
interface A {

    T find();
    void create();
    void delete();
    void update();
}

如果客户端只需要查询方法,那么接口类中的增、删、改方法是不需要暴露的。
那么如何隔离呢?创建另一个接口类专门提供查询方法。

interface Viewer {
    T find();
}

interface A extend Viewer {

    void create();
    void delete();
    void update();
}

现在只要给客户端Viewer接口类,就隔离了增、删、改方法。

接口隔离原则与单一职责原则很像,就是把接口的职责划分的更单一,但是初衷是不一样的,单一职责原则是类的设计原则,接口隔离原则是功能的隔离原则。

依赖反转原则(DIP)

依赖反转原则的英文翻译是“ Dependency Inversion Principle”,缩写为 DIP。Robert Martin 在 SOLID 原则中是这样定义它的:“High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.”。王争老师描述是这样的:高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

关于这个原则可以看我另一个文章《依赖反转原则》

 

 

除了SOLID原则,还有其他一些原则

KISS原则

KISS 原则的英文描述有好几个版本,比如下面这几个。

Keep It Simple and Stupid.

Keep It Short and Simple.

Keep It Simple and Straightforward.

不过,仔细看你就会发现,它们要表达的意思其实差不多,翻译成中文就是:尽量保持简单。

这个原则感觉很含糊,相信能写出的简单代码,谁都不想把它写的复杂。

感觉没有可操作性,更像是一种境界,就像“简约而不简单”、“化繁为简、无招胜有招”一样。当知识面够广,理论够扎实,经验够丰富,自然会更加的简单。

个人认为光简单是不够的,必须可读性好的代码就是真正简单的代码。

YAGNI原则

YAGNI 原则的英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。

这条原则也算是万金油了。当用在软件开发中的时候,它的意思是:不要去设计当前用不到的功能;不要去编写当前用不到的代码。实际上,这条原则的核心思想就是:不要做过度设计。

DRY 原则

DRY 原则英文描述为:Don’t Repeat Yourself。中文直译为:不要重复自己。将它应用在编程中,可以理解为:不要写重复的代码。

两种典型的代码重复:功能语义及实现逻辑重复、代码执行重复。

功能语义及实现逻辑重复的三种情形:

1、完全相同
class StringUtils{
    public boolean isEmpty(String str){
        return str == null || str.trim().length==0;
    }
}

class StrUtils{
    public boolean isEmpty(String str){
        return str == null || str.trim().length==0;
    }
}

2、方法名相同、实现不同
class StringUtils{
    public boolean isEmpty(String str){
        return str == null || "".equals(str.trim());
    }
}

class StrUtils{
    public boolean isEmpty(String str){
        return str == null || str.trim().length==0;
    }
}

3、方法名不同、实现不同
class StringUtils{
    public boolean isEmpty(String str){
        return str == null || "".equals(str.trim());
    }
}

class StrUtils{
    public boolean hasText(String str){
        return str == null || str.trim().length==0;
    }
}

虽然这两段代码方法名不同、实现不同,但是语义及执行结果是相同的,也是重复。


代码调用重复的情形:
class UserService {

    public User login(String username,String password){
        Assert.hasText(username, "用户名不能为空");
        Assert.hasText(password, "密码不能为空");
        User user = userService.findByUsername(username);
        //验证密码正确,此处省略....
        return user;
    }

    public User findByUsername(String username){
        Assert.hasText(username, "用户名不能为空");
        //查询用户,此处省略....
    }

}
用户判空 Assert.hasText(username, "用户名不能为空") 重复调用。



迪米特法则(LOD)

迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD。单从这个名字上来看,我们完全猜不出这个原则讲的是什么。不过,它还有另外一个更加达意的名字,叫作最小知识原则,英文翻译为:The Least Knowledge Principle。关于这个设计原则,我们先来看一下它最原汁原味的英文定义:Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.王争老师描述是这样的:每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。不过王争老师另一种描述更好理解:不该有直接依赖关系的类(模块)之间,不要有依赖;有依赖关系的类(模块)之间,尽量只依赖必要的接口(也就是定义中的“有限知识”)。

实际上迪米特法则只要做到两个原则(单一职责原则和接口隔离原则)基本就可以做到了。

  1. 不该有直接依赖关系的类之间,不要有依赖,也就是类的职责要单一,单一的职责才可以不依赖无关的类。
  2. 有依赖关系的类之间,尽量只依赖必要的接口,也就是通过接口进行隔离无关的接口方法;

所以要更好理解迪米特法则,可以先学习单一职责原则和接口隔离原则。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值