• 重新认识面向对象——Java写了五年,你真的弄明白什么是面向对象了吗?不,你一直都是在面向过程编程



    全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我

    什么是面向过程编程

    “面向过程”(Procedure Oriented,简称PO)是一种以过程为中心的编程思想。其原理就是将问题分解成一个一个详细的步骤,然后通过函数实现每一个步骤,并依次调用。

    面向过程,其实核心就是“过程”两个字。过程指的是解决问题的步骤,即先干什么、后干什么、再干什么、然后干什么……

    面向过程编程是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。

    面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。

    什么是面向对象编程

    面向对象编程中有两个非常重要、非常基础的概念,那就是类(class)和对象(object)。

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

    面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。

    面向对象编程相比较面向过程而言,多了很多巨大的优势:更加能够应对大规模复杂程序的开发;风格的代码更易复用、易扩展、易维护;语言更加人性化、更加高级、更加智能。

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

    用着面向对象语言,却一直干着面向过程的事

    springboot - web开发,也许你一直都在面向过程开发

    日常开发过程中,很多业务系统都是基于 MVC 三层架构来开发的,实际上,大部分朋友开发的 过程中,一直都是基于“贫血模型” 面向过程开发的,虽然定义了很多model、vo、po,但是不可否认,这的确是一种面向过程开发。

    虽然这种开发模式已经成为标准的 Web 项目的开发模式,但它却违反了面向对象编程风格,是一种彻彻底底的面向过程的编程风格,因此而被有些人称为反模式(anti-pattern)。特别是领域驱动设计(Domain Driven Design,简称 DDD)盛行之后,这种基于贫血模型的传统的开发模式就更加被人诟病。而基于充血模型的 DDD 开发模式越来越被人提倡。

    滥用getter、setter

    在之前参与的项目开发中,我经常看到,有同事定义完类的属性之后,就顺手把这些属性的 getter、setter 方法都定义上。有些同事更加省事,直接用 IDE 或者 Lombok 插件(如果是 Java 项目的话)自动生成所有属性的 getter、setter 方法。

    当我问起,为什么要给每个属性都定义 getter、setter 方法的时候,他们的理由一般是,为了以后可能会用到,现在事先定义好,类用起来就更加方便,而且即便用不到这些 getter、setter 方法,定义上它们也无伤大雅。

    实际上,这样的做法我是非常不推荐的。它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。

    提供了 public 的 getter、setter 方法,这就跟将这两个属性定义为 public 公有属性,没有什么两样了。外部可以通过 setter 方法随意地修改这两个属性的值。除此之外,任何代码都可以随意调用 setter 方法。这完全违反了面向对象的封装、访问权限控制。

    例如我们定义购物车,item表示购物车里面的商品,totalPrice记录总价格

    
    public class ShoppingCart {
      private int itemsCount;
      private double totalPrice;
      private List<ShoppingCartItem> items = new ArrayList<>();
      
      public int getItemsCount() {
        return this.itemsCount;
      }
      
      public void setItemsCount(int itemsCount) {
        this.itemsCount = itemsCount;
      }
      
      public double getTotalPrice() {
        return this.totalPrice;
      }
      
      public void setTotalPrice(double totalPrice) {
        this.totalPrice = totalPrice;
      }
    
      public List<ShoppingCartItem> getItems() {
        return this.items;
      }
    
      // ...省略其他方法...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    里面的所有参数都写了get、set方法,这意味着我们可以在外部任意操作每一个属性。
    比如说我们添加一个商品,或者清空购物车,可能在service中是这样实现的:

    ShoppingCart cart = new ShoppCart();
    ...
    
    cart.getItem().add(item); // 添加商品
    cart.setItemsCount(...); // itemsCount++;
    cart.setTotalPrice(...); // 计算总金额
    ...
    cart.getItems().clear(); // 清空购物车、计算总金额、计算总商品数
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    你可能会说,清空购物车这样的功能需求看起来合情合理啊,上面的代码没有什么不妥啊。你说得没错,需求是合理的,但是这样的代码写法,是典型的面向过程的编程,假如说清除购物车的代码,少写了一句计算总金额的逻辑,那就会直接导致购物车商品不正确;而且通过get方式获取的商品list,可以随意的往里面添加值删除值,这完全违背了面向对象的封装特性。

    而正确做法是,将这部分逻辑进行封装,取消不必要的set方法:

    
    public class ShoppingCart {
      private int itemsCount;
      private double totalPrice;
      private List<ShoppingCartItem> items = new ArrayList<>();
      
      public int getItemsCount() {
        return this.itemsCount;
      }
    
      public double getTotalPrice() {
        return this.totalPrice;
      }
    
      public List<ShoppingCartItem> getItems() {
        // 使用原型模式返回一个与原对象无关的对象,防止随意篡改
      }
      
      public void addItem(ShoppingCartItem item) {
        items.add(item);
        itemsCount++;
        totalPrice += item.getPrice();
      }
      // ...省略其他方法...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    基于“贫血模型”传统开发模式

    MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。它将整个项目分为三层:展示层、逻辑层、数据层。

    通常我们Model层只定义了接口的入口,使用@Controller接收参数;数据层一般连接数据库,写一些sql语句;service层才是真正的核心逻辑层。

    就像是上面滥用getter、setter的例子,相信各位小伙伴的项目代码中不占少数。

    这是一种典型的面向过程开发的模式,也就是“贫血模型”。

    像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。

    
    // Controller+VO(View Object) //
    public class UserController {
      private UserService userService; //通过构造函数或者IOC框架注入
      
      public UserVo getUserById(Long userId) {
        UserBo userBo = userService.getUserById(userId);
        UserVo userVo = [...convert userBo to userVo...];
        return userVo;
      }
    }
    
    public class UserVo {//省略其他属性、get/set/construct方法
      private Long id;
      private String name;
      private String cellphone;
    }
    
    // Service+BO(Business Object) //
    public class UserService {
      private UserRepository userRepository; //通过构造函数或者IOC框架注入
      
      public UserBo getUserById(Long userId) {
        UserEntity userEntity = userRepository.getUserById(userId);
        UserBo userBo = [...convert userEntity to userBo...];
        return userBo;
      }
    }
    
    public class UserBo {//省略其他属性、get/set/construct方法
      private Long id;
      private String name;
      private String cellphone;
    }
    
    // Repository+Entity //
    public class UserRepository {
      public UserEntity getUserById(Long userId) { //... }
    }
    
    public class UserEntity {//省略其他属性、get/set/construct方法
      private Long id;
      private String name;
      private String cellphone;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    基于“充血模型”的 DDD 开发模式

    领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。领域驱动设计这个概念并不新颖,早在 2004 年就被提出了,到现在已经有十几年的历史了。不过,它被大众熟知,还是基于另一个概念的兴起,那就是微服务。

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

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

    为什么“贫血模型”的面向过程盛行

    环境问题:

    • 近朱者赤,近墨者黑
      • 大多数人都是模仿别人的代码,而别人的代码基本上都是 demo,没有复杂的业务逻辑,基本是贫血模型
      • 找不到好的指导与学习对象
      • 思维固化,习以为常
    • 接触不到复杂业务项目
      • 做 web 项目的,很大一部分就是简单的 CURD,贫血模型就能解决
    • 公司以任务数来衡量个人价值

    个人问题:

    • 不考虑项目质量属性
      • 只关心当前业务,没有意识去思考后期该如何维护和响应业务变更
    • 求快不求质
      • 个人以任务数来自我满足
      • 没有 60 分和 100 分的概念
      • 需求分析、设计、编码合为一体

    努力向“充血模型”靠拢

    先说一下充血模型中各组件的角色:

    • controller 主要服务于非业务功能,比如说数据验证
    • service 服务于 use case,负责的是业务流程与对应规则
    • Domain 服务于核心业务逻辑和核心业务数据
    • rep 用于与外部交互数据

    吃透面向对象的基本思想,充分理解面向对象的设计原则与设计模式,让你的开发事半功倍。

    参考资料

    王争老师《设计模式之美》

  • 相关阅读:
    JavaScript——数据类型、类型转换
    es6新增-Promise详解(异步编程的解决方案1)
    DRM全解析 —— encoder详解(2)
    C++ 基础与深度分析 Chapter9 序列与关联容器(关联容器、适配器与生成器)
    2023年【北京市安全员-B证】考试试卷及北京市安全员-B证模拟考试题
    列表作为条件查询的参数
    socket can应用程序在发送时,怎么控制是标准帧还是扩展帧?
    第十八章《JDBC》第1节:JDBC简介
    第42节——路由知识额外扩展
    .NET周报 【5月第3期 2023-05-21】
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/127572713