美文网首页
设计模式 - 面向对象

设计模式 - 面向对象

作者: Zeppelin421 | 来源:发表于2022-04-17 10:17 被阅读0次

    面向过程

    什么是面向过程编程

    相较于面向对象编程以类为组织代码的基本单元,面向过程编程则是以过程(或方法)作为组织代码的基本单元。它最主要的特点就是数据和方法相分离。相较于面向对象编程语言,面向过程编程语言最大的特点就是不支持丰富的面向对象编程特性,比如继承、多态、封装。

    面向对象

    什么是面向对象编程和面向对象编程语言

    • 面向对象编程(OOP,Object Oriented Programming)
      是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性作为代码设计和实现的基石。
    • 面向对象编程语言(OOPL,Object Oriented Programming Language)
      是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性的编程语言

    如何判定一个编程语言是否是面向对象编程语言

    只要某种编程语言支持类或对象的语法概念,并且以此作为组织代码的基本单元,那就可以被粗略地认为它就是面向对象编程语言了。至于是否有现成的语法机制,完全地支持了面向对象编程的四大特性、是否对四大特性有所取舍或优化,可以不作为判定的标准。

    面向对象编程和面向对象编程语言之间的关系

    面向对象编程一般使用面向对象编程语言来进行,但是不用面向对象编程语言,照样可以进行面向对象编程。反过来讲,即使我们使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也有可能是面向过程编程风格的。

    什么是面向对象分析和面向对象设计

    面向对象分析(OOA,Object Oriented Analysis)就是要搞清楚做什么,面向对象设计(OOD,Object Oriented Design)就是要搞清楚怎么做。这两个阶段最终的产出是类的设计,包括程序被拆解为哪些类,每个类有哪些属性方法、类与类之间如何交互等等。

    面向对象软件开发要经历的三个阶段:OOA就是要搞清楚做什么;OOD就是要搞清楚怎么做;OOP就是将分析和设计的结果翻译成代码的过程

    面向过程 VS 面向对象

    面向对象编程相比面向过程编程的优势

    • 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发
    • 面向对象编程相比面向过程编程,具有更加丰富的特性。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。
    • 从编程语言跟机器打交道的演化规律中,面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能。

    哪些代码设计看似面向对象,实际是面向过程的

    • 滥用getter、setter方法
      它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。
      在设计实现类的时候,除非真的需要,否则尽量不要给属性定义setter方法。getter方法虽然相对安全些,但如果返回的是集合容器,也需要防范集合内部数据被修改的风险
    • 滥用全局变量和全局方法
      全局变量(单例类对象、静态成员变量、常量)通常被设计在Constants类中;全局方法通常设计在Utils类中。全局方法将方法和数据分离,破坏了封装特性,也是典型的面向过程风格。
      面对这两种设计,我们尽量能做到职责单一,定义一些细化的小类,比如RedisConstants,FileUtils,不是定义一个大而全的Constants类、Utils类。除此之外,如果能将这些类中的属性和方法,划分归并到其他业务类中,能极大地提高类的内聚性和代码的可复用性
    • 定义数据和方法分离的类
      传统的MVC叫作基于贫血模型的开发模式,这就是典型的数据与方法分离的类。
      采用DDD基于充血模型的开发模式,将数据和对应的业务逻辑封装到同一个类中。

    抽象类 VS 接口

    特性

    • 抽象类
      抽象类不允许被实例化,只能被继承。
      抽象类可以包含属性和方法,方法既可以包含代码实现,也可以不包含。不包含代码实现的叫抽象方法
      子类继承抽象类,必须实现抽象类中的所有抽象方法
    • 接口
      接口不能包含属性
      接口只能声明方法,方法不能包含代码实现
      类实现接口的时候,必须实现接口中声明的所有方法

    抽象类实际上就是类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承,表示一种 is-a 的关系。接口表示一种 has-a 的关系,表示具有某些功能。对于接口,有一个更加形象的叫法,那就是协议(contract)

    意义

    • 抽象类
      抽象类不能被实例化,只能被继承。继承能解决代码复用的问题,所以抽象类也是为代码复用而生的。
      抽象类是一种特殊的类,所以抽象类能解决的问题,使用普通类也能解决。比如抽象方法可以使用普通类的空方法代替,但普通空方法带来的问题是:影响代码的可读性,如果不了解背后的设计思想,必须通过查看子类才能明白其设计意图;在几百行的代码中可能会漏掉某个空方法的重写导致故障;普通类可以被实例化,这样会导致被误用的风险,虽然可以通过私有构造函数来解决,但不够优雅。
    • 接口
      接口是对行为的一种抽象,相当于一组协议或契约,所以接口更侧重于解耦。
      接口实现了约定和实现相分离,可以降低代码间的耦合性,能极大提高代码的灵活性、扩展性。

    应用场景

    • 抽象类
      如果要表示一种 is-a 的关系,并且是为了解决代码复用问题
    • 接口
      如果要表示一种 has-a 的关系,并且是为了解决抽象而非代码复用问题

    基于接口而非实现编程

    “Program to an interface, not an implementation”
    这条原则最早出现在 1994 年 GoF 的《设计模式》一书中,它先于很多编程语言,是一条比较抽象、泛化的设计思想。

    此处的“接口”二字从本质上讲,就是一组“协议”或“约定”,是功能提供者提供给使用者的一个“功能列表”。落实到具体的编码中,可以理解为编程语言中的接口或抽象类。

    “基于接口而非实现编程”这条原则的另一个表述是“基于抽象而非实现编程”。在软件开发中,最大的挑战之一就是需求的不断变化。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。

    public class AliyunImageStore {
      //...省略属性、构造函数等... 
      public void createBucketIfNotExisting(String bucketName) {
        // ...创建bucket代码逻辑...
        // ...失败会抛出异常.. 
      }
      public String generateAccessToken() {
        // ...根据accesskey/secrectkey等生成access token
      }
      public String uploadToAliyun(Image image, String bucketName, String accessToken) {
        //...上传图片到阿里云...
        //...返回图片存储在阿里云上的地址(url)...  
      }
      public Image downloadFromAliyun(String url, String accessToken) {
        //...从阿里云下载图片...
      }
    }
    

    遵从“基于接口而非实现编程”的原则,需要做到以下三点:

    • 函数的命名不能暴露任何实现细节
      比如:uploadToAliyun()就不符合要求,应该改为更加抽象的命名方式,upload()
    • 封装具体的实现细节
      比如:跟阿里云相关的特殊上传流程不应该暴露给调用者,createBucketIfNotExisting 或 generateAccessToken。我们应该对上传流程进行封装,对外提供一个包括所有上传细节的方法,给调用者使用
    • 为实现类定义抽象的接口
      具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程

    在软件开发的时候,一定要有抽象意识、封装意识、接口意识。在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。

    是否需要为每个类定义接口

    做任何事情都要讲一个“度”,过度使用这条原则,非得给每个类都定义接口,也会导致不必要的开发负担。

    这条原则的设计初衷是,将接口和实现分离,封装不稳定的实现,暴露稳定的接口。如果在业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以了。

    组合优于继承

    • 为什么不推荐使用继承
      继承用来表示类之间的 is-a 关系,可以解决代码复用问题。但继承层次过深,过复杂,也会影响到代码的可维护性。这种情况下,尽量少用甚至不用继承

    • 组合相比继承有哪些优势
      继承主要有三个作用:表示 is-a 关系;支持多态特性;代码复用。这三个作用都可以通过组合、接口、委托三个技术手段来达成,除此之外,利用组合能解决层次过深、过复杂的继承关系影响代码可维护性的问题

    public interface Flyable {
      void fly();
    }
    public class FlyAbility implements Flyable {
      @Override
      public void fly() { //... }
    }
    //省略Tweetable/TweetAbility/EggLayable/EggLayAbility
    
    public class Ostrich implements Tweetable, EggLayable {//鸵鸟
      private TweetAbility tweetAbility = new TweetAbility(); //组合
      private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
      //... 省略其他属性和方法...
      @Override
      public void tweet() {
        tweetAbility.tweet(); // 委托
      }
      @Override
      public void layEgg() {
        eggLayAbility.layEgg(); // 委托
      }
    }
    
    • 如何判断该用组合还是继承
      如果类之间的继承结构稳定、层次比较浅、关系不复杂,就可以大胆地使用继承。反之就尽量使用组合来替代继承。

    基于MVC贫血模式 VS 基于DDD充血模式

    什么是基于贫血模型的传统开发模式

    在MVC三层架构中,UserEntity 和 UserRepository 组成了数据访问层;UserBO 和 UserService 组成了业务逻辑层;UserVO 和 UserController 组成接口层。UserBO 是一个纯粹的数据结构,只包含数据,不包含业务逻辑。业务逻辑集中在 UserService 中。

    像 UserBO 这样只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。这种贫血模型将数据与操作分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。

    什么是基于充血模型的DDD开发模式

    充血模型(Rich Domain Model)与之相反,数据和对应的业务逻辑被封装到同一个类中。因此充血模型满足面向对象的封装特性,是典型的面向对象编程风格。

    DDD(领域驱动设计)主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。

    基于贫血模式的传统开发中,Service 层包含 Service 类和 BO 类,基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于 BO。不过 Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。总结一下就是,贫血模型重 Service 轻 BO;充血模型轻 Service 重 Domain。

    为什么基于贫血模式的传统开发受欢迎

    • 大部分情况下,系统业务可能都比较简单,都是基于 SQL 的 CRUD 操作,贫血模式足以应付。
    • 充血模型的设计要比贫血模型更加有难度。因为充血模型从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是想贫血模型那样只需要定义数据,之后有什么功能直接在 Service 层定义什么操作
    • 思维固话,转型有成本

    什么项目应该考虑使用充血模型的DDD开发模式

    对于业务不复杂的系统来说,基于贫血模型的传统开发模式简单够用。
    对于业务复杂的系统开发来说,基于充血模型的DDD开发模式,因为前期需要在设计上投入更多时间和精力,来提高代码的复用性和可维护性,所以相比于贫血模型的开发模式,更加有优势。

    Controller 层和 Repository 层有必要也进行充血领域建模吗?

    Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多,即便是设计成充血模型,类也非常单薄,看起来也很奇怪,所以没必要进行充血领域建模。

    尽管这样的设计是一种面向过程的编程风格,但只要控制好面向过程编程风格的副作用,一样可以开发出优秀的软件。

    • Repository 的 Entity 被设计成贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但 Entity 生命周期是有限的。一般来说,传递到 Service 层之后就会转化为 BO 或 Domain,Entity 的生命周期到此就结束了,所以并不会被到处任意修改
    • Controller 层的 VO实际上是一种 DTO,它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。所以将它设计成贫血模型是比较合理的。

    相关文章

      网友评论

          本文标题:设计模式 - 面向对象

          本文链接:https://www.haomeiwen.com/subject/wkokertx.html