面向对象设计基础
一、面向对象与面向对象编程语言
- 面向对象是一种编程的风格或者说是一种编程范式。是以类或者对象作为编写或者组织代码的基本单元。并且将封装、继承、多态作为代码设计和实现的基石。
- 面向对象编程语言: 面向对象编程语言支持类或者对象的语法机制,并且有线程的语法机制,能够方便的实现面向对象四大特性(抽象、封装、继承、多态)的编程语言。
二、面向对象的四大特性
- 封装:隐藏内部的实现细节,对外只提供统一的方法来实现对数据的访问。这样可以有效的起到保护数据的作用。 在下面使用虚拟钱包的案例,描述封装在面向对象程序设计中好处。
public class Wallet {
private String id;
private long createTime;
private BigDecimal balance;
private long balanceLastModifiedTime;
public Wallet(){
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public String getId() {
return id;
}
public long getCreateTime() {
return createTime;
}
public BigDecimal getBalance() {
return balance;
}
public long getBalanceLastModifiedTime() {
return balanceLastModifiedTime;
}
public void increaseBalance(BigDecimal increaseAmount){
if (increaseAmount.compareTo(BigDecimal.ZERO) < 0){
throw new InvalidAmountException("金额异常");
}
this.balance.add(increaseAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public void decreaseBalance(BigDecimal decreaseAmount){
if (decreaseAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreaseAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance.subtract(decreaseAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
在上述的钱包的案例中。钱包有四个属性分别是钱包的id、创建时间createTime、当前余额balance以及余额最后更新的时间blanceLastModifiedTime。而钱包的id和钱包的创建时间createTime是在钱包创建的时候就确定的。之后不会在发生变化,所以对外没有提供可修改这两个数据的接口。同时balance
与balanceLastmodifiedTime
是随着钱包中的金额变化一起变化。对于钱包的余额只能增加和减少不可以在被重新设置。封装隐藏内部具体的实现细节,只提供统一的对外接口,使用者不需要知道具体的细节使用即可。这样可以有效的降低被错误使用的概率。
3. 抽象:前面的封装是将数据隐藏起来,保护信息。抽象则是隐藏方法具体的实现,外部使用的时候只需要知道提供哪些功能,而不需要知道具体的实现。好的抽象的有需求时候的方便扩展。在下面的代码中我们使用ITableService
描述一个表相关的操作。代码如下所示
public interface ITableService {
/**
* 创建表曹醉哦
* @param tableInfo
*/
void save(TableInfo tableInfo);
/**
* 获取表的详细信息
* @param tableId
* @return
*/
TableInfo get(String tableId);
/**
* 删除表操作
* @param tableInfo
* @return
*/
Boolean delete(TableInfo tableInfo);
}
public class HiveTableService implements ITableService {
@Override
public void save(TableInfo tableInfo) {
}
@Override
public TableInfo get(String tableId) {
return null;
}
@Override
public Boolean delete(TableInfo tableInfo) {
return null;
}
}
上面的抽象中展示了共有的操作。目前我们只需要对Hive
表进行操作。但是在将来的某一天需求有变化需要对HBase
等多种表进行操作的时候只需要实现这个接口,然后在里面实现自定义的逻辑即可。然而对于外面的使用这来说只要关注接口提供对应的功能,而不需要关注具体的实现的细节。
5. 继承:继承描述的是一种is-a
的关系。比如在现实生活中中学生是一个人,那么他就会具有人所具备的属性。如下代码所示展示一个表都具有的公共属性被提取出来之后放在一起,后序需要设计到的表只需要继承这个类就具有这个这些属性,这样可以有效的减少重复代码
// 表共有的属性都会被抽象出来放在一起
public class BaseTableInfo {
private String instName;
private String dbName;
private String tableName;
private String description;
// get set方法
}
//Hive表不但拥有上述的属性,还具备自己独特的属性
public class TableInfo extends BaseTableInfo {
private List<String> columns;
private List<String> partitions;
}
在后面的扩展中可能还有HBase
表等各种各样的表,但是只需要继承共有属性,然后增加独有的属性即可
7. 多态:多态是指子类替换超类,在运行的过程中实际上是调用子类的方法。在java
中实现多态有两个方式。第一种是通过继承实现多态。第二中是通过接口实现多态。多态可以提高代码的扩展性和复用性。下面的两段代码展示了使用不同方式实现多态.
public abstract class BaseTableService<T> {
/**
* 保存表的通用方法
* @param tableInfo 需要保存表的基本信息
*/
public abstract void save(T tableInfo);
/**
* 删除表的通用方法
* @param tableId 表的唯一标识,表的id
* @return 删除成功返回 true 删除失败返回false
*/
public abstract Boolean remove(String tableId);
/**
* 根据表的id,获取表的详细信息
* @param tableId 表的id
* @return
*/
public abstract T get(String tableId);
}
public class HiveTableService extends BaseTableService<TableInfo> {
//其他属性
@Override
public void save(TableInfo tableInfo) {
}
@Override
public Boolean remove(String tableId) {
return null;
}
@Override
public TableInfo get(String tableId) {
return null;
}
}
// 基于接口的实现方式
public interface ITableService<T> {
void save(T tableInfo);
Boolean remove(String tableId);
T get(String tableId);
}
public class HiveTableService2 implements ITableService<TableInfo> {
@Override
public void save(TableInfo tableInfo) {
}
@Override
public Boolean remove(String tableId) {
return null;
}
@Override
public TableInfo get(String tableId) {
return null;
}
}
三、抽象类和接口
- 在上面的展示代码中我们定义了一个
TableService
的抽象类,在抽象类不但可以有抽象方法,也能存在非抽象方法。在抽象类中的成员边的访问控制是任意的。但是在定义接口的时候,在接口中所有的成员变量以及方法都是public
的。同时在接口中的方法都是没有实现的,都需要在实现类中实现对应的方法。并且在接口中的成员变量必须是常量。 - 那何时使用接口何时使用抽象类。抽象类表的是一种共有的属性。子类继承抽象类实现相应的功能,表是的
is-a
的关系。在实际的使用的过程中表示是一种是的属性的时候使用抽象类。而在接口的使用的时候,表示对象具有一种自己独有的特性的时候使用接口,就像我们在使用Spring
的时候。在service
层通过组合多个功能模块来实现一个通用的功能。
四、面向对象设计原则
- 基于接口编程而非实现编程:在软件开发的过程中唯一不变的就是变化。这句话听起来似乎很装X。但是仔细想想确实是这么一回事,在刚开始设计时候如果没有好的抽象,在后面在面对需求变更的时候,就需要我们去修改大量的代码。在代码修改之后带来的结果就是有可能写了新的bug。代码如下:
public class HiveTableStore {
private HiveTableService tableService;
public void saveHiveTable(TableInfo tableInfo){
tableService.save(tableInfo);
}
public Boolean removeHiveTable(String tableId){
return tableService.remote(tableId);
}
}
在设计的初期只考虑使用这个系统来存储Hive表。这样写确实没有问题,但是在后面有一天这个系统还需存储HBase
,kudu
等这些表的时候。我们就需要将存储Hive表的逻辑在重新实现一遍。这样不仅带来了大量的重复代码,还有可能写了新的bug。实际在使用的时候只需要对将共有的抽象出来。作为稳定的部分,把具体的实现封装起来。在使用的时候应该抽象如下的代码。
public interface TableStore {
void save(TableInfo tableInfo);
Boolean remove(String tableId);
}
去掉与具体关联的部分,在后面不管新增多少组件需要去管理表,只需要实现相应的接口。扩展对应的功能即可。
2. 多用组合少用继承:我们设计一个鸟类BaseBird
。在这个类中描述鸟的各种行为属性,如会飞、会下蛋、有叫等如下所示:
public abstract class BaseBird {
public abstract void fly();
public abstract void tweet();
}
让所有的鸟类都继承这个鸟的基类,然后根据不同的鸟去实现不同的动作。然后有些鸟是不具备上述的功能的如玩具鸟是不具备下蛋的功能。那如何处理这种情况呢在写两个类继承BaseBird
分为可以下蛋的鸟和不能下蛋的鸟。但是这样做会存在很大的隐患就是有可能在某个时候又有新的特性出来又要去修改继承的层次。这样做会导致继承的层次过深而很难维护。
那如何处理呢?在前面学了接口,我们可以为每一个特性定义一个接口,然后让对于的实现类实现对应的功能,定义的接口如下:
public interface Fly {
void fly();
}
public interface Tweet {
void tweet();
}
这样让每一个鸟类都实现对应的接口,根据需求实现不同的功能。但是这样做会带来大量的重复代码,每一个鸟都需要自己实现一次对应的接口。为了减少重复的代码。可以在定义两个实现类分别实现Fly
以及Tweet
这接口实现对应的功能。在使用的时候只需要将对应的实现类引入进去,通过组合的方式就可以轻松的实现对不同的鸟进行描述。