重新認識面向物件(持續更新)
重新認識面向物件
重新認識面向物件
1.面向物件程式設計和麵向物件程式語言
面向物件程式設計有兩個非常重要,非常基礎的概念,那就是類(class)和物件(object).
- 面向物件程式設計是一種程式設計正規化或程式設計風格.它以類或物件作為組織程式碼的基本單元,並將封裝,抽象,繼承,多型四個特性,作為程式碼設計和實現的基石;
- 面向物件程式語言是支援類或物件的語法機制,並有執行緒的語法機制,能方便地實現面向物件程式設計四大特性(封裝,抽象,繼承,多型)的程式語言;
1.1.封裝,抽象,繼承,多型解決的問題
1.1.1.封裝
封裝叫做資訊隱藏或者資料訪問保護.類通過暴露有限的訪問介面,授權外部僅能通過類提供的方法來訪問內部資訊或資料.對於封裝這個特性,我們需要程式語言本身提供一定的語法機制來支援.這個語法機制就是訪問許可權控制.
####1.1.2.抽象
抽象這個特性是非常容易實現的,並不需要非得依靠介面類或者抽象類這些特殊語法機制來支援。換句話說,並不是說一定要為實現類抽象出介面類,才叫作抽象。即便不編寫介面類,單純的實現類本身就滿足抽象特性。之所以這麼說,那是因為,類的方法是通過程式語言中的“函式”這一語法機制來實現的。通過函式包裹具體的實現邏輯,這本身就是一種抽象。呼叫者在使用函式的時候,並不需要去研究函式內部的實現邏輯,只需要通過函式的命名、註釋或者文件,瞭解其提供了什麼功能,就可以直接使用了。
####1.1.3.繼承
繼承最大的一個好處就是程式碼複用。假如兩個類有一些相同的屬性和方法,我們就可以將這些相同的部分,抽取到父類中,讓兩個子類繼承父類。這樣,兩個子類就可以重用父類中的程式碼,避免程式碼重複寫多遍。不過,這一點也並不是繼承所獨有的,我們也可以通過其他方式來解決這個程式碼複用的問題,比如利用組合關係而不是繼承關係.
####1.1.4.多型
多型是指子類可以替換父類,在實際的程式碼執行過程中,呼叫子類的方法實現.
多型這種特性也需要程式語言提供特殊的語法機制來實現.
- 第一個語法機制是程式語言要支援父類物件可以引用子類物件;
- 第二個語法機制是程式語言需要支援繼承;
- 第三個語法機制是程式語言要支援子類可以重寫父類中的方法;
2.程式碼設計看似是面向物件,實際是面向過程
2.1. 濫用getter,setter方法
這種做法實際上違反了面向物件程式設計的封裝特性,相當於將面向物件程式設計風格退化成面向過程程式設計風格.
public class ShoppingCart {
private int itemsCount;
private double totalPrice;
private List<ShoppingCartItem> items = new ArrayList<>();
public int getItemsCount() {
return this.itemsCount;
}
public void setItemsCount(int itemsCount) {
this.itemsCount = itemsCount;
}
public double getTotalPrice() {
return this.totalPrice;
}
public void setTotalPrice(double totalPrice) {
this.totalPrice = totalPrice;
}
public List<ShoppingCartItem> getItems() {
return this.items;
}
public void addItem(ShoppingCartItem item) {
items.add(item);
itemsCount++;
totalPrice += item.getPrice();
}
// ...省略其他方法...
}
任何程式碼都可以隨意呼叫setter方法,來重新設定屬性的值.
面向物件封裝的定義是:**通過訪問許可權控制,隱藏內部資料,外部僅能通過類提供的有限的介面訪問,修改內部資料.**暴露不應該暴露的setter方法,明顯違反了面向物件的封裝特性,資料沒有訪問許可權控制,任何程式碼都可以隨意修改它,程式碼就退化成了面向過程風格的.
對於items
屬性沒有設定setter方法,看似是沒有問題,但是getter方法,返回的是一個List集合容器.外部呼叫者在拿到這個容器之後,是可以操作容器內部資料的.
ShoppingCart cart = new ShoppCart();
...
cart.getItems().clear(); // 清空購物車
清空購物車這樣的功能雖然看起來合情合理,但是這樣子的程式碼寫法,會導致itemsCount
,totalPrice
,items
三者資料不一致,我們不應該將清空購物車的業務邏輯暴露給上層程式碼.正確的做法應該是,在ShoppingCart類中定義一個clear()
方法,將清空購物車的業務邏輯封裝在裡面.
如果想要看購物車中購買了什麼,那麼必須要提供items屬性的getter方法,我們可以通過Java提供的Collections.unmodifiableList()
方法,讓getter方法返回一個不可被修改的UnmodifiableList集合.
public class ShoppingCart {
// ...省略其他程式碼...
public List<ShoppingCartItem> getItems() {
return Collections.unmodifiableList(this.items);
}
}
public class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public void clear() {
throw new UnsupportedOperationException();
}
// ...省略其他程式碼...
}
ShoppingCart cart = new ShoppingCart();
List<ShoppingCartItem> items = cart.getItems();
items.clear();//丟擲UnsupportedOperationException異常
在設計實現類的時候,除非真的需要,否則,儘量不要給屬性定義setter方法.除此之外,儘管getter方法相對於setter方法要安全些,但是如果返回的是集合容器,也要防範集合內部資料被修改的危險;
2.2.濫用全域性變數和全域性方法
2.3.定義資料和方法分離的類
傳統的 MVC 結構分為 Model 層、Controller 層、View 層這三層。不過,在做前後端分離之後,三層結構在後端開發中,會稍微有些調整,被分為 Controller 層、Service 層、Repository 層。Controller 層負責暴露介面給前端呼叫,Service 層負責核心業務邏輯,Repository 層負責資料讀寫。而在每一層中,我們又會定義相應的 VO(View Object)、BO(Business Object)、Entity。一般情況下,VO、BO、Entity 中只會定義資料,不會定義方法,所有操作這些資料的業務邏輯都定義在對應的 Controller 類、Service 類、Repository 類中。這就是典型的面向過程的程式設計風格
3.抽象與介面
抽象類的比較典型的使用場景就是(模板設計模式).
Logger 是一個記錄日誌的抽象類,FileLogger 和 MessageQueueLogger 繼承 Logger,分別實現兩種不同的日誌記錄方式:記錄日誌到檔案中和記錄日誌到訊息佇列中。FileLogger 和 MessageQueueLogger 兩個子類複用了父類 Logger 中的 name、enabled、minPermittedLevel 屬性和 log() 方法,但因為這兩個子類寫日誌的方式不同,它們又各自重寫了父類中的 doLog() 方法。
// 抽象類
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(...);
}
}
介面的定義如下
// 介面
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 : filters) {
filter.doFilter(req);
}
} catch(RpcException e) {
// ...處理過濾結果...
}
// ...省略其他處理邏輯...
}
}
繼承關係是一種 is-a 的關係,那抽象類既然屬於類,也表示一種 is-a 的關係。相對於抽象類的 is-a 關係來說,介面表示一種 has-a 關係,表示具有某些功能。對於介面,有一個更加形象的叫法,那就是協議(contract)。
4.基於介面而非實現程式設計
越抽象,越頂層,越脫離具體某一實現的設計,越能提高程式碼的靈活性,越能應對未來需求變化.好的程式碼設計,不僅能應對當下的需求,而且在將來需求發生變化的時候,仍然能夠在不破壞原有程式碼設計的情況下靈活應對.
展示一個上傳下載的基於介面程式設計的正規化如下:
public interface ImageStore {
String upload(Image image, String bucketName);
Image download(String url);
}
public class AliyunImageStore implements ImageStore {
//...省略屬性、建構函式等...
public String upload(Image image, String bucketName) {
createBucketIfNotExisting(bucketName);
String accessToken = generateAccessToken();
//...上傳圖片到阿里雲...
//...返回圖片在阿里雲上的地址(url)...
}
public Image download(String url) {
String accessToken = generateAccessToken();
//...從阿里雲下載圖片...
}
private void createBucketIfNotExisting(String bucketName) {
// ...建立bucket...
// ...失敗會丟擲異常..
}
private String generateAccessToken() {
// ...根據accesskey/secrectkey等生成access token
}
}
// 上傳下載流程改變:私有云不需要支援access token
public class PrivateImageStore implements ImageStore {
public String upload(Image image, String bucketName) {
createBucketIfNotExisting(bucketName);
//...上傳圖片到私有云...
//...返回圖片的url...
}
public Image download(String url) {
//...從私有云下載圖片...
}
private void createBucketIfNotExisting(String bucketName) {
// ...建立bucket...
// ...失敗會丟擲異常..
}
}
// ImageStore的使用舉例
public class ImageProcessingJob {
private static final String BUCKET_NAME = "ai_images_bucket";
//...省略其他無關程式碼...
public void process() {
Image image = ...;//處理圖片,並封裝為Image物件
ImageStore imageStore = new PrivateImageStore(...);
imagestore.upload(image, BUCKET_NAME);
}
}
很多人在定義介面的時候,希望通過實現類來反推介面的定義.先把實現類寫好,然後看實現類中有哪些方法,如果這種方式,就有可能導致介面定義不夠抽象,依賴具體的實現.這樣的介面設計就沒有意義了.
5.多用組合少用繼承
在面向物件程式設計中,有一條非常經典的設計原則:組合優於繼承,多用組合少用繼承.
5.1.為什麼不推薦使用繼承?
繼承是面向物件的四大特性之一,用來表示類之間的is-a關係,可以解決程式碼複用的問題.雖然繼承有諸多作用,但繼承層次過深,過複雜,也會影響到程式碼的可維護性.
繼承最大的問題:繼承層次過深,繼承關係過於複雜會影響到程式碼的可讀性和可維護性.
5.2.組合相比繼承有哪些優勢?
我們可以利用組合,介面,委託三個技術手段來解決繼承存在的問題
public interface Flyable {
void fly();
}
public class FlyAbility implements Flyable {
@Override
public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility
public class Ostrich implements Tweetable, EggLayable {//鴕鳥
private TweetAbility tweetAbility = new TweetAbility(); //組合
private EggLayAbility eggLayAbility = new EggLayAbility(); //組合
//... 省略其他屬性和方法...
@Override
public void tweet() {
tweetAbility.tweet(); // 委託
}
@Override
public void layEgg() {
eggLayAbility.layEgg(); // 委託
}
}