移動架構這么多,如何一次搞定所有

我是一個Android猴, 主要從Android端來談一下對各種結構的看法,總結一下基礎架構的核心是什么?因此,這篇文章對于對架構一詞不是很了解,或者一知半解的朋友來講, 梳理一下大局觀;并且我覺得同樣的作為移動開發,IOS與Android的架構差異化并不是很大。

從移動端談架構,其實有點夸大了。因為移動端的項目往往不是很大,或者模塊不是很大。一般架構這個詞,可能用在Web端比較好一點,也更有效點,架構好了,意味這更穩健的運行效率, 更大體量。從移動端來談架構,無非是讓代碼可以優雅一點,解決一下常見的耦合等問題。

從Android誕生至今,移動端的架構變更了很多次,從最初的MVC到MVP, 從冷門的Flutter(由RN引入到移動端)到Google的AAC/MVVM;好像架構的思想一直在變,但是大抵都是換湯不換藥的,為什么這么說呢 ? 讓我們來總結一下。

MVC
MVP
MVVM
Flutter
AAC

以上的架構中MVX系列先不說,剩下的兩個是什么? 先來解釋一下。

Flutter
此Flutter非目前炒得火熱的Flutter, 而是由React Native衍生而來的,適用于移動端的框架。是的,這也是一種框架思想。Flutter的元素分為3種: View(不必多說), Model(也不必解釋吧), Store(這個要說一下,用于處理Action的核心類,類似Presenter的作用), Dispatcher(Action路由), Action(事件)。該框架類似于MVP, 只是通信模塊由接口,改為路由系統。

AAC([Android Architecture components]
不知道的可以查看一下官網:https://developer.android.com/topic/libraries/architecture/guide.html

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileFragment extends Fragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }
}

以上是來自官網的AAC-Demo的源碼,看結構其實就清楚了,如果允許自己命名的話,或者我們可以稱為MVVM了;ViewModel這個模塊,是不是像極了Controller或者Presenter。


通過以上框架的分析, 我們應該可以得出結論:

局部的架構,為什么說是局部架構?因為項目級得架構肯定就是大結構的組件化與插件化。

1.分層

從代碼的實現解耦。對于現在狹義上的架構,M和V是必然單獨的兩層,因為數據處理和UI嘛,界限很清楚。難以劃分層次的就是邏輯實現,也就是我們的業務處理。而Controller, Presenter, VM這些模塊的功能都是一致的

所以分層的維度幾乎已經確定,就是 數據處理(Model), UI顯示(View), 業務處理(X)

2.通信

不同的模塊,好像層次劃分都是一致的,雖然騷氣的取了不同的名字。但是區別就在于通信方式。

MVC/AAC的通信方式是對象, View與Model的交互是完全通過對象來實現的,如下

public class Model {

    /**
     * 這是Model模塊,負責數據處理。處理的方式不盡相同,但是本質一樣。如網絡請求, 數據庫, 文件等等
     */

    public void postLogin(String username, String password, Callback callback) {
        // 執行登陸請求,驗證帳號密碼是否正確
        callback.onResponse("登錄結果");
    }

    public interface Callback {
        void onResponse(String result);
    }
}
public class View {

    X controllerOrPresenter;

    public void onClick() {
        // 點擊登錄
        controllerOrPresenter.login("xxx", "xxx", new Model.Callback() {
            @Override
            public void onResponse(String result) {
                if ("success".equals(result)) {
                    // 登錄成功,則提示登錄成功,保存信息
                } else {
                    // 登錄失敗,則提示失敗
                }
            }
        });
    }
}
public class X {

    Model model;
    View view;

    public void login(String username, String password, Model.Callback callback) {
        // 驗證帳號, 密碼格式是否正確
        model.postLogin(username, password, callback);
    }

}

以上代碼是MVC模式, 是不是這么處理的?通過持有對象來達到模塊之間的通信。當時可能會覺得這樣耦合度比較高,這樣就出現了解決方案。于是MVC做成了MVP(哦,當然,那時候還不這么叫),通過接口的方式來通信,或許接口化沒那么徹底而已。AAC通過綁定頁面的周期做到隨頁面釋放并包裝


MVP的通信方式是完全接口式通信, V和P之間,甚至M和P之間也可以

MVP的方式來解釋一下,V和P分別實現自己的接口, IV和IP。然后分別傳如自己的接口達到調用目的。代碼如下:

public interface IP {
    void login(String username, String password);
}
public interface IV {

    void loginSuccess();

    void loginFailed();
}
public class View implements IV{

    X iP;

    public void onClick() {
        // 點擊登錄
        iP.login("xxx", "xxx");
    }

    @Override
    public void loginSuccess() {
        // 保存信息
        // 提示登錄成功了
    }

    @Override
    public void loginFailed() {
        // 提示登錄失敗
    }
}
public class X implements IP{

    /**
     * X 相當于MVP中的Presenter
     */

    Model model;
    IV iV;

    public void login(String username, String password) {
        // 驗證帳號, 密碼格式是否正確
        model.postLogin(username, password, new Model.Callback() {
            @Override
            public void onResponse(String result) {
                if("success".equals(result)) {
                    // 登錄成功
                    iV.loginSuccess();
                }else {
                    // 登錄失敗
                    iV.loginFailed();
                }
            }
        });
    }

}

以上是MVP的通信方式,完全的接口相互調用。可能一些架構思維比較前衛的公司,在前期就把MVC改造成了如今的MVP,還是那句話,只是那時候不那么叫罷了。


Flutter是以路由機制來實現解耦通信

在現在組件化風行的時代,相信各位對路由沒那么陌生了。就算不了解, 那么路由器總知道吧,那么先說路由器。

路由或路由器,分兩個模式: 接收信號, 發出信號。分別是多對一和一對多的關系。

舉個例子, 現實中的路由器,接收只有一個入口,但發出口有很多個,畢竟如果只有一個出口,那么路由器就沒用了。在路由機制中,入口也可以有多個。所以就是上面說的,接收是多對一(一當然是路由器), 發出是一對多(一還是路由器)。

簡單由一個圖來說明一下路由機制,圖不是很規整,明白就好。

image.png

在Flutter模式下,我們如何對應呢?

M/V/P或C不同模塊之間不能有耦合,即不持有對象,且不持有接口,完全解耦。那么各模塊怎么通信呢?通過向路由器發信號。所以M V P/C 都是上圖的信號源

M/V/P或C 不同模塊要交互,那么怎么得到信號呢? 這時候他們的角色就轉變了,不僅可以發信號,也可以接口信號,來做對應處理。 所以M V P/C 也同樣是上圖的手機(信號接口器)

以上的解釋, 不同模塊既可以發出信號,也可以接收信號。和Android中的一個組件很相似,就是Handler. Handler既可以發出消息,同時消息又在里面處理。

既然有發送信號的, 有接收信號的,那么必然有一個路由器負責接收與發送,在Flutter中就是Dispatcher。Dispatcher保存了不同信號與接收器的對應關系, 以此完成消息的分發

上面都提到了消息,現在正式的介紹一下,消息是角色是Action

看一下Flutter的元素

Model
View                                       
Store(Controller或Presenter)   處理業務邏輯
Action 消息對象,帶有動作與數據
Dispathcer 路由,管理消息與發送消息,保存有一張對應表

接下來通過代碼了解一下

public class Model {

    public void postLogin(User user, Callback callback) {
        // 請求登錄
        callback.onResponse(1);
    }

    public interface Callback {
        void onResponse(int result);
    }
}
public class User {
    public String username;
    public String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
}
public class View implements Dispatcher.IReceiver {


    public void onCreate() {
        Dispatcher.getDispatcher().register(this);
    }

    public void onDestroy() {
        Dispatcher.getDispatcher().unregister(this);
    }

    void onClick() {
        Dispatcher.getDispatcher()
                .sendEvent(
                        new Action("login", new User("xxx", "xxx")));
    }

    @Override
    public void onReceive(Action action) {
        if (action.name.equals("login-success")) {
            // 登錄成功提示
        } else {
            // 登錄失敗提示
        }
    }
}
public class X implements Dispatcher.IReceiver{

    Model model;

    public X() {
        model = new Model();
        Dispatcher.getDispatcher().register(this);
    }

    public void clearX() {
        model = null;
        Dispatcher.getDispatcher().unregister(this);
    }

    @Override
    public void onReceive(Action action) {
        if(action.name.equals("login")) {
            model.postLogin((User) action.data, new Model.Callback() {
                @Override
                public void onResponse(int result) {
                    if(result == 1) {
                        // 登錄成功
                        Dispatcher.getDispatcher().sendEvent(new Action("login-success", null));
                    }else {
                        // 登錄失敗
                        Dispatcher.getDispatcher().sendEvent(new Action("login-failed", null));
                    }
                }
            });
        }
    }
}
public class Action<T> {
    public String name; // 執行動作,比如“登錄”
    public T data; // 數據,比如username, password

    public Action(String name, T data) {
        this.name = name;
        this.data = data;
    }
}
public class Dispatcher {
    private static Dispatcher dispatcher = new Dispatcher();
    private List<IReceiver> receivers = new ArrayList<>();

    private Dispatcher(){}

    public static Dispatcher getDispatcher() {
        return dispatcher;
    }

    public void register(IReceiver receiver) {
        if (receivers.contains(receiver)) {
            throw new IllegalStateException("receiver has been registerd yet!");
        } else {
            receivers.add(receiver);
        }
    }

    public void unregister(IReceiver receiver) {
        if (receivers.contains(receiver)) {
            receivers.remove(receiver);
        }
    }

    public void sendEvent(Action action) {
        if (action != null && action.name != null && action.name.length() > 0) {
            for (IReceiver r : receivers) {
                if (r != null) {
                    r.onReceive(action);
                }
            }
        }
    }

    public interface IReceiver {
        void onReceive(Action action);
    }
}

解析一下以上代碼, 首先我們說過, 所有模塊既是消息發出者,也是消息接收者。代碼中,View和X都分別發出了消息進行登錄以及登錄結果成功或失敗;同時View和X也都注冊了接收器的接口,在onReceiver中可以接收消息。這樣做的好處是什么? View和X完全沒有耦合,既不持有對象, 也不持有接口,中間的通信都是通過Dispatcher進行分發的,解耦已經很徹底了。Dispathcer就是路由器,負責接收,并分發消息,應該很好理解,哪個組件想接收消息,那么就注冊一個接收器,這樣有合適的消息自然就接收到了。

當然,以上的代碼有點簡陋,可以從不同組件再行封裝,比如Action, 比如Dispatcher, 比如BaseView等等。我們可以通過給路由注冊機制添加Group與Tag(Action name)概念來優化效率問題,這里只是說思想。


我們知道了什么?

  1. 我稱移動端的架構思維為MVX,即是說按這個規則的分工被開發市場所接受了,我們不用費盡心思考慮狹義架構的分層問題了,就沿用Model-View-X來就可以。當然還可以自己加一些輔助的模塊層,如Worker負責異步, Converter負責轉換, Verify負責校驗等

2.移動端目前的架構,差異化在于通信機制。通過以上說明,通信機制主要分為3種:
1) 對象持有
2) 接口持有
3) 路由

  1. 通信方式中,對象持有是比較原始的,解耦率最低,建議放棄; 接口持有是個不錯的選擇,極大程度上實現解耦的訴求,但是解耦不徹底,相互持有交互方的接口。 路由機制也是個不錯的選擇,可以實現完全解耦,就像組件化一樣。但是路由機制的設計是個技術難點,怎么設計效率最高?更健壯?代碼可查閱性更好?這些都是值得思考的問題。

4.對于路由機制的優化,阿里的ARouter(用于組件通信)中,采用了分組的模式,我們可以采用;其次可以根據AnnotationProcessor的處理,為每一個注冊接收器的組件實現一個SupportActions來確保消息只發送給注冊了指定類型的模塊,也是個不錯的選擇。

通過以上分析與總結,可以看到,移動端的框架核心是一定的,只要理解了架構的核心思想,其實出多少新的框架,不過是加了一些解決部分問題的實現罷了。看懂了這篇文章,可能再出什么架構模式,或許看一眼就明白是怎么實現了呢?

如果你覺得這篇文章對你有幫助,請幫我打Call, 謝謝!!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容