Design Patterns: Object Oriented

Encapsulation

封装,也叫作信息隐藏或者数据访问保护,类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。

封装需要编程语言本身提供一定的语法机制来支持,即访问权限控制

封装的意义:

  • 保证某些被类保护的属性不会随意被修改,程序可控
  • 类仅仅通过有限的方法暴露必要的操作,能提高类的易用性

Abstraction

抽象,指如何隐藏方法的具体实现。不需要编程语言提供特定的语法机制来支持,有函数即可,抽象是一个非常通用的设计实现,并不单单用在OOP中,也可以用来指导架构设计等。

抽象的意义:

  • 提高代码的可扩展性、维护性,修改实现不需要修改定义,减少代码的改动范围
  • 是处理复杂系统的有效手段,能有效过滤掉不必要关注的信息

Inheritance

继承,用来表示类之间的is-a关系,需要编程语言提供特殊的语法机制来支持,可以分为:

  • 单继承:表示一个之类只继承一个父类
  • 多继承:表示一个之类可以继承多个父类

继承的意义,最大的好处是实现代码复用。

Polymorphism

多态,指之类可以替换父类,在实际额代码运行过程中,调用之类的方法实现。

多态特性的实现方式:

  • 继承加方法重写
  • 接口类语法
  • duck-typing语法:只要两个类具有相同的方法,就可以实现多态,并不需要类之间有任何关系,是一些动态语言所特有的语法机制

        class Logger:
            def record(self):
                print(“I write a log into file.”)
    
        class DB:
            def record(self):
                print(“I insert data into db. ”)
    
        def test(recorder):
                recorder.record()
    
        def demo():
            logger = Logger()
            db = DB()
            test(logger)
            test(db)
    

多态的意义:

  • 可以提高代码的可扩展性和复用性
  • 多态是很多设计模式、设计原则、编程技巧的代码实现基础

面向过程 vs 面向对象

面向过程和面向对象最基本的区别就是,代码的组织方式不同,面向过程风格的代码被组织成了一组方法集合及其数据结构,方法和数据结构的定义是很开的。面向对象风格的代码被组织成一组类,方法和数据结构被绑定在一起,定义在类中。

面向对象相比于面向过程的优势:

  • OOP更加能够应对大规模复杂程序的开发,其提供了一种更加清晰的、更加模块化的代码组织方式
  • OOP风格的代码更易复用、易扩展、易维护
  • OOP语言更加人性化、更加高级、更加智能

违反OOP的典型代码设计

  • 滥用getter、setter方法
    • 除非真的需要,否则尽量不要给属性定义setter方法
    • getter方法如果返回的是集合容器,也要防范集合内部数据被修稿的风险
  • 类的设计问题
    • Constants类
    • 滥用全局变量和全局方法
      • 在OOP中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法
      • 静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格
    • Utils类
    • 只包含静态方法不包含任何属性的utils类,是彻彻底底的面向过程的编程风格
    • 尽量做到职责单一、定义一些细小化的小类,而不是一些大而全的类
    • 尽量将这些类中的属性和方法,划分归并到其他业务类中,能极大提供类的内聚性和代码的可复用性
  • 基于贫血模型的开发模式
    • 定义数据和方法分离的类
    • 流行的原因:实现简单和上手快

Abstract Class vs Interface

抽象类的特性:

  • 抽象类不允许被实例化,只能被继承
  • 抽象类可以包含属性和方法
    • 方法可以包含代码实现,也可以不包含代码实现,即抽象方法
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法
  • 自下而上,先有之类的代码重复,然后再抽象成上层的父类,即抽象类

接口的特性:

  • 接口不能包含属性(也就是成员变量)
  • 接口只能声明方法,方法不能包含代码实现
  • 类实现接口的时候,必须实现接口中声明的所有方法
  • 自上而下,一般先设计接口,然后再去考虑具体的实现

抽象类是对成员变量和方法的抽象,表示一种is-a关系,为了解决代码复用问题。

接口仅仅是对方法的抽象,表示一种has-a关系,代表具有某些功能,对于接口,更加形象的叫法是协议(Contract),是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。

Program to an Interface, not an Implementation

基于接口而非实现编程,另一个表述方式是“基于抽象而非实现编程”, 我们在软件开发的时候,需要有抽象意识、封装意识,接口意识,越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性、扩展性、可维护性。

在定义接口时,命名要足够通用,不能包含跟具体实现相关的字眼,与特定实现有关的额方法不要定义在接口中。

Composition vs Inheritance

  • 继承(Inheritance):利用extends来扩展一个基类。is-a的关系。
  • 组合(composition):一个类的定义中使用其他对象。has-a的关系。
  • 委托(delegation):一个对象请求另一个对象的功能,捕获一个操作并将其发送到另一个对象。有uses-a, owns-a, has-a三种关系。

组合优于继承。

继承表示is-a关系,可以解决代码复用的问题,但是继承层次过深、过复杂,也会影响代码的可维护性,可以利用组合、接口、委托三个技术手段解决继承存在的这些问题。

继承主要的三个作用:表示is-a关系,支持多态特性,代码复用。组合能够解决层次过深、过复杂的继承关系影响代码可维护性的问题。

在实际项目开发中,我们需要根据具体的情况,来选择使用继承还是组合,如果类之间的继承结构稳定,层次比较浅,关系不复杂,就可以大胆使用继承,反之,系统越不稳定,继承层次很深,关系很复杂,就尽量使用组合来代替继承。

贫血 vs 充血

平时Web项目的业务开发,大部分是基于贫血模型(Anemic Domain Model)的MVC三层架构,贫血模型将数据与操作分离到不同的类中,破坏了对象的封装特性,是典型的面向过程的编程风格,也被有些人称为反模式(anti-pattern),适用于业务不复杂的系统开发。

基于充血模型的领域驱动设计(Domain Driven Design, 简称DDD)开发模式,将数据与对应的业务逻辑封装到同一个类中,是典型的面向对象的编程风格,因为前期需要在设计上投入更多的时间和精力,来提高代码的复用性和可维护性,所以其相比于基于贫血模型的开发模式,更加适用于复杂的系统开发。

DDD主要用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互,也是按照MVC三层架构分成的,与贫血模型的主要区别主要在Service层,在DDD中,将部分原来在Service类中的业务逻辑移动到了一个充血的Domain领域模型中,让Service类的实现依赖这个Domain类,Service类并不会完全移除,而是负责一些不适合放在Domain类中的功能。

基于贫血与充血模型的开发模式相比,Controller层和Repository层的代码基本上相同。

MVC三层架构:

  • M: Model,数据层
  • V: View,展示层
  • C: Controller,逻辑层

现在的Web或者App项目都是前后端分离的,后端负责暴露接口给前端调用,一般将后端项目分为:

  • Repository layer: 负责数据访问
  • Service layer: 负责业务逻辑
  • Controller layer: 负责暴露接口

基于贫血模型的传统开发模式,重Service轻BO;基于充血模型的DDD开发模式,轻Service重Domain。

为什么贫血模型更受欢迎

  • 大部分情况下,我们开发的系统业务可能都比较简单
  • 充血模型的设计要比贫血模型更加有难度
  • 思维已固化,转型有成本

类与类之间的交互关系

  1. 泛化(Generalization),可以简单理解为继承关系。
    public class A { ... }
    public class B extends A { ... }
  1. 实现(Realization),一般指接口和实现类之间的关系。
    public interface A { ... }
    public class B implement A { ... }
  1. 聚合(Aggregation)是一种包含关系,A类对象包含B类对象,B类对象的生命周期可以不依赖A类对象的生命周期,比如课程和学生。
    public class A {
    private B b;
    public A(B b) {
      this.b = b;
    }
    }
  1. 组合(Composition)也是一种包含关系,A类对象包含B类对象,B类对象的生命周期依赖A类对象的生命周期,B类对象不可单独存在,比如鸟与翅膀。

    public class A {
        private B b;
        public A() {
          this.b = new B();
        }
    }
    
  2. 关联(Association)是一种非常弱的关系,包含聚合、组合两种关系。

  3. 依赖(Dependency)是一种比关联关系更加弱的关系,包含关联关系。不管B类对象是A类对象的成员变量,还是A类的方法使用B类对象作为参数负责返回值、局部变量,只要B类对象和A类对象有任何关系,都称它们为有依赖关系。

    public class A {
      private B b;
      public A(B b) {
        this.b = b;
      }
    }
    或者
    public class A {
      private B b;
      public A() {
        this.b = new B();
      }
    }
    或者
    public class A {
      public void func(B b) { ... }
    }
    

总结

面向对象分析的产出是详细的需求描述、面向对象设计的产出是类。面向对象设计可以拆分为:

  1. 划分职责进而识别出有哪些类
  2. 定义类及其属性和方法a
  3. 定义类与类之间的交互关系
  4. 将类组装起来并提供执行入口
    1. 可能是一个main()函数
    2. 一组给外部调用的API接口

Reference

Note: Cover Picture