面向对象设计指南
一般说设计模式都是指面向对象的设计模式,因为面向对象语言可以借助封装、继承、多态等特性更好的达到复用性、可拓展性、可维护性。
面向对象一般指以类、对象为组织代码的基本单元,并将封装、继承、多态、抽象四个特性(抽象有的定义里并不认为是四大特性)作为代码设计与实现的基石。
封装:通过访问权限控制,只对外暴露必要的操作,保护数据;
继承:代码复用,结构美感。不过 Java 语言不支持多重继承,原因是如果 BC 都继承了 A 并重写了某个方法,D 同时继承 BC 会产生歧义;
多态:提高代码的复用性,主要通过两种方式实现:
- 继承:父类引用指向子类对象;
- 实现:接口引用指向具体实现类;
抽象:有时并不计入四大特性,用来保护实现,例如接口就是对实现的一种抽象,无需关注实现。
用了面向对象语言就是面向对象开发吗?
- 滥用 getter、setter 方法。lombok 的注解确实很方便,但这样其实违背了面向对象的封装特性,例如 createTime 等字段其实是不需要 setter 方法的,需要在创建对象的时候就确定。
- 滥用全局变量、全局方法(Constants、Utils)。这样会导致修改后所有引用的地方都重新编译,而且有的时候只需要其中的某几个变量(或方法)却导入了整个类。
- 功能拆分,不要定义一个大而全的类。例如 Constants 拆分为 DateConstants、RedisConstants、MysqlConstants
- **
定义数据与方法分离的类**。传统的 MVC 开发中,数据在相应的 BO、VO、PO 中,而操作却封装在对应的 Controller、Service 中,这就是典型的面向过程,也就是“贫血模型”的开发方法。不过这样的开发方式依然很流行,因为大部分的需求并不复杂,只是从数据库中找到一些字段,组织对应的 VO,先写 service 反推 controller。
如何理解接口与抽象类?
随着 jdk 版本的更新,接口也可以有默认实现,也可以定义变量作为常量使用。抽象类依然不允许被实例化,继承抽象类必须重写抽象类的所有方法。
先说结论:抽象类的作用更多是为了代码复用,而接口的作用则更偏向与“协议”,具备什么样的功能。
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
| public class BaseEntity implements Serializable {
private static final long serialVersionUID = 8417380540303280008L; @ApiModelProperty(value = "所属用户标识") @Column(name = "USER_ID") private String userId;
@ApiModelProperty(value = "记录是否有效,默认为1表示有效") @Column(name = "ACTIVE") private String active;
@ApiModelProperty(value = "创建时间,默认为当前时间") @Column(name = "CREATED_AT", updatable = false) @DateTimeFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND) @JsonFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND, timezone = Constants.TIMEZONE) private LocalDateTime createdAt;
@ApiModelProperty(value = "更新时间") @Column(name = "UPDATED_AT") @DateTimeFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND) @JsonFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND, timezone = Constants.TIMEZONE) private LocalDateTime updatedAt;
}
|
例如我们有一个上面的类,对于一个正常的删除来讲,一方面我们要查询这个数据是否存在(例如有些系统删除不存在的空数据会返回错误),另一方面判断当前登录用户是否具有删除权限(即资源的 USER_ID 是否为当前登录人或是否是当前登录人的下属),最后还需要记录日志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public void delete(String uuid) { Entity entity = getEntityFromDB(uuid); if (entity == null) { throw ...... } if (entity instanceof BaseEntity) { BaseEntity e = (BaseEntity)entity; if (!e.getActive.equals("1")) { } else { e.setActive("0"); saveEntityToDB(entity); } } }
protected abstract T saveToDataBase(T entity);
protected abstract Entity getEntityFromDB(String uuid);
|
借助抽象类与多态,可以提高代码的复用性,减少重复代码。
如何理解基于接口而非实现编程?
假如目前有一个上传图片到公有云的需求
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
| public class uploadPictureAliyunImpl { public String getToken() { }; public boolean createDictoryIfNotExists() { }; public boolean uploadPictureToAliyun() { }; }
public class Main() { public static void Main () { uploadPictureAliyunImpl impl = new uploadPictureAliyunImpl(); String token = impl.getToken(); ........ } }
|
如果这样实现,后期替换为其他云厂商,例如自有的私有云,就需要替换很多代码,实际上这种情况只需要定义一个上传图片的接口,由不同的存储来实现就行。
基于接口编程,即进行更好的抽象设计,不暴露过多实现。
为什么说多用组合少用继承?
以鸟(bird)为例,可以分为是否会飞、是否会下蛋……

当继承层次越来越深,关系会越来越复杂,会严重影响代码的稳定性与可维护性。但是当继承层次很浅且业务稳定时,依然可以利用继承和多态特性来实现特定功能。
继承实际上可以替换为组合来实现,例如定义两个接口:
1 2 3 4 5 6 7
| public interface Flyable { void fly(); }
public interface Eggable { void egg(); }
|
每一种鸟类根据自己的情况来实现对应接口即可,但是这会引入新的问题,例如有 n 个鸟类实现的 fly 接口都是一样的,那代码重复会十分严重,解决方式就是“委托”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class DefaultFlyableImpl implements Flyable { void fly() { ...... } }
public class AAABird implements Flyable { private DefaultFlyableImpl defaultFlyImpl = new DefaultFlyableImpl(); void fly() { defaultFlyImpl.fly(); } }
|