设计原则与思想:面向对象


设计原则与思想:面向对象

理论一:面向对象

概念:面向对象编程是一种编程范式或编程风格,它以类或对象作为组织代码的基本单元
特性:封装、抽象、继承、多态
区别:面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析设计的结果翻译成代码的过程

理论二:封装、抽象、继承、多态

  • 封装
    • What:隐藏信息,保护数据访问(如private)
    • How:暴露有限接口和属性,需要编程语音提供访问控制的语法(如Jva提供private/protected/public等)
    • Why:提高代码可维护性,降低接口复杂度,提高类的易用性
    • 白话总结:可通过暴露出来的方法来访问数据(如public),而屏蔽其不可修改的属性(如private)
  • 抽象
    • What:隐藏具体实现,使用者只需关心功能,无需关心实现
    • How:通过接口类(interface)或者抽象类(abstract)实现,特殊语法机制非必需
    • Why:提高代码的扩展性、维护性,降低复杂度,减少细节负担
    • 白话总结:使用者只需关心功能,无需关心实现
  • 继承
    • What:表示is-a关系(指的是类的父子继承关系),分为单继承(如Java)和多继承(如C++)
    • How:需要编程语言提供特殊语法机制(如Java的extend,C++的:)
    • Why:解决代码复用问题
    • 白话总结:比如Cat继承自Animal,父类(Animal)有动物的共同特性(如吃饭睡觉),子类(Cat)可有单独的特性(如卖萌)
  • 多态
    • What:子类替换父类,在运行时调用子类的实现
    • How:需要编程语言提供特殊语法技术(如支持继承、支持父类引用子类、支持子类重写父类方法等)
    • Why:提高代码扩展性和复用性
    • 白话总结:如Animal animal = new Cat();,调用animal的方法反映的是Cat的特性
      public class Animal {
      private String name;
      public String getName(){return this.name;}
      public void setName(String name){this.name = name;}
      //叫声
      public String shout(){return "default";}
      }
      
      public class Cat extends Animal{
      @Override
      public String getName() {
        //不同子类会有自己的name(多态)
        return "猫";
      }
      @Override 
      public String shout() {
        //不同子类会有自己的叫声(多态)
        return "喵";
        }
       }

理论三:面向对象相比面向过程有哪些优势?

  • 概念

    • 面向对象编程以类为组织代码的基本单元
    • 面向过程编程以过程/方法作为租住代码的基本单元
  • 面向对象编程比起面向过程编程的优势

    • 更能应对复杂类型的程序开发(对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结果)
    • 编写的代码更加易扩展、易复用、易维护(具有丰富的特性如封装、抽象、继承、多态)
    • 更加人性化、更加高级、更加智能(从编程语言与机器打交道的方式演进规律总结)
  • 代码区别例子

    • 面向过程编程(如C语言)
      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");
      }
    • 面向对象编程(Java语言)
      
       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.滥用getter、setter方法

背景/现象:大部分情况为了快速开发,都会给类所有属性都加上getter、setter方法(或者使用Lombok注解)
优化:设计实现类的时候,非必要的时候尽量不要给属性定义setter方法(避免乱修改),同时如果getter返回的是集合容器也需防范集合内部数据被修改的风险(如List可以取到其中的对象修改属性如name)
优点:尽可能保证数据安全
优化前的例子

/**
 * 购物车
 */
public class ShoppingCar {
  private double totalPrice;//总金额
  private int itemCount;//商品数量
  private List<ShoppingItem> items;//商品列表

  public double getTotalPrice() {
    return totalPrice;
  }

  //虽然totalPrice属性定义了private,但是却提供了public的set方法,导致totalPrice可以被随意修改(不符合逻辑)
  public void setTotalPrice(double totalPrice) {
    this.totalPrice = totalPrice;
  }

  public int getItemCount() {
    return itemCount;
  }

  //虽然itemCount属性定义了private,但是却提供了public的set方法,导致itemCount可以被随意修改(不符合逻辑)
  public void setItemCount(int itemCount) {
    this.itemCount = itemCount;
  }

  public List<ShoppingItem> getItems() {
    //返回一个不可修改的集合容器(防止外部直接操作容器如add、clear等)
    return Collections.unmodifiableList(this.items);
  }

  //虽然items属性定义了private,但是却提供了public的set方法,导致items可以被随意修改(不符合逻辑)
  public void setItems(List<ShoppingItem> items) {
    this.items = items;
  }
}

优化后的例子

/**
 * 购物车
 */
public class ShoppingCar {
  private double totalPrice;//总金额
  private int itemCount;//商品数量
  private List<ShoppingItem> items;//商品列表

  public double getTotalPrice() {
    return totalPrice;
  }

  public int getItemCount() {
    return itemCount;
  }

  public List<ShoppingItem> getItems() {
    return items;
  }

  public void addItem(ShoppingItem item){
      itemCount++;
      totalAmount+=item.getPrice();
      items.add(item);
  }
  
  //...省略其他代码
}

2.Constants类、Utils类的设计问题

背景/现象:平时为了统一管理常量,我们会定义一个大而全的Constants类、Utils
优化:定义细化的小类,如RedisConstants类、FileUtils类,一个类只负责一个/多个同场景的功能
优点:尽量做到职责单一,提高类的内聚性和代码的可复用性
优化前的例子

public class Constants {
  /**
   * mysql相关常量
   */
  public static final String MYSQL_ADDRESS = "127.0.0.1";
  public static final String MYSQL_PORT = "8086";
  public static final String MYSQL_USERNAME = "root";
  public static final String MYSQL_PASSWORD = "admin";

  /**
   * rabbitmq相关常量
   */
  public static final String RABBITMQ_QUEUE = "rabbitmq_queue";
  public static final String RABBITMQ_EXCHANGE = "rabbitmq_exchange";
  public static final String RABBITMQ_ROUTING_KEY = "rabbitmq_routing_key";
}

优化后的代码

public class MysqlConstants {
  /**
   * mysql相关常量
   */
  public static final String MYSQL_ADDRESS = "127.0.0.1";
  public static final String MYSQL_PORT = "8086";
  public static final String MYSQL_USERNAME = "root";
  public static final String MYSQL_PASSWORD = "admin";
}
public class RabbitmqConstants {
  /**
   * rabbitmq相关常量
   */
  public static final String RABBITMQ_QUEUE = "rabbitmq_queue";
  public static final String RABBITMQ_EXCHANGE = "rabbitmq_exchange";
  public static final String RABBITMQ_ROUTING_KEY = "rabbitmq_routing_key";
}

3.基于贫血模型的开发模式

背景/现象:定义数据和方法分离的类(常见的就是MVC模式)
优化:暂不优化
优点:代码解耦、提高扩展性和可读性
例子

/**
 * 实体类
 */
@Data
public class User {
    private String name;
    private int age;
}

/**
 * Service
 */
@Service
public class UserService {
    public User getUser() {
        return new User();
    }
}

/**
 * 控制层
 */
@Controller
public class UserController {
    @AutoWired
    private UserService userService;
    
    @RequestMapping("/getUser")
    public User getUser() {
        return userService.getUser();
    }
}

理论五:接口和抽象类的区别

  • 接口

    • 定义:如java中的interface类,也叫做协议contract
    • 存在意义:是一种has-a关系,是为了解决代码解耦问题(表示具有某一组行为特性,隔离接口和具体的实现,提高代码的扩展性)
    • 不能包含属性,只能声明方法(不能包含代码实现)
    • 类实现(implements)接口的时候,必须实现接口中声明的所有方法
  • 抽象类

    • 定义:如java中的abstract类
    • 存在意义:是一种is-a关系,是为了解决代码复用问题
    • 可以包含属性和方法(可包含代码实现也可以不包含)
    • 抽象方法:不包含代码实现的方法(关键字abstract,子类继承抽象类必须实现抽象类中的所有抽象方法)
    • 不允许被实例化(new),只能被继承(extends)
  • 总结

    • 使用场景: 如果要表示一种is-a的关系,并且是为了解决代码复用问题,就用抽象类;如果要表示一种has-a的关系,并且是为了解决代码复用问题,就用接口
    • 使用规则: 抽象类只能单继承,接口可以多实现

理论六:基于接口而非实现编程

  • 基于接口而非实现编程:也可叫做基于抽象而非实现编程
    • 我们在做软件开发的时候,一定要有抽象意识、封装意识、接口意识
    • 越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性、扩展性、可维护性
  • 定义接口的规范
    • 命名要足够通用,不能包含跟具体实现相关的字眼
    • 与特定实现有关的方法不要定义在接口中
  • 不仅可以指导非常细节的编程卡法,还能指导更加上层的架构设计、系统设计等。比如服务端与客户端之间的”接口”设计、类库的”接口”设计。
  • 实战模拟:图片的上传与下载
    public class AliyunImageStore {
    
      /**
       * 根基accessKey/serectKey等生成access token
       * @return
       */
      public String generateAccessToken() {return null;}
    
      /**
       * 上传图片到阿里云
       * @param image
       * @param bucketName
       * @param accessToken
       * @return 图片存储在阿里云上的地址
       */
      public String uploadToAliyun(Image image, String bucketName, String accessToken){return null;}
    
      /**
       * 从阿里云下载图片
       * @param url
       * @param accessToken
       * @return
       */
      public Image downloadFromAliyun(String url, String accessToken){return null;}
    }
    上面的代码是一个简单的基于阿里云的图片上传与下载功能。
    接口设计看起来并没有太大问题,但是软件开发中唯一不变的就是变化。
    如果过了一段时间,我们自建了私有云,不再将图片存储到艾丽云,而是将图片存储到自建私有云上。

问题:原先的接口命名暴露了实现细节(aliyun),如果复用的话需要修改命名,那么这样对于调用该代码的地方修改量会很大
解决:阿里云和私有云存储图片基本一致(阿里云需要access token而私有云不需要),所以我们可以考虑抽一个顶层接口类,来屏蔽特定实现细节(如access token)

public interface ImageStore {
    String upload(Image image, String bucketName);
    Image download(String url);
}

/**
 * 阿里云图片存储(需access token)
 */
public class AliyunImageStore implements ImageStore {
  /**
   * 根基accessKey/serectKey等生成access token
   * @return
   */
  public String generateAccessToken() {return null;}

  public String upload(Image image, String bucketName) {
    String accessToken = this.generateAccessToken();
    //...
    return null;
  }
  
  public Image download(String url) {
    String accessToken = this.generateAccessToken();
    //...
    return null;
  }
}

/**
 * 私有云图片存储(不需access token)
 */
public class PrivateImageStore implements ImageStore {
  public String upload(Image image, String bucketName) {return null;}
  public Image download(String url) {return null;}
}

理论七:多用组合少用继承?(ps.不是很明白组合是什么场景好用)

1.为什么不推荐使用继承?

  • 虽然继承有诸多作用,但继承层次过深、过复杂
  • 会影响代码的可维护性

2.组合相比继承有哪些优势?

  • 继承主要有三个作用:
    • 表示is-a关系
    • 支持多态特性
    • 代码复用
  • 组合优势
    • 可以通过和接口委托三个技术手段来达成继承的三个作用(在上面)
    • 可以解决层次过深、过复杂的继承关系影响代码可维护性的问题

3.如何判断该用组合还是继承?

  • 使用继承的场景
    • 类之间的继承结构稳定
    • 类之间的层次比较浅
    • 类之间的关系不复杂
  • 使用组合的场景
    • 类之间的继承结构不稳定
    • 类之间的层次比较深
    • 类之间的关系复杂

实战篇(暂时跳过)

实战一(上):基于贫血模型的MVC架构违背OOP吗?

实战一(下):利用利于充血模型的DDD开发一个虚拟钱包系统?

实战二(上):对接口鉴权做面向对象分析

实战二(下):利用面向对象设计和编程开发接口鉴权功能


文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录