设计原则与思想:面向对象
理论一:面向对象
概念:面向对象编程是一种编程范式或编程风格,它以类或对象作为组织代码的基本单元
特性:封装、抽象、继承、多态
区别:面向对象分析就是要搞清楚做什么,面向对象设计就是要搞清楚怎么做,面向对象编程就是将分析和设计的结果翻译成代码的过程
理论二:封装、抽象、继承、多态
- 封装
- 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:表示
- 多态
- 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.如何判断该用组合还是继承?
- 使用
继承
的场景- 类之间的继承结构稳定
- 类之间的层次比较浅
- 类之间的关系不复杂
- 使用
组合
的场景- 类之间的继承结构不稳定
- 类之间的层次比较深
- 类之间的关系复杂