如何優化好UITableView,值得思考

来源:https://www.cnblogs.com/mysweetAngleBaby/archive/2022/09/16/16700872.html
-Advertisement-
Play Games

如果你覺得 UITableViewDelegate 和 UITableViewDataSource 這兩個協議中有大量方法每次都是複製粘貼,實現起來大同小異;如果你覺得發起網路請求並解析數據需要一大段代碼,加上刷新和載入後簡直複雜度爆表,如果你想知道為什麼下麵的代碼可以滿足上述所有要求: 解耦後的V ...


如果你覺得 UITableViewDelegate 和 UITableViewDataSource 這兩個協議中有大量方法每次都是複製粘貼,實現起來大同小異;如果你覺得發起網路請求並解析數據需要一大段代碼,加上刷新和載入後簡直複雜度爆表,如果你想知道為什麼下麵的代碼可以滿足上述所有要求:

1

解耦後的VC

MVC

在討論解耦之前,我們要弄明白 MVC 的核心:控制器(以下簡稱 C)負責模型(以下簡稱 M)和視圖(以下簡稱 V)的交互。
這裡所說的 M,通常不是一個單獨的類,很多情況下它是由多個類構成的一個層。最上層的通常是以 Model 結尾的類,它直接被 C 持有。Model 類還可以持有兩個對象:

  1. Item:它是實際存儲數據的對象。它可以理解為一個字典,和 V 中的屬性一一對應
  2. Cache:它可以緩存自己的 Item(如果有很多)
    常見的誤區:
  3. 一般情況下數據的處理會放在 M 而不是 C(C 只做不能復用的事)
  4. 解耦不只是把一段代碼拿到外面去。而是關註是否能合併重覆代碼, 並且有良好的拖展性。

原始版

在 C 中,我們創建 UITableView 對象,然後將它的數據源和代理設置為自己。也就是自己管理著 UI 邏輯和數據存取的邏輯。在這種架構下,主要存在這些問題:

  1. 違背 MVC 模式,現在是 V 持有 C 和 M。
  2. C 管理了全部邏輯,耦合太嚴重。
  3. 其實絕大多數 UI 相關都是由 Cell 而不是 UITableView 自身完成的。
    為瞭解決這些問題,我們首先弄明白,數據源和代理分別做了那些事。
    數據源
    它有兩個必須實現的代理方法:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

簡單來說,只要實現了這個兩個方法,一個簡單的 UITableView 對象就算是完成了。
除此以外,它還負責管理 section 的數量,標題,某一個 cell 的編輯和移動等。
代理
代理主要涉及以下幾個方面的內容:

  1. cell、headerView 等展示前、後的回調。
  2. cell、headerView 等的高度,點擊事件。
    最常用的也是兩個方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

提醒:絕大多數代理方法都有一個 indexPath 參數

優化數據源

最簡單的思路是單獨把數據源拿出來作為一個對象。
這種寫法有一定的解耦作用,同時可以有效減少 C 中的代碼量。然而總代碼量會上升。我們的目標是減少不必要的代碼。
比如獲取每一個 section 的行數,它的實現邏輯總是高度類似。然而由於數據源的具體實現方式不統一,所以每個數據源都要重新實現一遍。

SectionObject

首先我們來思考一個問題,數據源作為 M,它持有的 Item 長什麼樣?答案是一個二維數組,每個元素保存了一個 section 所需要的全部信息。因此除了有自己的數組(給cell用)外,還有 section 的標題等,我們把這樣的元素命名為 SectionObject:

@interface KtTableViewSectionObject : NSObject
@property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 協議中的 titleForHeaderInSection 方法可能會用到
@property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 協議中的 titleForFooterInSection 方法可能會用到
@property (nonatomic, retain) NSMutableArray *items;
- (instancetype)initWithItemArray:(NSMutableArray *)items;
@end

Item

其中的 items 數組,應該存儲了每個 cell 所需要的 Item,考慮到 Cell 的特點,基類的 BaseItem 可以設計成這樣:

@interface KtTableViewBaseItem : NSObject
@property (nonatomic, retain) NSString *itemIdentifier;
@property (nonatomic, retain) UIImage *itemImage;
@property (nonatomic, retain) NSString *itemTitle;
@property (nonatomic, retain) NSString *itemSubtitle;
@property (nonatomic, retain) UIImage *itemAccessoryImage;
- (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage;
@end

父類實現代碼

規定好了統一的數據存儲格式以後,我們就可以考慮在基類中完成某些方法了。以 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 方法為例,它可以這樣實現:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.sections.count > section) {
        KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section];
        return sectionObject.items.count;
    }
    return 0;
}

比較困難的是創建 cell,因為我們不知道 cell 的類型,自然也就無法調用 alloc 方法。除此以外,cell 除了創建,還需要設置 UI,這些都是數據源不應該做的事。
這兩個問題的解決方案如下:

  1. 定義一個協議,父類返回基類 Cell,子類視情況返回合適的類型。
  2. 為 Cell 添加一個 setObject 方法,用於解析 Item 並更新 UI。

優勢

經過這一番折騰,好處是相當明顯的:

  1. 子類的數據源只需要實現 cellClassForObject 方法即可。原來的數據源方法已經在父類中被統一實現了。
  2. 每一個 Cell 只要寫好自己的 setObject 方法,然後坐等自己被創建,被調用這個方法即可。
  3. 子類通過 objectForRowAtIndexPath 方法可以快速獲取 item,不用重寫。
    對照 demo(SHA-1:6475496),感受一下效果。

優化代理

我們以之前所說的,代理協議中常用的兩個方法為例,看看怎麼進行優化與解耦。
首先是計算高度,這個邏輯並不一定在 C 完成,由於涉及到 UI,所以由 Cell 負責實現即可。而計算高度的依據就是 Object,所以我們給基類的 Cell 加上一個類方法:

+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;

另外一類問題是以處理點擊事件為代表的代理方法, 它們的主要特點是都有 indexPath 參數用來表示位置。然而實際在處理過程中,我們並不關係位置,關心的是這個位置上的數據。
因此,我們對代理方法做一層封裝,使得 C 調用的方法中都是帶有數據參數的。因為這個數據對象可以從數據源拿到,所以我們需要能夠在代理方法中獲取到數據源對象。
為了實現這一點, 最好的辦法就是繼承 UITableView:

@protocol KtTableViewDelegate<UITableViewDelegate>
@optional
- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;
- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section;
// 將來可以有 cell 的編輯,交換,左滑等回調
// 這個協議繼承了UITableViewDelegate ,所以自己做一層中轉,VC 依然需要實現某
@end
@interface KtBaseTableView : UITableView<UITableViewDelegate>
@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource;
@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate;
@end

cell 高度的實現如下,調用數據源的方法獲取到數據:

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
    id<KtTableViewDataSource> dataSource = (id<KtTableViewDataSource>)tableView.dataSource;
    KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
    Class cls = [dataSource tableView:tableView cellClassForObject:object];
    return [cls tableView:tableView rowHeightForObject:object];
}

優勢
通過對 UITableViewDelegate 的封裝(其實主要是通過 UITableView 完成),我們獲得了以下特性:

  1. C 不用關心 Cell 高度了,這個由每個 Cell 類自己負責
  2. 如果數據本身存在數據源中,那麼在代理協議中它可以被傳給 C,免去了 C 重新訪問數據源的操作。
  3. 如果數據不存在於數據源,那麼代理協議的方法會被正常轉發(因為自定義的代理協議繼承自 UITableViewDelegate)
    對照 demo(SHA-1:ca9b261),感受一下效果。

更加 MVC,更加簡潔

在上面的兩次封裝中,其實我們是把 UITableView 持有原生的代理和數據源,改成了 KtTableView 持有自定義的代理和數據源。並且預設實現了很多系統的方法。
到目前為止,看上去一切都已經完成了,然而實際上還是存在一些可以改進的地方:

  1. 目前仍然不是 MVC 模式!
  2. C 的邏輯和實現依然可以進一步簡化
    基於以上考慮, 我們實現一個 UIViewController 的子類,並且把數據源和代理封裝到 C 中。
@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate>
@property (nonatomic, strong) KtBaseTableView *tableView;
@property (nonatomic, strong) KtTableViewDataSource *dataSource;
@property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用來創建 tableView
- (instancetype)initWithStyle:(UITableViewStyle)style;
@end

為了確保子類創建了數據源,我們把這個方法定義到協議里,並且定義為 required。

成果與目標

現在我們梳理一下經過改造的 TableView 該怎麼用:

  1. 首先你需要創建一個繼承自 KtTableViewController 的視圖控制器,並且調用它的 initWithStyle 方法。
    objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];
  2. 在子類 VC 中實現 createDataSource 方法,實現數據源的綁定。
*   (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這 一步創建了數據源 } ```

1.在數據源中,需要指定 cell 的類型。

*   (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; } 

1.在 Cell 中,需要通過解析數據,來更新 UI 並返回自己的高度。

*   (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父類的 setObject 方法。 

還有什麼要優化的
到目前為止,我們實現了對 UITableView 以及相關協議、方法的封裝,使它更容易使用,避免了很多重覆、無意義的代碼。
在使用時,我們需要創建一個控制器,一個數據源,一個自定義 Cell,它們正好是基於 MVC 模式的。因此,可以說在封裝與解耦方面,我們已經做的相當好了,即使再花大力氣,也很難有明顯的提高。
但關於 UITableView 的討論遠遠沒有結束,我列出了以下需要解決的問題

  1. 在這種設計下,數據的回傳不夠方便,比如 cell 的給 C 發消息。
  2. 下拉刷新與上拉載入如何集成
  3. 網路請求的發起,與解析數據如何集成
    關於第一個問題,其實是普通的 MVC 模式中 V 和 C 的交互問題,可以在 Cell(或者其他類) 中添加 weak 屬性達到直接持有的目的,也可以定義協議。
    問題二和三是另一大塊話題,網路請求大家都會實現,但如何優雅的集成進框架,保證代碼的簡單和可拓展,就是一個值得深入思考,研究的問題了。接下來我們就重點討論網路請求。

為何創建網路層

一個 iOS 的網路層框架該如何設計?這是一個非常寬泛,也超出我能力範圍之外的問題。業內已有一些優秀的,成熟的思路和解決方案,由於能力,角色所限,我決定從一個普通開發者而不是架構師的角度來說說,一個普通的、簡單的網路層該如何設計。我相信再複雜的架構,也是由簡單的設計演化而來的。
對於絕大多數小型應用來說,集成 AFNetworking 這樣的網路請求框架就足以應付 99% 以上的需求了。但是隨著項目的擴大,或者用長遠的眼光來考慮,直接在 VC 中調用具體的網路框架(下麵以 AFNetworking 為例),至少存在以下問題:

  1. 一旦日後 AFNetworking 停止維護,而且我們需要更換網路框架,這個成本將無法想象。所有的 VC 都要改動代碼,而且絕大多數改動都是雷同的。
    這樣的例子真實存在,比如我們的項目中就依然使用早已停止維護的 ASIHTTPRequest,可以預見,這個框架遲早要被替換。
  2. 現有的框架可能無法實現我們的需求。以 ASIHTTPRequest 為例,它的底層用 NSOperation 來表示每一個網路請求。眾所周知,一個 NSOperation 的取消,並不是簡單調用 cancel 方法就可以的。在不修改源碼的前提下,一旦它被放入隊列,其實是無法取消的。
  3. 有時候我們的需求僅僅是進行網路請求,還會對這個請求進行各種自定義的拓展。比如我們可能要統計請求的發起和結束時間,從而計算網路請求,數據解析的步驟的耗時。有時候,我們希望設計一個通用組件,並且支持由各個業務部門去自定義具體的規則。比如可能不同的部門,會為 HTTP 請求添加不同的頭部。
  4. 網路請求還有可能有其他廣泛需要添加的需求,比如請求失敗時的彈窗,請求時的日誌記錄等等。
    參考當前代碼(SHA-1:a55ef42)感受一下沒有任何網路層時的設計。

如何設計網路層

其實解決方案非常簡單:

所有的電腦問題,都可以通過添加中間層來解決

讀者可以自行思考,為什麼添加中間層可以解決上述三個問題。

三大模塊

對於一個網路框架來說,我認為主要有三個方面值得去設計:

  1. 如何請求
  2. 如何回調
  3. 數據解析

一個完整的網路請求一般由以上三個模塊組成,我們逐一分析每個模塊實現時的註意事項:

發起請求

發起請求時,一般有兩種思路,第一種是把所有要配置的參數寫到同一個方法中,借用 與時俱進,HTTP/2下的iOS網路層架構設計 一文中的代碼表示:

+ (void)networkTransferWithURLString:(NSString *)urlString
                       andParameters:(NSDictionary *)parameters
                              isPOST:(BOOL)isPost
                        transferType:(NETWORK_TRANSFER_TYPE)transferType
                   andSuccessHandler:(void (^)(id responseObject))successHandler
                   andFailureHandler:(void (^)(NSError *error))failureHandler {
                           // 封裝AFN
                   }

這種寫法的好處在於所有參數一目瞭然,而且簡單易用,每次都調用這個方法即可。但是缺點也很明顯,隨著參數和調用次數的增多,網路請求的代碼很快多到爆炸。
另一組方法則是將 API 設置成一個對象,把要傳入的參數作為這個對象的屬性。在發起請求時,只要設置好對象的相關屬性,然後調用一個簡單的方法即可。

@interface DRDBaseAPI : NSObject
@property (nonatomic, copy, nullable) NSString *baseUrl;
@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject,  NSError * _Nullable error);
- (void)start;
- (void)cancel;
...
@end

根據前文提到的 Model 和 Item 的概念,那麼應該可以想到:這個用於訪問網路的 API 對象,其實是作為 Model 的一個屬性

Model 負責對外暴露必要的屬性和方法,而具體的網路請求則由 API 對象完成,同時 Model 也應該持有真正用來存儲數據的 Item。

如何回調

一次網路請求的返回結果應該是一個 JSON 格式的字元串,通過系統的或者一些開源框架可以將它轉換成字典。

接下來我們需要使用 runtime 相關的方法,將字典轉換成 Item 對象。

最後,Model 需要將這個 Item 賦值給自己的屬性,從而完成整個網路請求。

如果從全局角度來說,我們還需要一個 Model 請求完成的回調,這樣 VC 才能有機會做相應的處理。

考慮到 Block 和 Delegate 的優缺點,我們選擇用 Block 來完成回調。

數據解析

這一部分主要是利用 runtime 將字典轉換成 Item,它的實現並不算難,但是如何隱藏好實現細節,使上層業務不用過多關心,則是我們需要考慮的問題。

我們可以定義一個基類的 Item,並且為它定義一個 parseData 函數:

// KtBaseItem.m
- (void)parseData:(NSDictionary *)data {
    // 解析 data 這個字典,為自己的屬性賦值
    // 具體的實現請見後面的文章
}

封裝 API 對象

首先,我們封裝一個 KtBaseServerAPI 對象,這個對象的主要目的有三個:

  1. 隔離具體的網路庫的實現細節,為上層提供一個穩定的的介面
  2. 可以自定義一些屬性,比如網路請求的狀態,返回的數據等,方便的調用
  3. 處理一些公用的邏輯,比如網路耗時統計
    具體的實現請參考 Git 提交歷史:SHA-1:76487f7

Model 與 Item

BaseModel

Model 主要需要負責發起網路請求,並且處理回調,來看一下基類的 Model 如何定義:

@interface KtBaseModel
// 請求回調
@property (nonatomic, copy) KtModelBlock completionBlock;
//網路請求
@property (nonatomic,retain) KtBaseServerAPI *serverApi;
//網路請求參數
@property (nonatomic,retain) NSDictionary *params;
//請求地址 需要在子類init中初始化
@property (nonatomic,copy)   NSString *address;
//model緩存
@property (retain,nonatomic) KtCache *ktCache;

它通過持有 API 對象完成網路請求,可以定製自己的存儲邏輯,控制請求方式的選擇(長、短鏈接,JSON或protobuf)。
Model 應該對上層暴露一個非常簡單的調用介面,因為假設一個 Model 對應一個 URL,其實每次請求只需要設置好參數,就可以調用合適的方法發起請求了。
由於我們不能預知請求何時結束,所以需要設置請求完成時的回調,這也需要作為 Model 的一個屬性。

BaseItem

基類的 Item 主要是負責 property name 到 json path 的映設,以及 json 數據的解析。最核心的字典轉模型實現如下:

- (void)parseData:(NSDictionary *)data {
    Class cls = [self class];
    while (cls != [KtBaseItem class]) {
        NSDictionary *propertyList = [[KtClassHelper sharedInstance] propertyList:cls];
        for (NSString *key in [propertyList allKeys]) {
            NSString *typeString = [propertyList objectForKey:key];
            NSString* path = [self.jsonDataMap objectForKey:key];
            id value = [data objectAtPath:path];
            [self setfieldName:key fieldClassName:typeString value:value];
        }
        cls = class_getSuperclass(cls);
    }
}

完整代碼參考 Git 提交歷史:SHA-1:77c6392

如何使用

在實際使用時,首先要創建子類的 Modle 和 Item。子類的 Model 應該持有 Item 對象,並且在網路請求回調時,將 API 中攜帶的 JSON 數據賦值給 Item 對象。
這個 JSON 轉對象的過程在基類的 Item 中實現,子類的 Item 在創建時,需要指定屬性名和 JSON 路徑之間的對應關係。
對於上層來說,它需要生成一個 Model 對象,設置好它的路徑以及回調,這個回調一般是網路請求返回時 VC 的操作,比如調用 reloadData 方法。這時候的 VC 可以確定,網路請求的數據就存在 Model 持有的 Item 對象中。
具體代碼參考 Git 提交歷史:SHA-1:8981e28

下拉刷新

很多應用的 UITableview 都具有下拉刷新和上拉載入的功能,在實現這個功能時,我們主要考慮兩點:
1
隱藏底層的實現細節,對外暴露穩定易用的介面
2
Model 和 Item 如何實現
第一點已經是老生常談,參考 SHA-1 61ba974 就可以看到如何實現一個簡單的封裝。
重點在於對於 Model 和 Item 的改造。

ListItem

這個 Item 沒有什麼別的作用,就是定義了一個屬性 pageNumber,這是需要與服務端協商的。Model 將會根據這個屬性這個屬性判斷有沒有全部載入完。

// In .h
@interface KtBaseListItem : KtBaseItem
@property (nonatomic, assign) int pageNumber;
@end
// In .m
- (id)initWithData:(NSDictionary *)data {
    if (self = [super initWithData:data]) {
        self.pageNumber = [[NSString stringWithFormat:@"%@", [data objectForKey:@"page_number"]] intValue];
    }
    return self;
}

對於 Server 來說,如果每次都返回 page_number 無疑是非常低效的,因為每次參數都可能不同,計算總數據量是一項非常耗時的工作。因此在實際使用中,客戶端可以和 Server 約定,返回的結果中帶有 isHasNext 欄位。通過這個欄位,我們一樣可以判斷是否載入到最後一頁。

ListModel

它持有一個 ListItem 對象, 對外暴露一組載入方法,並且定義了一個協議 KtBaseListModelProtocol,這個協議中的方法是請求結束後將要執行的方法。

@protocol KtBaseListModelProtocol <NSObject>
@required
- (void)refreshRequestDidSuccess;
- (void)loadRequestDidSuccess;
- (void)didLoadLastPage;
- (void)handleAfterRequestFinish; // 請求結束後的操作,刷新tableview或關閉動畫等。
@optional
- (void)didLoadFirstPage;
@end
@interface KtBaseListModel : KtBaseModel
@property (nonatomic, strong) KtBaseListItem *listItem;
@property (nonatomic, weak) id<KtBaseListModelProtocol> delegate;
@property (nonatomic, assign) BOOL isRefresh; // 如果為是,表示刷新,否則為載入。
- (void)loadPage:(int)pageNumber;
- (void)loadNextPage;
- (void)loadPreviousPage;
@end

實際上,當 Server 端發生數據的增刪時,只傳 nextPage 這個參數是不能滿足要求的。兩次獲取的頁面並非完全沒有交集,很有可能他們具有重覆元素,所以 Model 還應該肩負起去重的任務。為了簡化問題,這裡就不完整實現了。

RefreshTableViewController

它實現了 ListMode 中定義的協議,提供了一些通用的方法,而具體的業務邏輯則由子類實現。

#pragma -mark KtBaseListModelProtocol
- (void)loadRequestDidSuccess {
    [self requestDidSuccess];
}
- (void)refreshRequestDidSuccess {
    [self.dataSource clearAllItems];
    [self requestDidSuccess];
}
- (void)handleAfterRequestFinish {
    [self.tableView stopRefreshingAnimation];
    [self.tableView reloadData];
}
- (void)didLoadLastPage {
    [self.tableView.mj_footer endRefreshingWithNoMoreData];
}
#pragma -mark KtTableViewDelegate
- (void)pullUpToRefreshAction {
    [self.listModel loadNextPage];
}
- (void)pullDownToRefreshAction {
    [self.listModel refresh];
}

實際使用

在一個 VC 中,它只需要繼承 RefreshTableViewController,然後實現 requestDidSuccess 方法即可。下麵展示一下 VC 的完整代碼,它超乎尋常的簡單:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createModel];
    // Do any additional setup after loading the view, typically from a nib.
}
- (void)createModel {
    self.listModel = [[KtMainTableModel alloc] initWithAddress:@"/mooclist.php"];
    self.listModel.delegate = self;
}
- (void)createDataSource {
    self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 這一步創建了數據源
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (void)requestDidSuccess {
    for (KtMainTableBookItem *book in ((KtMainTableModel *)self.listModel).tableViewItem.books) {
        KtTableViewBaseItem *item = [[KtTableViewBaseItem alloc] init];
        item.itemTitle = book.bookTitle;
        [self.dataSource appendItem:item];
    }
}

其他的判斷,比如請求結束時關閉動畫,最後一頁提示沒有更多數據,下拉刷新和上拉載入觸發的方法等公共邏輯已經被父類實現了。
具體代碼見 Git 提交歷史:SHA-1:0555db2
寫在結尾
網路請求的設計架構到此就全部結束了,它還有很多值的拓展的地方。還是那句老話,沒有通用的架構,只有最適合業務的架構。
我的 Demo 為了方便演示和閱讀,通常都是先實現底層的類和方法,然後再由上層調用。但實際上這種做法在實際開發中是不現實的。我們總是在發現大量冗餘,無意義的代碼後,才開始設計架構。
因此在我看來,真正的架構過程是當業務發生變更(通常是變複雜了)時,我們開始應該思考當前哪些操作是可以省略的(由父類或代理實現),最上層應該以何種方式調用底層的服務。一旦設計好了最上層的調用方式,就可以逐步向底層實現了。
由於本人水平也有限,本文的架構並不優秀,希望在深入理解設計模式,積累更多經驗後,再與大家分享收穫。

青山不改,綠水常流。謝謝大家!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文分別使用 SFC(模板方式)和 tsx 方式對 Element Plus *el-menu* 組件進行二次封裝,實現配置化的菜單,有了配置化的菜單,後續便可以根據路由動態渲染菜單。 ...
  • vue3中,新增了 defineComponent ,它並沒有實現任何的邏輯,只是把接收的 Object 直接返回,它的存在是完全讓傳入的整個對象獲得對應的類型,它的存在就是完全為了服務 TypeScript 而存在的。 我都知道普通的組件就是一個普通的對象,既然是一個普通的對象,那自然就不會獲得自 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、前言 入職的第一個需求是跟著一位前端大佬一起完成的一個活動項目。 由於是一起開發,當然不會放過閱讀大佬的代碼的機會。 因為我的頁面中需要使用到倒計時功能,發現大佬的已經寫了個現成的倒計時組件,於是直接就拿過來用了。 傳個參數就實現了功 ...
  • 本人的工作項目中,需求是: 點擊“列印”按鈕,打開pdf預覽彈出框,彈出框有:頭部選擇列印模板、列印方式、印表機,都是下拉選擇框;中部是pdf預覽塊;底部是確定列印。 準備工作: 預覽pdf,後端介面返回了pdf預覽地址,可線上直接打開。vue-pdf插件可以滿足需求。 選擇方式如果選擇本地列印,下 ...
  • 1.如果只比較兩個值的話 效果是這種的 // 這是<template>的 <el-row> <el-col :span="12"> <el-form-item label="計劃評審日期(起)" prop="planPsDateStart"> <el-date-picker v-model="vm. ...
  • 每日3題 1 以下代碼執行後,控制臺中的輸出內容為? // 以下代碼執行後,瀏覽器的控制臺中輸出的內容是什麼 var arr = [0, 1, 2]; arr[10] = 10; var newArr = arr.filter((x) => x undefined); console.log(new ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 uniapp上如何實現安卓app微信登錄功能?下麵本篇文章給大家分享一下uniapp上實現安卓app微信登錄的許可權申請、開發的具體操作流程,希望對大家有所幫助! 微信開放平臺提供了微信的一些開放介面,比如微信登錄、分享支付等,為其他各平臺 ...
  • 我的前端之旅。本節整合了"A Complete Guide to Flexbox"最新版本,介紹了flexbox的所有屬性,外帶幾個實用的例子。 ...
一周排行
    -Advertisement-
    Play Games
  • 不廢話,直接代碼 private Stack<Action> actionStack = new Stack<Action>(); private void SetCellValues() { var worksheet = Globals.ThisAddIn.Application.ActiveS ...
  • OpenAPI 規範是用於描述 HTTP API 的標準。該標準允許開發人員定義 API 的形狀,這些 API 可以插入到客戶端生成器、伺服器生成器、測試工具、文檔等中。儘管該標準具有普遍性和普遍性,但 ASP.NET Core 在框架內預設不提供對 OpenAPI 的支持。 當前 ASP.NET ...
  • @DateTimeFormat 和 @JsonFormat 是 Spring 和 Jackson 中用於處理日期時間格式的註解,它們有不同的作用: @DateTimeFormat @DateTimeFormat 是 Spring 框架提供的註解,用於指定字元串如何轉換為日期時間類型,以及如何格式化日 ...
  • 一、背景說明 1.1 效果演示 用python開發的爬蟲採集軟體,可自動抓取抖音評論數據,並且含二級評論! 為什麼有了源碼還開發界面軟體呢?方便不懂編程代碼的小白用戶使用,無需安裝python、無需懂代碼,雙擊打開即用! 軟體界面截圖: 爬取結果截圖: 以上。 1.2 演示視頻 軟體運行演示視頻:見 ...
  • SpringBoot筆記 SpringBoot文檔 官網: https://spring.io/projects/spring-boot 學習文檔: https://docs.spring.io/spring-boot/docs/current/reference/html/ 線上API: http ...
  • 作為後端工程師,多數情況都是給別人提供介面,寫的好不好使你得重視起來。 最近我手頭一些活,需要和外部公司對接,我們需要提供一個介面文檔,這樣可以節省雙方時間、也可以防止後續扯皮。這是就要考驗我的介面是否規範化。 1. 介面名稱清晰、明確 顧名思義,介面是做什麼的,是否準確、清晰?讓使用這一眼就能知道 ...
  • 本文介紹基於Python語言,遍歷文件夾並從中找到文件名稱符合我們需求的多個.txt格式文本文件,並從上述每一個文本文件中,找到我們需要的指定數據,最後得到所有文本文件中我們需要的數據的合集的方法~ ...
  • Java JUC&多線程 基礎完整版 目錄Java JUC&多線程 基礎完整版1、 多線程的第一種啟動方式之繼承Thread類2、多線程的第二種啟動方式之實現Runnable介面3、多線程的第三種實現方式之實現Callable介面4、多線的常用成員方法5、線程的優先順序6、守護線程7、線程的讓出8、線 ...
  • 實時識別關鍵詞是一種能夠將搜索結果提升至新的高度的API介面。它可以幫助我們更有效地分析文本,並提取出關鍵詞,以便進行進一步的處理和分析。 該介面是挖數據平臺提供的,有三種模式:精確模式、全模式和搜索引擎模式。不同的模式在分詞的方式上有所不同,適用於不同的場景。 首先是精確模式。這種模式會儘量將句子 ...
  • 1 為啥要折騰搭建一個專屬圖床? 技術大佬寫博客都用 md 格式,要在多平臺發佈,圖片就得有外鏈 後續如博客遷移,國內博客網站如掘金,簡書,語雀等都做了防盜鏈,圖片無法遷移 2 為啥選擇CloudFlare R2 跳轉:https://dash.cloudflare.com/ 有白嫖額度 免費 CD ...