理解面向对象
这篇博文主要想说面向对象的几大特性:封装,抽象,继承,多态,以及我们究竟如何去使用。
1. 面向对象概述
1.1 面向对象编程
一种编程范式/风格,以类或对象作为组织代码的基本单元,并将封装,抽象,继承,多态四个特性作为代码设计和实现的基石。
整个编程的过程:
- Object Oriented Analysis
- Object Oriented Design
- Object Oriented Programming
1.2 面向对象编程的四大特征
1.2.1 封装 Encapsulation
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();
}
// 注意:下面对get方法做了代码折叠,是为了减少代码所占文章的篇幅
public String getId() { return this.id; }
public long getCreateTime() { return this.createTime; }
public BigDecimal getBalance() { return this.balance; }
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; }
public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
虚拟钱包,对于自身变量,用private来标注,然后通过对应的getter,setter方法允许外界来访问一部分变量,允许进行一定的修改。
没有封装则意味着不可控,即任何代码都可以被任何人访问,修改的代码可以遍布在包的任何角落,会影响代码的可读性,以及可维护性。只暴露出有限多的接口,供外界来使用。
总结: 封装是为了隐藏信息,保护数据
1.2.2 继承 Inheritance
用来表述is a的关系,java支持单继承。
继承最大的好处就是代码复用,比如两个子类的共同代码抽取到父类当中,然后父类来共同使用。
但是过度使用的话会容易导致层级数量太多,反而降低代码的可读性。
1.2.3 多态 Polymorphism
- 继承加方法重写实现
public class DynamicArray {
private static final int DEFAULT_CAPACITY = 10;
protected int size = 0;
protected int capacity = DEFAULT_CAPACITY;
protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
public int size() { return this.size; }
public Integer get(int index) { return elements[index];}
//...省略n多方法...
public void add(Integer e) {
ensureCapacity();
elements[size++] = e;
}
protected void ensureCapacity() {
//...如果数组满了就扩容...代码省略...
}
}
public class SortedDynamicArray extends DynamicArray {
@Override
public void add(Integer e) {
ensureCapacity();
int i;
for (i = size-1; i>=0; --i) { //保证数组中的数据有序
if (elements[i] > e) {
elements[i+1] = elements[i];
} else {
break;
}
}
elements[i+1] = e;
++size;
}
}
public class Example {
public static void test(DynamicArray dynamicArray) {
dynamicArray.add(5);
dynamicArray.add(1);
dynamicArray.add(3);
for (int i = 0; i < dynamicArray.size(); ++i) {
System.out.println(dynamicArray.get(i));
}
}
public static void main(String args[]) {
DynamicArray dynamicArray = new SortedDynamicArray();
test(dynamicArray); // 打印结果:1、3、5
}
}
- 利用接口类实现多态特性
public interface Iterator {
String hasNext();
String next();
String remove();
}
public class Array implements Iterator {
private String[] data;
public String hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}
public class LinkedList implements Iterator {
private LinkedListNode head;
public String hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}
public class Demo {
private static void print(Iterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
public static void main(String[] args) {
Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}
多态可以很大程度上提高代码的可扩展性和复用性
1.2.4 抽象 Abstraction
抽象,主要是为了隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些方法具体是如何实现的。
public interface IPictureStorage {
void savePicture(Picture picture);
Image getPicture(String pictureId);
void deletePicture(String pictureId);
void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);
}
public class PictureStorage implements IPictureStorage {
// ...省略其他属性...
@Override
public void savePicture(Picture picture) { ... }
@Override
public Image getPicture(String pictureId) { ... }
@Override
public void deletePicture(String pictureId) { ... }
@Override
public void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) { ... }
}
使用接口或者abstract class,然后调用者就只需要知道需要传入什么参数,传出什么参数,就可以试用了。
2. 面向对象 vs 面向过程
需要对这两个概念有更深的理解,很多时候,我们是在用面向对象的语言写面向过程的代码,对于到底什么是面向对象,如何写真的面向对象的代码,我们还是有很多无法确定的地方。
面向过程的编程是一种编程范式,以过程(方法,函数,操作)作为组织代码的基本单元,以数据(可以理解为成员变量,属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
2.1 面向对象编程的优势
2.1.1 更能够应对大规模复杂程序的开发
因为对于面向过程的编程风格来说,整个程序的处理流程会偏向于线性,流程化,但是实际应用场景中,关系错综复杂,会很难将程序拆解为一组顺序执行的方法。而面向对象的方式就可以比较好的解决这个问题了。
2.1.2 更易复用,扩展和维护
面向对象通过类这种组织方式能够将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程编程那样,数据可以被任意方法的随意修改
我们通过使用多态的特性,可以在需要修改一个功能实现的时候,通过实现一个新的子类的方式,在子类当中重写原来的功能逻辑,用子类替代父类。 —- 对修改关闭,对扩展开放。
2.2 Warning/ Bad Smell - 看似面向对象的面向过程的代码
首先值得注意的是,这里提及的都是我们需要注意的地方,但并不是说我们完全不能这样子写。譬如util class,很多时候我们是需要的,因为确实可以不带数据的,只在input,output传递所有信息就够了。
2.2.1 getter setter方法的问题
当我们习惯性的给所有的属性都加上getter, setter方法的时候,其实是破坏了Java的封装的特性的,我们使用private 标注属性,再适当的设置setter,getter方法是因为我们不想将对于代码/数据的控制权交给他人,而疯狂的getter,setter方法会让Java的封装优势荡然无存,只是从原来的直接访问属性变成通过getter,setter方法来访问。没有起到任何保障安全的作用。
注意如果是集合容器的话,要防范集合内部的数据被修改的危险。另外,setter方法的使用需要谨慎些,只有在必需的时候再用。
2.2.2 全局变量和全局方法
常见的全局变量有:
- 单例类对象
- 静态成员变量
- 常量
常见的全局方法有:
- 静态方法
Constants类往往会越加越大,而且会很难维护。而且如果我们开发的其他项目需要复用这些constants,哪怕我们只使用一个,那么最终也会不得不将整个文件加载进去,没有必要,而且会变得非常的慢。我们可以将Constants类拆分为功能更加单一的多个类,或者直接将这些常量定义到对应的class当中。这也是个很好的选择。
3.1 什么是接口? 什么是抽象类?
3.1.1 抽象类定义
下面是一个模板设计的实例,Logger被用来记录日志,FileLogger和MessageQueueLogger继承Logger,分别实现两种不同的日志记录方式
// 抽象类
public abstract class Logger {
private String name;
private boolean enabled;
private Level minPermittedLevel;
public Logger(String name, boolean enabled, Level minPermittedLevel) {
this.name = name;
this.enabled = enabled;
this.minPermittedLevel = minPermittedLevel;
}
public void log(Level level, String message) {
boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());
if (!loggable) return;
doLog(level, message);
}
protected abstract void doLog(Level level, String message);
}
// 抽象类的子类:输出日志到文件
public class FileLogger extends Logger {
private Writer fileWriter;
public FileLogger(String name, boolean enabled,
Level minPermittedLevel, String filepath) {
super(name, enabled, minPermittedLevel);
this.fileWriter = new FileWriter(filepath);
}
@Override
public void doLog(Level level, String mesage) {
// 格式化level和message,输出到日志文件
fileWriter.write(...);
}
}
// 抽象类的子类: 输出日志到消息中间件(比如kafka)
public class MessageQueueLogger extends Logger {
private MessageQueueClient msgQueueClient;
public MessageQueueLogger(String name, boolean enabled,
Level minPermittedLevel, MessageQueueClient msgQueueClient) {
super(name, enabled, minPermittedLevel);
this.msgQueueClient = msgQueueClient;
}
@Override
protected void doLog(Level level, String mesage) {
// 格式化level和message,输出到消息中间件
msgQueueClient.send(...);
}
}
抽象类的特性:
- 抽象类不允许被实例化,只能被继承
- 抽象类可以包含属性与方法,方法可以包含代码实现,也可以不包含,设计成抽象方法
- 子类继承抽象类,必须实现抽象类当中的所有抽象方法
3.1.2 接口定义
// 接口
public interface Filter {
void doFilter(RpcRequest req) throws RpcException;
}
// 接口实现类:鉴权过滤器
public class AuthencationFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...鉴权逻辑..
}
}
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...限流逻辑...
}
}
// 过滤器使用demo
public class Application {
// filters.add(new AuthencationFilter());
// filters.add(new RateLimitFilter());
private List<Filter> filters = new ArrayList<>();
public void handleRpcRequest(RpcRequest req) {
try {
for (Filter filter : fitlers) {
filter.doFilter(req);
}
} catch(RpcException e) {
// ...处理过滤结果...
}
// ...省略其他处理逻辑...
}
}
使用interface关键字实现一个Filter接口,AuthencationFilter和RatelimiterFilter分别实现对于RPC请求的鉴权和限流的过滤功能。
- 接口不能包含属性
- 接口只能声明方法,方法不能包含代码实现
- 类实现接口的时候,必须实现接口当中声明的所有方法
抽象类和继承类似,其实表征的是一种is-a的关系;而接口表征的是一种has-a的关系/ 协议,表示具有某些功能
3.2 区别/ 都能解决什么样的编程问题?
3.2.1 抽象类 存在的意义
抽象类不能实例化,只能被继承。主要是用来解决代码复用的问题的。多个子类可以继承抽象类当中定义的属性和方法,避免在子类当中,重复编写相同的代码。
为什么必须是抽象类来做代码复用呢?
- 因为可以利用多态来做了
- 减少父类代码被错误的直接使用的风险
- 也可以增加代码的可读性
3.2.2 接口的存在意义
接口更侧重于解耦,接口是对行为的一种抽象,相当于一组协议或者契约。这样调用者只需要关注抽象的接口,不需要了解具体的实现
Reference
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 stone2paul@gmail.com
文章标题:理解面向对象
文章字数:2.7k
本文作者:Leilei Chen
发布时间:2020-02-25, 13:20:52
最后更新:2020-03-04, 09:43:08
原始链接:https://www.llchen60.com/%E7%90%86%E8%A7%A3%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。