主頁 > 移動端開發 > iOS開發 - 面向物件設計的六大設計原則(附 Demo & UML類圖)

iOS開發 - 面向物件設計的六大設計原則(附 Demo & UML類圖)

2021-06-09 20:15:28 移動端開發

學習初衷與講解方式

筆者想在 iOS 從業第三年結束之前系統學習一下關于設計模式方面的知識,而在學習設計模式之前,覺得更有必要先學習面向物件設計(OOD:Object Oriented Design)的幾大設計原則,為后面設計模式的學習打下基礎,

本篇分享的就是筆者近階段學習和總結的面向物件設計的六個設計原則:

縮寫 英文名稱 中文名稱
SRP Single Responsibility Principle 單一職責原則
OCP Open Close Principle 開閉原則
LSP Liskov Substitution Principle 里氏替換原則
LoD Law of Demeter ( Least Knowledge Principle) 迪米特法則(最少知道原則)
ISP Interface Segregation Principle 介面分離原則
DIP Dependency Inversion Principle 依賴倒置原則

注意,通常所說的SOLID(上方表格縮寫的首字母,從上到下)設計原則沒有包含本篇介紹的迪米特法則,而只有其他五項,另外,本篇不包含合成/聚合復用原則(CARP),因為筆者認為該原則沒有其他六個原則典型,而且在實踐中也不容易違背,有興趣的同學可以自行查資料學習,

在下一章節筆者將分別講解這些設計原則,講解的方式是將概念與代碼及其對應的UML 類圖結合起來講解的方式,

代碼的語言使用的是筆者最熟悉的Objective-C語言,雖然是一個比較小眾的語言,但是因為有 UML 類圖的幫助,而且主流的面向物件語言關于類,介面(Objective-C里面是協議)的使用在形式上類似,所以筆者相信語言的小眾不會對知識的理解產生太大的阻力,

另外,在每個設計模式的講解里,筆者會首先描述一個應用場景(需求點),接著用兩種設計的代碼來進行對比講解:先提供相對不好的設計的代碼,再提供相對好的設計的代碼,而且兩種代碼都會附上標準的 UML 類圖來進行更形象地對比,幫助大家來理解,同時也可以幫助不了解 UML 類圖的讀者先簡單熟悉一下 UML 類圖的語法,

本篇文章所展示的Demo和UML 類圖都在筆者維護的一個專門的GitHub庫中:object-oriented-design,

六大設計原則

本篇講解六大設計原則的順序大致按照難易程式排列,在這里最先講解開閉原則,因為其在理解上比較簡單,而且也是其他設計原則的基石,

注意:

  1. 六個原則的講解所用的例子之間并沒有關聯,所以閱讀順序可以按照讀者的喜好來定,
  2. Java語言里的介面在Objective-C里面叫做協議,雖然Demo是用Objective-C寫的,但是因為協議的叫法比較小眾,故后面一律用介面代替協議這個說法,

原則一:開閉原則(Open Close Principle)

定義

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

即:一個軟體物體如類、模塊和函式應該對擴展開放,對修改關閉,

定義的解讀

  • 用抽象構建框架,用實作擴展細節,
  • 不以改動原有類的方式來實作新需求,而是應該以實作事先抽象出來的介面(或具體類繼承抽象類)的方式來實作,

優點

實踐開閉原則的優點在于可以在不改動原有代碼的前提下給程式擴展功能,增加了程式的可擴展性,同時也降低了程式的維護成本,

代碼講解

下面通過一個簡單的關于在線課程的例子講解一下開閉原則的實踐,

需求點

設計一個在線課程類:

由于教學資源有限,開始的時候只有類似于博客的,通過文字講解的課程,
但是隨著教學資源的增多,后來增加了視頻課程,音頻課程以及直播課程,

先來看一下不好的設計:

不好的設計

最開始的文字課程類:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //課程名稱
@property (nonatomic, copy) NSString *courseIntroduction;  //課程介紹
@property (nonatomic, copy) NSString *teacherName;         //講師姓名
@property (nonatomic, copy) NSString *content;             //課程內容

@end

Course類宣告了最初的在線課程所需要包含的資料:

  • 課程名稱
  • 課程介紹
  • 講師姓名
  • 文字內容

接著按照上面所說的需求變更:增加了視頻,音頻,直播課程:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //課程名稱
@property (nonatomic, copy) NSString *courseIntroduction;  //課程介紹
@property (nonatomic, copy) NSString *teacherName;         //講師姓名
@property (nonatomic, copy) NSString *content;             //文字內容

//新需求:視頻課程
@property (nonatomic, copy) NSString *videoUrl;

//新需求:音頻課程
@property (nonatomic, copy) NSString *audioUrl;

//新需求:直播課程
@property (nonatomic, copy) NSString *liveUrl;

@end

三種新增的課程都在原Course類中添加了對應的url,也就是每次添加一個新的型別的課程,都在原有Course類里面修改:新增這種課程需要的資料,

這就導致:我們從Course類實體化的視頻課程物件會包含并不屬于自己的資料:audioUrlliveUrl:這樣就造成了冗余,視頻課程物件并不是純粹的視頻課程物件,它包含了音頻地址,直播地址等成員,

很顯然,這個設計不是一個好的設計,因為(對應上面兩段敘述):

  1. 隨著需求的增加,需要反復修改之前創建的類,
  2. 給新增的類造成了不必要的冗余,

之所以會造成上述兩個缺陷,是因為該設計沒有遵循對修改關閉,對擴展開放的開閉原則,而是反其道而行之:開放修改,而且不給擴展提供便利,

難么怎么做可以遵循開閉原則呢?下面看一下遵循開閉原則的較好的設計:

較好的設計

首先在Course類中僅僅保留所有課程都含有的資料:

//================== Course.h ==================

@interface Course : NSObject

@property (nonatomic, copy) NSString *courseTitle;         //課程名稱
@property (nonatomic, copy) NSString *courseIntroduction;  //課程介紹
@property (nonatomic, copy) NSString *teacherName;         //講師姓名

接著,針對文字課程,視頻課程,音頻課程,直播課程這三種新型的課程采用繼承Course類的方式,而且繼承后,添加自己獨有的資料:

文字課程類:

//================== TextCourse.h ==================

@interface TextCourse : Course

@property (nonatomic, copy) NSString *content;             //文字內容

@end

視頻課程類:

//================== VideoCourse.h ==================

@interface VideoCourse : Course

@property (nonatomic, copy) NSString *videoUrl;            //視頻地址

@end

音頻課程類:

//================== AudioCourse.h ==================

@interface AudioCourse : Course

@property (nonatomic, copy) NSString *audioUrl;            //音頻地址

@end

直播課程類:

//================== LiveCourse.h ==================

@interface LiveCourse : Course

@property (nonatomic, copy) NSString *liveUrl;             //直播地址

@end

這樣一來,上面的兩個問題都得到了解決:

  1. 隨著課程型別的增加,不需要反復修改最初的父類(Course),只需要新建一個繼承于它的子類并在子類中添加僅屬于該子類的資料(或行為)即可,
  2. 因為各種課程獨有的資料(或行為)都被分散到了不同的課程子類里,所以每個子類的資料(或行為)沒有任何冗余,

而且對于第二點:或許今后的視頻課程可以有高清地址,視頻加速功能,而這些功能只需要在VideoCourse類里添加即可,因為它們都是視頻課程所獨有的,同樣地,直播課程后面還可以支持在線問答功能,也可以僅加在LiveCourse里面,

我們可以看到,正是由于最初程式設計合理,所以對后面需求的增加才會處理得很好,

下面來看一下這兩個設計的UML 類圖,可以更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐開閉原則:

實踐了開閉原則:

在實踐了開閉原則的 UML 類圖中,四個課程類繼承了Course類并添加了自己獨有的屬性,(在 UML 類圖中:實線空心三角箭頭代表繼承關系:由子類指向其父類)

如何實踐

為了更好地實踐開閉原則,在設計之初就要想清楚在該場景里哪些資料(或行為)是一定不變(或很難再改變)的,哪些是很容易變動的,將后者抽象成介面或抽象方法,以便于在將來通過創造具體的實作應對不同的需求,

原則二:單一職責原則(Single Responsibility Principle)

定義

A class should have a single responsibility, where a responsibility is nothing but a reason to change.

即:一個類只允許有一個職責,即只有一個導致該類變更的原因,

定義的解讀

  • 類職責的變化往往就是導致類變化的原因:也就是說如果一個類具有多種職責,就會有多種導致這個類變化的原因,從而導致這個類的維護變得困難,

  • 往往在軟體開發中隨著需求的不斷增加,可能會給原來的類添加一些本來不屬于它的一些職責,從而違反了單一職責原則,如果我們發現當前類的職責不僅僅有一個,就應該將本來不屬于該類真正的職責分離出去,

  • 不僅僅是類,函式(方法)也要遵循單一職責原則,即:一個函式(方法)只做一件事情,如果發現一個函式(方法)里面有不同的任務,則需要將不同的任務以另一個函式(方法)的形式分離出去,

優點

如果類與方法的職責劃分得很清晰,不但可以提高代碼的可讀性,更實際性地更降低了程式出錯的風險,因為清晰的代碼會讓bug無處藏身,也有利于bug的追蹤,也就是降低了程式的維護成本,

代碼講解

單一職責原則的demo比較簡單,通過物件(屬性)的設計上講解已經足夠,不需要具體的客戶端呼叫,我們先看一下需求點:

需求點

初始需求:需要創造一個員工類,這個類有員工的一些基本資訊,

新需求:增加兩個方法:

  • 判定員工在今年是否升職
  • 計算員工的薪水

先來看一下不好的設計:

不好的設計

//================== Employee.h ==================

@interface Employee : NSObject

//============ 初始需求 ============
@property (nonatomic, copy) NSString *name;       //員工姓名
@property (nonatomic, copy) NSString *address;    //員工住址
@property (nonatomic, copy) NSString *employeeID; //員工ID

//============ 新需求 ============
//計算薪水
- (double)calculateSalary;

//今年是否晉升
- (BOOL)willGetPromotionThisYear;

@end

由上面的代碼可以看出:

  • 在初始需求下,我們創建了Employee這個員工類,并宣告了3個員工資訊的屬性:員工姓名,地址,員工ID,
  • 在新需求下,兩個方法直接加到了員工類里面,

新需求的做法看似沒有問題,因為都是和員工有關的,但卻違反了單一職責原則:因為這兩個方法并不是員工本身的職責

  • calculateSalary這個方法的職責是屬于會計部門的:薪水的計算是會計部門負責,
  • willPromotionThisYear這個方法的職責是屬于人事部門的:考核與晉升機制是人事部門負責,

而上面的設計將本來不屬于員工自己的職責強加進了員工類里面,而這個類的設計初衷(原始職責)就是單純地保留員工的一些資訊而已,因此這么做就是給這個類引入了新的職責,故此設計違反了單一職責原則

我們可以簡單想象一下這么做的后果是什么:如果員工的晉升機制變了,或者稅收政策等影響員工工資的因素變了,我們還需要修改當前這個類,

那么怎么做才能不違反單一職責原則呢?- 我們需要將這兩個方法(責任)分離出去,讓本應該處理這類任務的類來處理,

較好的設計

我們保留員工類的基本資訊:

//================== Employee.h ==================

@interface Employee : NSObject

//初始需求
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *address;
@property (nonatomic, copy) NSString *employeeID;

接著創建新的會計部門類:

//================== FinancialApartment.h ==================

#import "Employee.h"

//會計部門類
@interface FinancialApartment : NSObject

//計算薪水
- (double)calculateSalary:(Employee *)employee;

@end

人事部門類:

//================== HRApartment.h ==================

#import "Employee.h"

//人事部門類
@interface HRApartment : NSObject

//今年是否晉升
- (BOOL)willGetPromotionThisYear:(Employee*)employee;

@end

通過創建了兩個分別專門處理薪水和晉升的部門,會計部門和人事部門的類:FinancialApartment 和 HRApartment,把兩個任務(責任)分離了出去,讓本該處理這些職責的類來處理這些職責,

這樣一來,不僅僅在此次新需求中滿足了單一職責原則,以后如果還要增加人事部門和會計部門處理的任務,就可以直接在這兩個類里面添加即可,

下面來看一下這兩個設計的UML 類圖,可以更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐單一職責原則:

實踐了單一職責原則:

可以看到,在實踐了單一職責原則的 UML 類圖中,不屬于Employee的兩個職責被分類了FinancialApartment類 和 HRApartment類,(在 UML 類圖中,虛線箭頭表示依賴關系,常用在方法引數等,由依賴方指向被依賴方)

上面說過除了類要遵循單一職責設計原則之外,在函式(方法)的設計上也要遵循單一職責的設計原則,因函式(方法)的單一職責原則理解起來比較容易,故在這里就不提供Demo和UML 類圖了,

可以簡單舉一個例子:

APP的默認導航欄的樣式是這樣的:

  • 白色底
  • 黑色標題
  • 底部有陰影

那么創建默認導航欄的偽代碼可能是這樣子的:

//默認樣式的導航欄
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{

    //create white color background view

    //create black color title

    //create shadow bottom
}

現在我們可以用這個方法統一創建默認的導航欄了,
但是過不久又有新的需求來了,有的頁面的導航欄需要做成透明的,因此需要一個透明樣式的導航欄:

  • 透明底
  • 白色標題
  • 底部無陰影

針對這個需求,我們可以新增一個方法:

//透明樣式的導航欄
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{

    //create transparent color background view

    //create white color title
}

看出問題來了么?在這兩個方法里面,創造background view和 title color title的方法的差別僅僅是顏色不同而已,而其他部分的代碼是重復的,
因此我們應該將這兩個方法抽出來:

//根據傳入的顏色引數設定導航欄的背景色
- (void)createBackgroundViewWithColor:(UIColor)color;

//根據傳入的標題字串和顏色引數設定標題
- (void)createTitlewWithColorWithTitle:(NSString *)title color:(UIColor)color;

而且上面的制造陰影的部分也可以作為方法抽出來:

- (void)createShadowBottom;

這樣一來,原來的兩個方法可以寫成:

//默認樣式的導航欄
- (void)createDefaultNavigationBarWithTitle:(NSString *)title{

    //設定白色背景
    [self createBackgroundViewWithColor:[UIColor whiteColor]];

    //設定黑色標題
    [self createTitlewWithColorWithTitle:title color:[UIColor blackColor]];

    //設定底部陰影
    [self createShadowBottom];
}

//透明樣式的導航欄
- (void)createTransParentNavigationBarWithTitle:(NSString *)title{

    //設定透明背景
    [self createBackgroundViewWithColor:[UIColor clearColor]];

    //設定白色標題
    [self createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];
}

而且我們也可以將里面的方法拿出來在外面呼叫也可以:

設定默認樣式的導航欄:

//設定白色背景
[navigationBar createBackgroundViewWithColor:[UIColor whiteColor]];

//設定黑色標題
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor blackColor]];

//設定陰影
[navigationBar createShadowBottom];

設定透明樣式的導航欄:

//設定透明色背景
[navigationBar createBackgroundViewWithColor:[UIColor clearColor]];

//設定白色標題
[navigationBar createTitlewWithColorWithTitle:title color:[UIColor whiteColor]];

這樣一來,無論寫在一個大方法里面呼叫或是分別在外面呼叫,都能很清楚地看到導航欄的每個元素是如何生成的,因為每個職責都分配到了一個單獨的方法里面,而且還有一個好處是,透明導航欄如果遇到淺色背景的話,使用白色字體不如使用黑色字體好,所以遇到這種情況我們可以在createTitlewWithColorWithTitle:color:方法里面傳入黑色色值,
而且今后可能還會有更多的導航欄樣式,那么我們只需要分別改變傳入的色值即可,不需要有大量的重復代碼了,修改起來也很方便,

如何實踐

對于上面的員工類的例子,或許是因為我們先入為主,知道一個公司的合理組織架構,覺得這么設計理所當然,但是在實際開發中,我們很容易會將不同的責任揉在一起,這點還是需要開發者注意的,

原則三:依賴倒置原則(Dependency Inversion Principle)

定義

  • Depend upon Abstractions. Do not depend upon concretions.
  • Abstractions should not depend upon details. Details should depend upon abstractions
  • High-level modules should not depend on low-level modules. Both should depend on abstractions.

即:

  • 依賴抽象,而不是依賴實作,
  • 抽象不應該依賴細節;細節應該依賴抽象,
  • 高層模塊不能依賴低層模塊,二者都應該依賴抽象,

定義解讀

  • 針對介面編程,而不是針對實作編程,
  • 盡量不要從具體的類派生,而是以繼承抽象類或實作介面來實作,
  • 關于高層模塊與低層模塊的劃分可以按照決策能力的高低進行劃分,業務層自然就處于上層模塊,邏輯層和資料層自然就歸類為底層,

優點

通過抽象來搭建框架,建立類和類的關聯,以減少類間的耦合性,而且以抽象搭建的系統要比以具體實作搭建的系統更加穩定,擴展性更高,同時也便于維護,

代碼講解

下面通過一個模擬專案開發的例子來講解依賴倒置原則,

需求點

實作下面這樣的需求:

用代碼模擬一個實際專案開發的場景:前端和后端開發人員開發同一個專案,

不好的設計

首先生成兩個類,分別對應前端和后端開發者:

前端開發者:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject

- (void)writeJavaScriptCode;

@end

//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeJavaScriptCode{
    NSLog(@"Write JavaScript code");
}

@end

后端開發者:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject

- (void)writeJavaCode;

@end

//================== BackEndDeveloper.m ==================

@implementation BackEndDeveloper

- (void)writeJavaCode{
    NSLog(@"Write Java code");
}
@end

這兩個開發者分別對外提供了自己開發的方法:writeJavaScriptCodewriteJavaCode

接著創建一個Project類:

//================== Project.h ==================

@interface Project : NSObject

//構造方法,傳入開發者的陣列
- (instancetype)initWithDevelopers:(NSArray *)developers;

//開始開發
- (void)startDeveloping;

@end

//================== Project.m ==================

#import "Project.h"
#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray *_developers;
}

- (instancetype)initWithDevelopers:(NSArray *)developers{

    if (self = [super init]) {
        _developers = developers;
    }
    return self;
}

- (void)startDeveloping{

    [_developers enumerateObjectsUsingBlock:^(id  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {

        if ([developer isKindOfClass:[FrondEndDeveloper class]]) {

            [developer writeJavaScriptCode];

        }else if ([developer isKindOfClass:[BackEndDeveloper class]]){

            [developer writeJavaCode];

        }else{
            //no such developer
        }
    }];
}

@end

Project類中,我們首先通過一個構造器方法,將開發者的陣列傳入project的實體物件,然后在開始開發的方法startDeveloping里面,遍歷陣列并判斷元素型別的方式讓不同型別的開發者呼叫和自己對應的函式,

思考一下,這樣的設計有什么問題?

問題一:

假如后臺的開發語言改成了GO語言,那么上述代碼需要改動兩個地方:

  • BackEndDeveloper:需要向外提供一個writeGolangCode方法,
  • Project類的startDeveloping方法里面需要將BackEndDeveloper類的writeJavaCode改成writeGolangCode

問題二:

假如后期老板要求做移動端的APP(需要iOS和安卓的開發者),那么上述代碼仍然需要改動兩個地方:

  • 還需要給Project類的構造器方法里面傳入IOSDeveloperAndroidDeveloper兩個類,而且按照現有的設計,還要分別向外部提供writeSwiftCodewriteKotlinCode
  • Project類的startDeveloping方法里面需要再多兩個elseif判斷,專門判斷IOSDeveloperAndroidDeveloper這兩個類,

開發安卓的代碼也可以用Java,但是為了和后臺的開發代碼區分一下,這里用了同樣可以開發安卓的Kotlin語言,

很顯然,在這兩種假設的場景下,高層模塊(Project)都依賴了低層模塊(BackEndDeveloper)的改動,因此上述設計不符合依賴倒置原則

那么該如何設計才可以符合依賴倒置原則呢?

答案是將開發者寫代碼的方法抽象出來,讓Project類不再依賴所有低層的開發者類的具體實作,而是依賴抽象,而且從下至上,所有底層的開發者類也都依賴這個抽象,通過實作這個抽象來做自己的任務

這個抽象可以用介面,也可以用抽象類的方式來做,在這里筆者用使用介面的方式進行講解:

較好的設計

首先,創建一個介面,介面里面有一個寫代碼的方法writeCode

//================== DeveloperProtocol.h ==================

@protocol DeveloperProtocol <NSObject>

- (void)writeCode;

@end

然后,讓前端程式員和后端程式員類實作這個介面(遵循這個協議)并按照自己的方式實作:

前端程式員類:

//================== FrondEndDeveloper.h ==================

@interface FrondEndDeveloper : NSObject<DeveloperProtocol>
@end

//================== FrondEndDeveloper.m ==================

@implementation FrondEndDeveloper

- (void)writeCode{
    NSLog(@"Write JavaScript code");
}
@end

后端程式員類:

//================== BackEndDeveloper.h ==================

@interface BackEndDeveloper : NSObject<DeveloperProtocol>
@end

//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{
    NSLog(@"Write Java code");
}
@end

最后我們看一下新設計后的Project類:

//================== Project.h ==================

#import "DeveloperProtocol.h"

@interface Project : NSObject

//只需傳入遵循DeveloperProtocol的物件陣列即可
- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers;

//開始開發
- (void)startDeveloping;

@end

//================== Project.m ==================

#import "FrondEndDeveloper.h"
#import "BackEndDeveloper.h"

@implementation Project
{
    NSArray <id <DeveloperProtocol>>* _developers;
}

- (instancetype)initWithDevelopers:(NSArray <id <DeveloperProtocol>>*)developers{

    if (self = [super init]) {
        _developers = developers;
    }
    return self;

}

- (void)startDeveloping{

    //每次回圈,直接向物件發送writeCode方法即可,不需要判斷
    [_developers enumerateObjectsUsingBlock:^(id<DeveloperProtocol>  _Nonnull developer, NSUInteger idx, BOOL * _Nonnull stop) {

        [developer writeCode];
    }];

}

@end

新的Project的構造方法只需傳入遵循DeveloperProtocol協議的物件構成的陣列即可,這樣也比較符合現實中的需求:只需要會寫代碼就可以加入到專案中,

而新的startDeveloping方法里:每次回圈,直接向當前物件發送writeCode方法即可,不需要對程式員的型別做判斷,因為這個物件一定是遵循DeveloperProtocol介面的,而遵循該介面的物件一定會實作writeCode方法(就算不實作也不會引起重大錯誤),

現在新的設計接受完了,我們通過上面假設的兩個情況來和之前的設計做個對比:

假設1:后臺的開發語言改成了GO語言

在這種情況下,只需更改BackEndDeveloper類里面對于DeveloperProtocol介面的writeCode方法的實作即可:

//================== BackEndDeveloper.m ==================
@implementation BackEndDeveloper

- (void)writeCode{

    //Old:
    //NSLog(@"Write Java code");

    //New:
    NSLog(@"Write Golang code");
}
@end

而在Project里面不需要修改任何代碼,因為Project類只依賴了介面方法WriteCode,沒有依賴其具體的實作,

我們接著看一下第二個假設:

假設2:后期老板要求做移動端的APP(需要iOS和安卓的開發者)

在這個新場景下,我們只需要將新創建的兩個開發者類:IOSDeveloperAndroidDeveloper分別實作DeveloperProtocol介面的writeCode方法即可,

同樣,Project的介面和實作代碼都不用修改:客戶端只需要在Project的構建方法的陣列引數里面添加這兩個新類的實體即可,不需要在startDeveloping方法里面添加型別判斷,原因同上,

我們可以看到,新設計很好地在高層類(Project)與低層類(各種developer類)中間加了一層抽象,解除了二者在舊設計中的耦合,使得在低層類中的改動沒有影響到高層類,

同樣是抽象,新設計同樣也可以用抽象類的方式:創建一個Developer的抽象類并提供一個writeCode方法,讓不同的開發者類繼承與它并按照自己的方式實作writeCode方法,這樣一來,在Project類的構造方法就是傳入已Developer型別為元素的陣列了,有興趣的小伙伴可以自己實作一下~

下面來看一下這兩個設計的UML 類圖,可以更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐依賴倒置原則:

實踐了依賴倒置原則:

在實踐了依賴倒置原則的 UML 類圖中,我們可以看到Project僅僅依賴于新的介面;而且低層的FrondEndDevelopeBackEndDevelope類按照自己的方式實作了這個介面:通過介面解除了原有的依賴,(在 UML 類圖中,虛線三角箭頭表示介面實線,由實作方指向介面)

如何實踐

今后在處理高低層模塊(類)互動的情景時,盡量將二者的依賴通過抽象的方式解除掉,實作方式可以是通過介面也可以是抽象類的方式,

原則四:介面分離原則(Interface Segregation Principle)

定義

Many client specific interfaces are better than one general purpose interface.

即:多個特定的客戶端介面要好于一個通用性的總介面,

定義解讀

  • 客戶端不應該依賴它不需要實作的介面,
  • 不建立龐大臃腫的介面,應盡量細化介面,介面中的方法應該盡量少,

需要注意的是:介面的粒度也不能太小,如果過小,則會造成介面數量過多,使設計復雜化,

優點

避免同一個介面里面包含不同類職責的方法,介面責任劃分更加明確,符合高內聚低耦合的思想,

代碼講解

下面通過一個餐廳服務的例子講解一下介面分離原則,

需求點

現在的餐廳除了提供傳統的店內服務,多數也都支持網上下單,網上支付功能,寫一些介面方法來涵蓋餐廳的所有的下單及支付功能,

不好的設計

//================== RestaurantProtocol.h ==================

@protocol RestaurantProtocol <NSObject>

- (void)placeOnlineOrder;         //下訂單:online
- (void)placeTelephoneOrder;      //下訂單:通過電話
- (void)placeWalkInCustomerOrder; //下訂單:在店里

- (void)payOnline;                //支付訂單:online
- (void)payInPerson;              //支付訂單:在店里支付

@end

在這里宣告了一個介面,它包含了下單和支付的幾種方式:

  • 下單:

    • online下單
    • 電話下單
    • 店里下單(店內服務)
  • 支付

    • online支付(適用于online下單和電話下單的顧客)
    • 店里支付(店內服務)

這里先不討論電話下單的顧客是用online支付還是店內支付,

對應的,我們有三種下單方式的顧客:

1.online下單,online支付的顧客

//================== OnlineClient.h ==================

#import "RestaurantProtocol.h"

@interface OnlineClient : NSObject<RestaurantProtocol>
@end

//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOnlineOrder{
    NSLog(@"place on line order");
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}
@end

2.電話下單,online支付的顧客

//================== TelephoneClient.h ==================

#import "RestaurantProtocol.h"

@interface TelephoneClient : NSObject<RestaurantProtocol>
@end

//================== TelephoneClient.m ==================

@implementation TelephoneClient

- (void)placeOnlineOrder{
    //not necessarily
}

- (void)placeTelephoneOrder{
    NSLog(@"place telephone order");
}

- (void)placeWalkInCustomerOrder{
    //not necessarily
}

- (void)payOnline{
    NSLog(@"pay on line");
}

- (void)payInPerson{
    //not necessarily
}

@end

3.在店里下單并支付的顧客:

//================== WalkinClient.h ==================

#import "RestaurantProtocol.h"

@interface WalkinClient : NSObject<RestaurantProtocol>
@end

//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOnlineOrder{
   //not necessarily
}

- (void)placeTelephoneOrder{
    //not necessarily
}

- (void)placeWalkInCustomerOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOnline{
   //not necessarily
}

- (void)payInPerson{
    NSLog(@"pay in person");
}

@end

我們發現,并不是所有顧客都必須要實作RestaurantProtocol里面的所有方法,由于介面方法的設計造成了冗余,因此該設計不符合介面隔離原則

注意,Objective-C中的協議可以通過@optional關鍵字設定不需要必須實作的方法,該特性不與介面分離原則沖突:只要屬于同一類責任的介面,都可以放入同一介面中,

那么如何做才符合介面隔離原則呢?我們來看一下較好的設計,

較好的設計

要符合介面隔離原則,只需要將不同型別的介面分離出來即可,我們將原來的RestaurantProtocol介面拆分成兩個介面:下單介面和支付介面,

下單介面:

//================== RestaurantPlaceOrderProtocol.h ==================

@protocol RestaurantPlaceOrderProtocol <NSObject>

- (void)placeOrder;

@end

支付介面:

//================== RestaurantPaymentProtocol.h ==================

@protocol RestaurantPaymentProtocol <NSObject>

- (void)payOrder;

@end

現在有了下單介面和支付介面,我們就可以讓不同的客戶來以自己的方式實作下單和支付操作了:

首先創建一個所有客戶的父類,來遵循這個兩個介面:

//================== Client.h ==================

#import "RestaurantPlaceOrderProtocol.h"
#import "RestaurantPaymentProtocol.h"

@interface Client : NSObject<RestaurantPlaceOrderProtocol,RestaurantPaymentProtocol>
@end

接著另online下單,電話下單,店內下單的顧客繼承這個父類,分別實作這兩個介面的方法:

1.online下單,online支付的顧客

//================== OnlineClient.h ==================

#import "Client.h"
@interface OnlineClient : Client
@end

//================== OnlineClient.m ==================

@implementation OnlineClient

- (void)placeOrder{
    NSLog(@"place on line order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end

2.電話下單,online支付的顧客

//================== TelephoneClient.h ==================
#import "Client.h"
@interface TelephoneClient : Client
@end

//================== TelephoneClient.m ==================
@implementation TelephoneClient

- (void)placeOrder{
    NSLog(@"place telephone order");
}

- (void)payOrder{
    NSLog(@"pay on line");
}

@end

3.在店里下單并支付顧客:

//================== WalkinClient.h ==================

#import "Client.h"
@interface WalkinClient : Client
@end

//================== WalkinClient.m ==================

@implementation WalkinClient

- (void)placeOrder{
    NSLog(@"place walk in customer order");
}

- (void)payOrder{
    NSLog(@"pay in person");
}

@end

因為我們把不同職責的介面拆開,使得介面的責任更加清晰,簡潔明了,不同的客戶端可以根據自己的需求遵循所需要的介面來以自己的方式實作,

而且今后如果還有和下單或者支付相關的方法,也可以分別加入到各自的介面中,避免了介面的臃腫,同時也提高了程式的內聚性,

下面來看一下這兩個設計的UML 類圖,可以更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐介面分離原則:

實踐了介面分離原則:

通過遵守介面分離原則,介面的設計變得更加簡潔,而且各種客戶類不需要實作自己不需要實作的介面,

如何實踐

在設計介面時,尤其是在向現有的介面添加方法時,我們需要仔細斟酌這些方法是否是處理同一類任務的:如果是則可以放在一起;如果不是則需要做拆分,

做iOS開發的朋友對UITableViewUITableViewDelegateUITableViewDataSource這兩個協議應該會非常熟悉,這兩個協議里的方法都是與UITableView相關的,但iOS SDK的設計者卻把這些方法放在不同的兩個協議中,原因就是這兩個協議所包含的方法所處理的任務是不同的兩種:

  • UITableViewDelegate:含有的方法是UITableView的實體告知其代理一些點擊事件的方法,即事件的傳遞,方向是從UITableView的實體到其代理,
  • UITableViewDataSource:含有的方法是UITableView的代理傳給UITableView一些必要資料供UITableView展示出來,即資料的傳遞,方向是從UITableView的代理到UITableView

很顯然,UITableView協議的設計者很好地實踐了介面分離的原則,值得我們大家學習,

原則五:迪米特法則(Law of Demeter)

定義

You only ask for objects which you directly need.

即:一個物件應該對盡可能少的物件有接觸,也就是只接觸那些真正需要接觸的物件,

定義解讀

  • 迪米特法則也叫做最少知道原則(Least Know Principle), 一個類應該只和它的成員變數,方法的輸入,回傳引數中的類作交流,而不應該引入其他的類(間接交流),

優點

實踐迪米特法則可以良好地降低類與類之間的耦合,減少類與類之間的關聯程度,讓類與類之間的協作更加直接,

代碼講解

下面通過一個簡單的關于汽車的例子來講解一下迪米特法則,

需求點

設計一個汽車類,包含汽車的品牌名稱,引擎等成員變數,提供一個方法回傳引擎的品牌名稱,

不好的設計

Car類:

//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject

//構造方法
- (instancetype)initWithEngine:(GasEngine *)engine;

//回傳私有成員變數:引擎的實體
- (GasEngine *)usingEngine;

@end

//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car
{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{

    self = [super init];

    if (self) {
        _engine = engine;
    }
    return self;
}

- (GasEngine *)usingEngine{

    return _engine;
}

@end

從上面可以看出,Car的構造方法需要傳入一個引擎的實體物件,而且因為引擎的實體物件被賦到了Car物件的私有成員變數里面,所以Car類給外部提供了一個回傳引擎物件的方法:usingEngine

而這個引擎類GasEngine有一個品牌名稱的成員變數brandName

//================== GasEngine.h ==================
@interface GasEngine : NSObject

@property (nonatomic, copy) NSString *brandName;

@end

這樣一來,客戶端就可以拿到引擎的品牌名稱了:

//================== Client.m ==================

#import "GasEngine.h"
#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{

    GasEngine *engine = [car usingEngine];
    NSString *engineBrandName = engine.brandName;//獲取到了引擎的品牌名稱
    return engineBrandName;
}

上面的設計完成了需求,但是卻違反了迪米特法則,原因是在客戶端的findCarEngineBrandName:中引入了和入參(Car)和回傳值(NSString)無關的GasEngine物件,增加了客戶端與
GasEngine的耦合,而這個耦合顯然是不必要更是可以避免的,

接下來我們看一下如何設計可以避免這種耦合:

較好的設計

同樣是Car這個類,我們去掉原有的回傳引擎物件的方法,而是增加一個直接回傳引擎品牌名稱的方法:

//================== Car.h ==================

@class GasEngine;

@interface Car : NSObject

//構造方法
- (instancetype)initWithEngine:(GasEngine *)engine;

//直接回傳引擎品牌名稱
- (NSString *)usingEngineBrandName;

@end

//================== Car.m ==================

#import "Car.h"
#import "GasEngine.h"

@implementation Car
{
    GasEngine *_engine;
}

- (instancetype)initWithEngine:(GasEngine *)engine{

    self = [super init];

    if (self) {
        _engine = engine;
    }
    return self;
}

- (NSString *)usingEngineBrandName{
    return _engine.brand;
}

@end

因為直接usingEngineBrandName直接回傳了引擎的品牌名稱,所以在客戶端里面就可以直接拿到這個值,而不需要間接地通過原來的GasEngine實體來獲取,

我們看一下客戶端操作的變化:

//================== Client.m ==================

#import "Car.h"

- (NSString *)findCarEngineBrandName:(Car *)car{

    NSString *engineBrandName = [car usingEngineBrandName]; //直接獲取到了引擎的品牌名稱
    return engineBrandName;
}

與之前的設計不同,在客戶端里面,沒有引入GasEngine類,而是直接通過Car實體獲取到了需要的資料,

這樣設計的好處是,如果這輛車的引擎換成了電動引擎(原來的GasEngine類換成了ElectricEngine類),客戶端代碼可以不做任何修改!因為它沒有引入任何引擎類,而是直接獲取了引擎的品牌名稱,

所以在這種情況下我們只需要修改Car類的usingEngineBrandName方法實作,將新引擎的品牌名稱回傳即可,

下面來看一下這兩個設計的UML 類圖,可以更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐迪米特法則:

實踐了迪米特法則:

很明顯,在實踐了迪米特法則的 UML 類圖里面,沒有了ClientGasEngine的依賴,耦合性降低,

如何實踐

今后在做物件與物件之間互動的設計時,應該極力避免引出中間物件的情況(需要匯入其他物件的類):需要什么物件直接回傳即可,降低類之間的耦合度,

原則六:里氏替換原則(Liskov Substitution Principle)

定義

In a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)

即:所有參考基類的地方必須能透明地使用其子類的物件,也就是說子類物件可以替換其父類物件,而程式執行效果不變,

定義的解讀

在繼承體系中,子類中可以增加自己特有的方法,也可以實作父類的抽象方法,但是不能重寫父類的非抽象方法,否則該繼承關系就不是一個正確的繼承關系,

優點

可以檢驗繼承使用的正確性,約束繼承在使用上的泛濫,

代碼講解

在這里用一個簡單的長方形與正方形的例子講解一下里氏替換原則,

需求點

創建兩個類:長方形和正方形,都可以設定寬高(邊長),也可以輸出面積大小,

不好的設計

首先宣告一個長方形類,然后讓正方形類繼承于長方形,

長方形類:

//================== Rectangle.h ==================

@interface Rectangle : NSObject
{
@protected double _width;
@protected double _height;
}

//設定寬高
- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

//獲取寬高
- (double)width;
- (double)height;

//獲取面積
- (double)getArea;

@end

//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}

- (double)getArea{
    return _width * _height;
}

@end

正方形類:

//================== Square.h ==================

@interface Square : Rectangle
@end

//================== Square.m ==================

@implementation Square

- (void)setWidth:(double)width{

    _width = width;
    _height = width;
}

- (void)setHeight:(double)height{

    _width = height;
    _height = height;
}

@end

可以看到,正方形類繼承了長方形類以后,為了保證邊長永遠是相等的,特意在兩個set方法里面強制將寬和高都設定為傳入的值,也就是重寫了父類Rectangle的兩個set方法,但是里氏替換原則里規定,子類不能重寫父類的方法,所以上面的設計是違反該原則的,

而且里氏替換原則原則里面所屬:子類物件能夠替換父類物件,而程式執行效果不變,我們通過一個例子來看一下上面的設計是否符合:

在客戶端類寫一個方法:傳入一個Rectangle型別并回傳它的面積:

- (double)calculateAreaOfRect:(Rectangle *)rect{
    return rect.getArea;
}

我們先用Rectangle物件試一下:

Rectangle *rect = [[Rectangle alloc] init];
rect.width = 10;
rect.height = 20;

double rectArea = [self calculateAreaOfRect:rect];//output:200

長寬分別設定為10,20以后,結果輸出200,沒有問題,

現在我們使用Rectange的子類Square的物件替換原來的Rectange物件,看一下結果如何:

Square *square = [[Square alloc] init];
square.width = 10;
square.height = 20;

double squareArea = [self calculateAreaOfRect:square];//output:400

結果輸出為400,結果不一致,再次說明了上述設計不符合里氏替換原則,因為子類的物件square替換父類的物件rect以后,程式執行的結果變了,

不符合里氏替換原則就說明該繼承關系不是正確的繼承關系,也就是說正方形類不能繼承于長方形類,程式需要重新設計,

我們現在看一下比較好的設計,

較好的設計

既然正方形不能繼承于長方形,那么是否可以讓二者都繼承于其他的父類呢?答案是可以的,

既然要繼承于其他的父類,它們這個父類肯定具備這兩種形狀共同的特點:有4個邊,那么我們就定義一個四邊形的類:Quadrangle

//================== Quadrangle.h ==================

@interface Quadrangle : NSObject
{
@protected double _width;
@protected double _height;
}

- (void)setWidth:(double)width;
- (void)setHeight:(double)height;

- (double)width;
- (double)height;

- (double)getArea;
@end

接著,讓Rectangle類和Square類繼承于它:

Rectangle類:

//================== Rectangle.h ==================

#import "Quadrangle.h"

@interface Rectangle : Quadrangle

@end

//================== Rectangle.m ==================

@implementation Rectangle

- (void)setWidth:(double)width{
    _width = width;
}

- (void)setHeight:(double)height{
    _height = height;
}

- (double)width{
    return _width;
}

- (double)height{
    return _height;
}

- (double)getArea{
    return _width * _height;
}

@end

Square類:

//================== Square.h ==================

@interface Square : Quadrangle
{
    @protected double _sideLength;
}

-(void)setSideLength:(double)sideLength;

-(double)sideLength;

@end

//================== Square.m ==================

@implementation Square

-(void)setSideLength:(double)sideLength{    
    _sideLength = sideLength;
}

-(double)sideLength{
    return _sideLength;
}

- (void)setWidth:(double)width{
    _sideLength = width;
}

- (void)setHeight:(double)height{
    _sideLength = height;
}

- (double)width{
    return _sideLength;
}

- (double)height{
    return _sideLength;
}

- (double)getArea{
    return _sideLength * _sideLength;
}

@end

我們可以看到,RectangeSquare類都以自己的方式實作了父類Quadrangle的公共方法,而且由于Square的特殊性,它也宣告了自己獨有的成員變數_sideLength以及其對應的公共方法,

注意,這里RectangeSquare并不是重寫了其父類的公共方法,而是實作了其抽象方法,

下面來看一下這兩個設計的UML 類圖,可以更形象地看出兩種設計上的區別:

UML 類圖對比

未實踐里氏替換原則:

實踐了里氏替換原則:

如何實踐

里氏替換原則是對繼承關系的一種檢驗:檢驗是否真正符合繼承關系,以避免繼承的濫用,因此,在使用繼承之前,需要反復思考和確認該繼承關系是否正確,或者當前的繼承體系是否還可以支持后續的需求變更,如果無法支持,則需要及時重構,采用更好的方式來設計程式,

最后的話

到這里關于六大設計原則的講解已經結束了,本篇文章所展示的Demo和UML 類圖都在筆者維護的一個專門的GitHub庫中:object-oriented-design,想看Demo和UML圖的同學可以點擊鏈接查看,歡迎fork,更歡迎給出更多語言的不同例子的PR~ 而且后續還會添加關于設計模式的 代碼和 UML 類圖,

關于這幾個設計原則還有最后一點需要強調的是:
設計原則是設計模式的基石,但是很難在使實際開發中的某個設計中全部都滿足這些設計原則,因此我們需要抓住具體設計場景的特殊性,有選擇地遵循最合適的設計原則,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/286142.html

標籤:iOS

上一篇:iOS開發面試時,常問的72個問題串列

下一篇:iOS面試--虎牙最新iOS開發面試題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more