• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

无信不立

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

【重温设计模式】面向编程的设计思想和设计原则

一、 面向编程的理解

1、什么是面向对象编程

  • 面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。

2、抽象类和接口

  • 抽象类:表示类之间的 is-a 关系,不能被实力化,解决代码复用问题,同时保持多态特性。  自下而上的行为,先发现子类逻辑重复,再抽象到抽象类。
  • 接口:表示解藕,对行为的抽象,基于接口而非实现的编程思想。提高程序的可扩展性,和灵活性。自上而下的设计行为,先定义规则,再去实现。

3、接口(面向接口而非实现编程思想)

  • 接口是一组协议,或者约定。是功能提供者提供给使用者的一组功能列表。
  • 将接口和实现分离,封装不稳定的实现,暴露稳定的接口给上游系统,上游系统面向稳定的接口而非实现编程,不依赖不稳定的实现细节,当实现发生变化时,上游系统不用改动,以此降低耦合性,提高代码扩展性。
  • 接口的定义,要抽象,共性,不要有特殊性的定义。

4、多用组合少用继承

(1)继承

  • 继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。这也是为什么我们不推荐使用继承;
  • 如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。

 

(2)组合

  • 利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。
  • 继承层次深,且容易变化,利用组合还能解决层次过深、过复杂的继承关系影响代码可维护性的问题。

 

基于继承实现的代码示意

 View Code

基于组合实现的代码示意(组合、接口、委托)

 View Code

 

二、 面向编程的模式区别

1、MVC的贫血模型(违反面向对象编程的思想)

  • MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。
  • 将后端项目分为 Repository 层(数据库访问, entity数据模型)、Service 层(业务处理,BO数据模型)、Controller 层(接口暴露,DTO数据模型)
  • BO模型和service层的将数据和业务逻辑分离的模式,就是贫血模型

2、DDD的充血模型 (符合面向对象编程思想)

  • DDD充血模型,将业务分为 Repository 层(数据库访问,Entity数据模型) ,Service 层(业务处理,Domain领域模型),Controller 层(接口暴露,DTO数据模型)
  • Domain领域模型(既包含数据,也包含数据处理业务逻辑),数据和行为封装在类中。Service层负责一些不适合放在 Domain 类中的功能
  • 代码示例:
public class VirtualWallet {
    // Domain领域模型(充血模型)
    private Long id;
    private Long createTime = System.currentTimeMillis();
    ;
    private BigDecimal balance = BigDecimal.ZERO;

    public VirtualWallet(Long preAllocatedId) {
        this.id = preAllocatedId;
    }

    public BigDecimal balance() {
        return this.balance;
    }

    public void debit(BigDecimal amount) {
        if (this.balance.compareTo(amount) < 0) {
            throw new InsufficientBalanceException(...);
        } this.balance.subtract(amount);
    }

    public void credit(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidAmountException(...);
        } this.balance.add(amount);
    }
}


//Service层
public class VirtualWalletService {
    // 通过构造函数或者IOC框架注入
    private VirtualWalletRepository walletRepo;
    private VirtualWalletTransactionRepository transactionRepo;

    public VirtualWallet getVirtualWallet(Long walletId) {
        VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
        VirtualWallet wallet = convert(walletEnticonvert(walletEntity); 
        return wallet;
    }

    public BigDecimal getBalance(Long walletId) {
        return walletRepo.getBalance(walletId);
    }

    public void debit(Long walletId, BigDecimal amount) {
        VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
        VirtualWallet wallet = convert(walletEntity);
        wallet.debit(amount);
        walletRepo.updateBalance(walletId, wallet.balance());
    }

    public void credit(Long walletId, BigDecimal amount) {
        VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
        VirtualWallet wallet = convert(walletEntity);
        wallet.credit(amount);
        walletRepo.updateBalance(walletId, wallet.balance());
    }

    public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { 
        //...跟基于贫血模型的传统开发模式的代码一样... }
    }
}
View Code

3、MVC的贫血模型和DDD充血模型的区别

基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,主要区别在 Service 层。在基于充血模型的开发模式下,我们将部分原来在 Service 类中的业务逻辑移动到了一个充血的 Domain 领域模型中,让 Service 类的实现依赖这个 Domain 类。

在基于充血模型的 DDD 开发模式下,Service 类并不会完全移除,而是负责一些不适合放在 Domain 类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功能、幂等事务等非功能性的工作。

基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,Controller 层和 Repository 层的代码基本上相同。这是因为,Repository 层的 Entity 生命周期有限,Controller 层的 VO 只是单纯作为一种 DTO。两部分的业务逻辑都不会太复杂。业务逻辑主要集中在 Service 层。所以,Repository 层和 Controller 层继续沿用贫血模型的设计思路是没有问题的。

 

三、 面向编程的五大设计原则

1、单一职责原则

定义:一个类或者模块只负责完成一个职责(或者功能) 【模块和类都可以以单一职责原则进行合理化设计的标准】

该原则关注点:一个类或模块的设计的职责是否单一,单一的背后代表着功能的内聚,与程序的其他类和模块是解耦的。

如何判断一个类的职责是否单一的标准

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

 

2、开闭原则

定义:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。【扩展和修改的定义,要基于看问题的视角,修改关闭(改动和修改影响原处理逻辑)】

该原则关注的点:对代码的修改,尽可能不影响已存在的代码逻辑,只是需要新增类和对原有数据结构进行属性添加可完成需求变化。

 

如何判断设么是对扩展开放、对修改关闭

  • 识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。
  • 添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。关于定义,我们有两点要注意。第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。

 

3、里氏替换原则

定义:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

该原则关注的点:类的继承和重写方法的标准。重写的父类方法的实现不违背父类定义方法的语义。

 

如何判断代码是否符合里氏替换原则

  • 子类违背父类声明要实现的功能
  • 子类违背父类对输入、输出、异常的约定
  • 子类违背父类注释中所罗列的任何特殊说明

 

 

4、接口隔离原则

  • 如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
  • 如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
  • 如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
  • 单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考的角度也是不同的。
  • 接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

 

5、依赖倒置原则(高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不需要依赖具体实现细节,具体实现细节依赖抽象。)

  • 控制反转:实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。
  • 依赖注入:依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或“注入”)给类来使用。
  • 依赖注入框架:我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。比如Spring框架
  • 依赖反转原则:依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不需要依赖具体实现细节,具体实现细节依赖抽象。

 

6、KISS(简单原则),YAGNI(不需要设计不需要的功能)

KISS原则:简单(指导如何做)

  • 不要使用同事可能不懂的技术来实现代码;
  • 不要重复造轮子,善于使用已经有的工具类库;
  • 不要过度优化。

YAGNI 原则:(指导不要做)

  • 英文全称是:You Ain’t Gonna Need It。直译就是:你不会需要它。核心思想就是:不要做过度设计。

7、DRY 原则(不重复原则)

  • 实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则
  • 实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。
  • 而代码执行重复也算是违反 DRY 原则。

 

8、迪米特法则,最小知识原则(不改有直接依赖关系的类之间,不要有依赖,有依赖关系的类之间,尽量只依赖必要的接口)

  • “高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓“松耦合”指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
  • 迪米特法则的描述为:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少

posted on 2020-07-14 22:12  无信不立  阅读(207)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3