设计模式之美-课程笔记2

理论三:面向对象的优势以及面向过程编程的意义

  1. 什么是面向过程编程以及面向过程编程语言?
  2. 面向对象编程相比于面向过程编程的优势?
  3. 为什么说POP更高级?
  4. 有哪些看似是OO其实是PO风格的代码?
  5. 在面向对象编程中,为什么更容易写出面向过程风格的代码?
  6. OPP和PP没用武之地了吗?

什么是面向过程编程以及面向过程编程语言

类比于OOD和OOP,定义如下:

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

看例子对比下

面向过程,按照步骤(动作)一步步来操作数据。方法和数据的定义分开。

struct User {
  char name[64];
  int age;
  char gender[16];
};

struct User parse_to_user(char* text) {
  // 将text(“小王&28&男”)解析成结构体struct User
}

char* format_to_text(struct User user) {
  // 将结构体struct User格式化成文本("小王\t28\t男")
}

void sort_users_by_age(struct User users[]) {
  // 按照年龄从小到大排序users
}

void format_user_file(char* origin_file_path, char* new_file_path) {
  // open files...
  struct User users[1024]; // 假设最大1024个用户
  int count = 0;
  while(1) { // read until the file is empty
    struct User user = parse_to_user(line);
    users[count++] = user;
  }
  
  sort_users_by_age(users);
  
  for (int i = 0; i < count; ++i) {
    char* formatted_user_text = format_to_text(users[i]);
    // write to new file...
  }
  // close files...
}

int main(char** args, int argv) {
  format_user_file("/home/zheng/user.txt", "/home/zheng/formatted_users.txt");
}

面向对象:分析得到对象,给对象几个方法(操作对象)。

 public class User {
  private String name;
  private int age;
  private String gender;
  
  public User(String name, int age, String gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
  
  public static User praseFrom(String userInfoText) {
    // 将text(“小王&28&男”)解析成类User
  }
  
  public String formatToText() {
    // 将类User格式化成文本("小王\t28\t男")
  }
}

public class UserFileFormatter {
  public void format(String userFile, String formattedUserFile) {
    // Open files...
    List users = new ArrayList<>();
    while (1) { // read until file is empty 
      // read from file into userText...
      User user = User.parseFrom(userText);
      users.add(user);
    }
    // sort users by age...
    for (int i = 0; i < users.size(); ++i) {
      String formattedUserText = user.formatToText();
      // write to new file...
    }
    // close files...
  }
}

public class MainApplication {
  public static void main(String[] args) {
    UserFileFormatter userFileFormatter = new UserFileFormatter();
    userFileFormatter.format("/home/zheng/users.txt", "/home/zheng/formatted_users.txt");
  }
}

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

1. OOP更能应对大规模复杂程序的开发

大规模程序的处理流程会成为一张网状图错综复杂。主线也不止一条。

OOP允许我们先建模,再考虑他们的行为,最后才是按照流程把他们搭建。相比更清晰容易一点。另外,类天然作为组织数据对象和行为的一个载体,较好的帮我们组织代码,形成模块化。

2. OOP的代码更加易复用、易扩展、易维护

OOP的四大特性让这些实现变得更容易。

3. OOP更人性化、更高级、更智能

更贴近人类的思路。(相比于指令化顺序运行。)

理论四:哪些代码设计看似是面向对象,其实是面向过程?

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

1. 滥用Getter和Setter

一段代码分析

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;
  }
  
  public void addItem(ShoppingCartItem item) {
    items.add(item);
    itemsCount++;
    totalPrice += item.getPrice();
  }
  // ...省略其他方法...
}

这个代码中,我们定义了三个变量,对于itemsCounttotalPrice,我们定义了Getter和Setter,对于items,我们定义了GetteraddItem

  1. 乍一看没问题,仔细一想(通过业务逻辑),前两个变量的Setter方法提供了修改的途径,可能导致被不正确修改后,与iterms里属性不一致。面向对象封装,就是通过控制访问权限,隐藏内部数据,暴露有限的接口访问、修改内部数据。

  2. Items是一个集合,他的Getter返回的是一个集合容器List,外部调用者拿到它之后仍然可以改变。比如

    ShoppingCart cart = new ShoppCart();
    ...
    cart.getItems().clear(); // 清空购物车
    

    同样的,清空购物车可能会导致数据不一致与总数量和价格。我们应该定义一个clear方法,将清空逻辑封装在里面, 透明的给使用者调用。

    public class ShoppingCart {
      // ...省略其他代码...
      public void clear() {
        items.clear();
        itemsCount = 0;
        totalPrice = 0.0;
      }
    }
    
  3. 此时我们又要获取到items,获取购物车列表,怎么办?Java中提供了一个Collections.unmodifiableList()方法,这个方法重写了List容器中修改相关的方法add(),clear(),一旦调用这些,会抛出一个UnsupportedOperationException异常。因此实现让Getter返回一个不可变容器。

    public class ShoppingCart {
      // ...省略其他代码...
      public List<ShoppingCartItem> getItems() {
        return Collections.unmodifiableList(this.items);
      }
    }
    
    public class UnmodifiableList<E> extends UnmodifiableCollection<E>
                              implements List<E> {
      public boolean add(E e) {
        throw new UnsupportedOperationException();
      }
      public void clear() {
        throw new UnsupportedOperationException();
      }
      // ...省略其他代码...
    }
    
    ShoppingCart cart = new ShoppingCart();
    List<ShoppingCartItem> items = cart.getItems();
    items.clear();//抛出UnsupportedOperationException异常
    
  4. 还有一个问题,虽然改变不了容器,但是对于每个对象的属性,我们还是可以更改。e.g.

    ShoppingCart cart = new ShoppingCart();
    cart.add(new ShoppingCartItem(...));
    List<ShoppingCartItem> items = cart.getItems();
    ShoppingCartItem item = items.get(0);
    item.setPrice(19.0); // 这里修改了item的价格属性
    

总结下来就是,除非真的需要,尽量不定义Setter方法。除此之外,Getter返回集合容器的时候也要注意。

2. 滥用全局变量和全局方法

  1. 全局变量:单例类对象,静态成员变量,常量。常见的全局方法有静态方法。静态方法一般用来操作变量或者外部数据。(数据和方法分类,不符合封装特性)->面向过程风格。

  2. 例子看看(常量)

    public class Constants {
      public static final String MYSQL_ADDR_KEY = "mysql_addr";
      public static final String MYSQL_DB_NAME_KEY = "db_name";
      public static final String MYSQL_USERNAME_KEY = "mysql_username";
      public static final String MYSQL_PASSWORD_KEY = "mysql_password";
      
      public static final String REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
      public static final int REDIS_DEFAULT_MAX_TOTAL = 50;
      public static final int REDIS_DEFAULT_MAX_IDLE = 50;
      public static final int REDIS_DEFAULT_MIN_IDLE = 20;
      public static final String REDIS_DEFAULT_KEY_PREFIX = "rt:";
      
      // ...省略更多的常量定义...
    }
    

    代码的可维护性变差。大家都在往这个常量类中增加内容,这个类会变得很大,查找费时,代码提交容易冲突。

    增加了代码的编译时间。

    依赖这个类的代码越来越多,每次修改这个Constant类,依赖他的类都得重新编译,浪费时间。可能影响开发效率。

    影响代码的复用性。比如在另一个项目中要复用一个类,而这个类依赖这个Constant,那么又得将整个Constant一并引入。(许多无关的常量)

  3. 如何改进?

    1. 拆分。mysql相关的就拆成MySQLConstant,Redis就是RedisConstant。
    2. 或者不定义Constant,而是哪个类使用这个常量,就将它定义在那个类中。比如RedisConfig中定义Redis相关的常量。提高了类设计的内聚性和代码的复用性。
  4. 另一个例子(Utils)

    Utils类存在的意义就是复用通用的工具。

    我们也知道,继承可以提供代码复用,但是有时两个类不一定具有继承关系。所以定义一个静态类,来使用的时候拼接到合适的地方。

    这个其实是非常“面向过程”的。但是他确实解决了开发中的问题,所以我们辩证对待,不能滥用而非不能用。认真思考是否需要定义这种类每次。

    你需要单独定义util类吗?是否可以将utils中的方法放到其他类?都回答完这些问题再做决定。

    我们的最终目的是写出合适的代码以尽可能小的代价。

    utils也可以像Constant一样适当的细化拆分。

3. 定义数据和方法分离的类

  1. 数据在一个类,操作数据的方法在另一个类。

    后端开发MVC中到处都是-> _->

    传统的 MVC 结构分为 Model 层、Controller 层、View 层这三层。不过,在做前后端分离之后,三层结构在后端开发中,会稍微有些调整,被分为 Controller 层、Service 层、Repository 层。Controller 层负责暴露接口给前端调用,Service 层负责核心业务逻辑,Repository 层负责数据读写。而在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格

  2. 这种叫做基于贫血模型的开发模式。后续会讲。

在面向对象编程中,为什么容易写出面向过程风格的代码?

面向过程更符合一种流程化的思想(相对简单),面向对象自底向上,先将任务和实体分解,理清之间的交互再组装。

面向过程编程及面向过程编程语言就真的无用武之地了吗?

  1. 微小程序;
  2. 数据处理;
  3. 以算法为主,数据为辅助,面向过程更适合一点;
  4. 面向过程有点像面向对象的基础;
  5. 辩证看待两种风格的配合。最终目的是写出好代码。易用易拓展,易读易维护。