[Android] Clean Architecture là gì? Ưu nhược điểm và cách ứng dụng trong lập trình mobile

Đăng bởi: Admin | Lượt xem: 4195 | Chuyên mục: Android

Tôi đã tham gia nhiều dự án phát triển phần mềm, từ những app nhỏ cho đến hệ thống lớn. Có nhiều kiến trúc đã được áp dụng, thô sơ nhất là mô hình God Activity (Một activity làm tất cả), rồi MVC (Model View Controller), MVP (Model View Presenter), … Biến đổi theo các mô hình, độ phức tạp của code, khả năng test, tính phân tách chức năng sẽ tăng dần.


Các mô hình kể trên đều có những ưu và khuyết điểm riêng, nhưng chúng vẫn gặp phải một vấn đề. Đó là sự phụ thuộc vào framework (Android) trong business logic. Lấy ví dụ mô hình MVP, presenter là nơi sẽ implement các function của hệ thống. Tại đó chúng ta vẫn thấy sự phụ thuộc vào các thành phần của Android Framework. Điều này dẫn đến sự khó khăn trong việc viết unit test, và khả năng chuyển đổi sang nền tảng mới. Tôi đã đọc và tìm được một mô hình kiến trúc giải quyết vấn đề này. Đó chính là Clean Architecture, sẽ được trình bày ở bài viết dưới đây.

1. Clean Architecture là gì?

Clean Architecture được xây dựng dựa trên tư tưởng "độc lập" kết hợp với các nguyên lý thiết kế hướng đối tượng(đại diện tiêu biểu là Dependency Inversion). Độc lập ở đây nghĩa là việc project không bị phụ thuộc vào framework và các công cụ sử dụng trong quá trình kiểm thử.

Kiến trúc của Clean Architecture chia thành 4 layer với một quy tắc phụ thuộc. Các layer bên trong không nên biết bất kỳ điều gì về các layer bên ngoài. Điều này có nghĩa là nó có quan hệ phụ thuộc nên "hướng" vào bên trong. Nhìn vào hình vẽ minh họa sau đây:

Entities: là khái niệm dùng để mô tả các Business Logic. Đây là layer quan trọng nhất, là nơi bạn thực hiện giải quyết các vấn đề - mục đích khi xây dựng app. Layer này không chứa bất kỳ một framework nào, bạn có thể chạy nó mà không cần emulator. Nó giúp bạn dễ dàng test, maintain và develop phần business logic.

Use case : chứa các rule liên quan trực tiếp tới ứng dụng cục bộ (application-specific business rules).

Interface Adapter : tập hợp các adapter phục vụ quá trình tương tác với các công nghệ.

Framework and Drivers : chứa các công cụ về cơ sở dữ liệu và các framework, thông thường bạn sẽ không phải lập trình nhiều ở tầng này. Tuy nhiên cần chắc chắn về mức ưu tiên sử dụng các công cụ này trong project.

Thông thường thì một ứng dụng của bạn có thể có tùy ý số lượng các layer. Thường thì một ứng dụng Android sẽ có 3 layer:

  • Outer: Implementation layer: là nơi mà tất cả mọi thứ của framwork xảy ra, điều này bao gồm tất cả các công cụ Android như là tạo các activity, các fragment, gửi intent, networking và databases.

  • Middle: Interface adapter layer: là hoạt động như một kết nối giữa business logic và framework specific code.

  • Inner: Business logic layer: tương tự như trên.

Đối với mỗi layer ở trên core layer đều có trách nhiệm chuyển đổi các model thành các model layer thấp hơn trước khi các layer thấp hơn có thể sử dụng đến chúng. Tại sao việc chuyển đổi model là cần thiết? Ví dụ, các business logic model của bạn có thể không thích hợp cho việc hiển thị chúng đối với người dùng cuối, bạn có thể sẽ phải kết hợp nhiều nhiều business logic model cùng một lúc. Vì vậy, bạn nên tạo một lớp ViewModel để có thể dễ dàng hiển thị UI. Sau đó, hãy sử dụng một lớp converter ở outer layer để chuyển đổi các business model của bạn sao cho thích hợp với ViewModel. Tóm lại là chuyển đổi Model để phù hợp với chức năng của từng layer!

2. Lợi ích của Clean Architecture

Theo như mình được biết thì Clean Architecture mang lại những lợi ích sau:

  • Mạch lạc - dễ xem (bản gốc ghi screaming với dụng ý là chỉ cần nhìn cấu trúc package cũng có thể hiểu được mục đích và cơ chế hoạt động của ứng dụng)

  • Linh hoạt - thể hiện ở khả năng độc lập, không phụ thuộc vào framework, database, application server.

  • Dễ kiểm thử - testable

3. Hạn chế của Clean Architecture

Bên cạnh những lợi ích trên thì Clean Architecture còn những hạn chế sau:

  • Không thể sử dụng framework theo cách mỳ ăn liền- do luật dependency inversion.

  • Khó áp dụng

  • Indirect - quá nhiều interface?

  • Cồng kềnh - thể hiện ở việc có quá nhiều class so với các project cùng mục tiêu (tuy nhiên các class được thêm vào đều có chủ ý và đáp ứng đúng quy định khi triển khai kiến trúc)

Nếu suy luận từ những hạn chế của Clean Architecture, chúng ta rút ra được một số điểm cần chú ý khi áp dụng Clean Architecture:

  • Trình độ của team? Khi khả năng team member có hạn và họ không thích kiểu kiến trúc này (có thể do thói quen hoặc do "lười" đổi mới) thì dĩ nhiên áp đặt lên đầu họ các tư tưởng ở mục #1 chỉ kéo hiệu suất của team đi xuống.

  • Cần duy trì và nâng cấp hệ thống kể cả khi người làm ra nó đã rời khỏi công ty?

  • Cần duy trì hệ thống ổn định, không phụ thuộc vào sự sinh-tử của framework?

4. Ví dụ về Clean Architecture

Tôi đã viết một project đơn giản để minh họa. Các bạn có thể tìm thấy ở đây.

Cấu trúc

Cấu trúc phổ biến cho một Android app sẽ như sau:

  • Outer layer packages: UI, Storage, Network, etc.

  • Middle layer packages: Presenters, Converters

  • Inner layer packages: Interactors, Models, Repositories, Executor

Outer layer : nơi Framework hoạt động

Middle layer : kết nối thực hiện business logic của bạn.

Presenters - xử lý các event từ UI (như là click, touch) và phục vụ các callback từ inner layer.

Converters - Các Converters object có trách nhiệm chuyển đổi inner models thành outer models và ngược lại.

Inner layer : chứa code ở mức cao nhất. Đây là lớp thực hiện business logic của hệ thống. Các thành phần của lớp này có thể chạy mà không cần đến Activity, Fragment,…  Phần ví dụ minh họa dưới đây inner layer sẽ gồm 2 package entities và use case.

Kịch bản yêu cầu

Hệ thống bán hàng qua nền tảng Android. Bạn cần viết code để handle use case: Người dùng muốn lấy danh sách những hàng còn trong kho.  Hệ thống kiểm tra trong cơ sở dữ liệu và trả về danh sách kết quả. (Có thông báo lỗi kết nối nếu có)

Tôi sẽ xây dựng 4 package :

entities : Chứa 1 entity

usecase: Xử lý logic của use case

presentation: Sử dụng mô hình MVP để liên kết view và logic của use case

storage: Code truy vấn dữ liệu

Inner layer

Xây dựng Entities

public class ShopItem {
    String mItemId;
    String mItemName;
    String mItemCategory;
    double mCost;
    boolean mIsAvailable;

    public ShopItem(String id, String name, String category, double cost) {
        this.mItemId = id;
        this.mItemName = name;
        this.mItemCategory = category;
        this.mCost = cost;
    }
}

Class ShopItem có thể được sử dụng ở các lớp ngoài.

Xây dựng usecases

Interactor : những class thực sự chứa business logic của bạn. Chúng được chạy ở background và giao tiếp event với layer ngoài sử dụng callbacks.  Trong ví dụ này chúng ta chỉ cần một interactor GetAvailableItemInteractor.

public interface GetAvailableItemInteractor {
    interface Callback {
        void onLoadListItemSuccess(ArrayList<ShopItem> shopItems);
        void onLoadListItemFail(String failMessage);
    }

    void run();
    void notifyWhenLoadListItemFail();
    void notifyWhenLoadListItemSuccessful(ArrayList<ShopItem> shopItems);
}

Đây thực chất là một Interface . Theo tôi để có thể hiểu rõ về Clean Architecture, chúng ta cần nắm rõ bản chất của Interface trong Java . Trong interface này, Callback  được định nghĩa để thực hiện trả về kết quả sau khi công việc truy vấn DB hoàn thành. Callback chịu trách nhiệm giao tiếp với UI tại main thread. Phương thức run là phương thức thực hiện truy vấn DB và xử lý logic, notifyWhenLoadListItemFail và notifyWhenLoadListItemSuccessful là 2 phương thức tương ứng việc thông báo lấy dữ liệu lỗi và thành công. Cả 3 phương thức này sẽ đc implement ở class GetAvailableItemInteractorImpl.

public class GetAvailableItemInteractorImpl implements GetAvailableItemInteractor {
    MainThread mMainThread;
    ShopRepository mShopRepository;
    Callback mCallBack;

    public GetAvailableItemInteractorImpl(MainThread mainThread, Callback callback, ShopRepository shopRepository) {
        mMainThread = mainThread;
        mCallBack = callback;
        mShopRepository = shopRepository;
    }

    @Override
    public void run() {
        mMainThread.post(new Runnable() {

            @Override
            public void run() {
                if (mShopRepository.isConnectionSuccessful()) {
                    ArrayList<ShopItem> shopItems = mShopRepository.getAllAvailableItems();
                    notifyWhenLoadListItemSuccessful(shopItems);
                } else {
                    notifyWhenLoadListItemFail();
                }
            }
        });
    }

    @Override
    public void notifyWhenLoadListItemFail() {
        mMainThread.post(new Runnable() {

            @Override
            public void run() {
                mCallBack.onLoadListItemFail("Connection fail");
            }
        });
    }

    @Override
    public void notifyWhenLoadListItemSuccessful(final ArrayList<ShopItem> shopItems) {
        mMainThread.post(new Runnable() {

            @Override
            public void run() {
                mCallBack.onLoadListItemSuccess(shopItems);
            }
        });
    }
}

Chúng ta cần một repository để truy vấn DB, nhưng vấn đề ở đây là lớp use case không được sử dụng các thành phần của Framework, hoặc DB (Ví dụ : Content Provider). Vì vậy repository này chỉ là một interface và được implement bởi class của lớp bên ngoài

public interface ShopRepository {
    boolean isConnectionSuccessful();
    ArrayList<ShopItem> getAllAvailableItems();
}

Class MainThread “đóng giả” một handler của Android để thực hiện truy vấn DB hoặc giao tiếp với UI. Vì trong lớp này không thể dùng thành phần Android, nên MainThread cũng chỉ là interface

public interface MainThread {
    void  post(Runnable runnable);
}

Tôi đã xem một hệ thống thực tế viết theo Clean Architecture, và thấy rằng phần quản lý Thread trong core domain được viết khá phức tạp. Vì giới hạn bài viết và điều đó chưa thực sự quan trọng đối với các bạn lúc nà, nên tôi chỉ dùng một class MainThread đơn giản.

Trong kiến trúc Clean, lớp bên trong sẽ có nhiều thành phần “đóng giả” thành phần của lớp ngoài như thế.

Middle layer và Outer layer

Hai lớp này được triển khai trong 2 package presentation và storage.

Tôi không nói từ tương ứng vì một phần của package presentation sẽ thuộc lớp ngoài cùng, lớp liên quan đến Android Framework. Cụ thể :

ShopItemActivityShopItemActivityTheadAndroidDBRepository cần sử dụng Framework Android

ShopItemContractShopItemPresenter thuộc lớp presenter.

Sở dĩ có sự pha trộn trong package presentation là vì tôi sử dụng mô hình MVP để liên kết core logic và view.

Viết một contract xác định mối liên kết giữa Presenter và View

import com.example.vilastudio.cleanarchitect.entities.ShopItem;
import com.example.vilastudio.cleanarchitect.usecases.MainThread;
import com.example.vilastudio.cleanarchitect.usecases.ShopRepository;

import java.util.ArrayList;

/**
 * Created by Nguyen Van Vinh on 6/23/2018.
 */

public class ShopItemContract {
    interface Presenter {
        void attachView(View view);
        void detachView(View view);
        void attachRepository(ShopRepository shopRepository);
        void attachThread(MainThread mainThread);
        void getAllAvailableItem();
    }
    interface View {
        void updateListAvailableItem(ArrayList<ShopItem> shopItems);
        void notifyWhenConnectFail(String failMessage);
    }
}

Và lần lượt Presenter,  View được implement tương ứng.

import com.example.vilastudio.cleanarchitect.entities.ShopItem;
import com.example.vilastudio.cleanarchitect.usecases.GetAvailableItemInteractor;
import com.example.vilastudio.cleanarchitect.usecases.GetAvailableItemInteractorImpl;
import com.example.vilastudio.cleanarchitect.usecases.MainThread;
import com.example.vilastudio.cleanarchitect.usecases.ShopRepository;

import java.util.ArrayList;

/**
 * Created by Nguyen Van Vinh on 6/23/2018.
 */

public class ShopItemPresenter implements ShopItemContract.Presenter, GetAvailableItemInteractor.Callback {
    ShopItemContract.View mView;
    ShopRepository mShopRepository;
    MainThread mMainThread;
    GetAvailableItemInteractorImpl mGetAvailableItemInteractor;
    @Override
    public void attachView(ShopItemContract.View view) {
        mView = view;
    }

    @Override
    public void detachView(ShopItemContract.View view) {
    }

    @Override
    public void attachRepository(ShopRepository shopRepository) {
        mShopRepository = shopRepository;
    }

    @Override
    public void attachThread(MainThread mainThread) {
        mMainThread = mainThread;
    }

    @Override
    public void getAllAvailableItem() {
        mGetAvailableItemInteractor = new GetAvailableItemInteractorImpl(mMainThread,this,mShopRepository);
        mGetAvailableItemInteractor.run();
    }

    @Override
    public void onLoadListItemSuccess(ArrayList<ShopItem> shopItems) {
        mView.updateListAvailableItem(shopItems);
    }

    @Override
    public void onLoadListItemFail(String failMessage) {
        mView.notifyWhenConnectFail(failMessage);
    }
}

Trong MVP, View sẽ tương ứng Activity, Fragment, Dialog

public class ShopItemActivity extends AppCompatActivity implements ShopItemContract.View{
    Context mContext;
    Button mBtGetAll;
    ShopItemPresenter mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
        setContentView(R.layout.test_activity);
        mBtGetAll = (Button)findViewById(R.id.test_bt_get);
        mBtGetAll.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.getAllAvailableItem();
            }
        });
        initPresenter();
    }

    public void initPresenter() {
        mPresenter = new ShopItemPresenter();
        mPresenter.attachView(this);
        mPresenter.attachRepository(new AndroidDBRepository(mContext));
        mPresenter.attachThread(new ShopItemActivityThread());
    }

    @Override
    public void updateListAvailableItem(ArrayList<ShopItem> shopItems) {
        //Do update list
        if (shopItems != null) {
            Toast.makeText(mContext,"Da load "+shopItems.size(),Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(mContext,"Khong co item nao ",Toast.LENGTH_SHORT).show();
        }

    }

    @Override
    public void notifyWhenConnectFail(String message) {
        Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
    }
}

 “Diễn viên đóng thế” MainThead của lớp bên trong được implement tại đây

public class ShopItemActivityThread implements MainThread {
    Handler mHandler;
    public ShopItemActivityThread() {
        mHandler = new Handler();
    }
    @Override
    public void post(Runnable runnable) {
        mHandler.postDelayed(runnable,100);
    }
}

mHandler là một thành phần của Android, vì thế nó chỉ có thể được implement ở lớp ngoài cùng.

Chúng ta quay lại hàm thực thi chính của presenter

    @Override
    public void getAllAvailableItem() {
        mGetAvailableItemInteractor = new GetAvailableItemInteractorImpl(mMainThread,this,mShopRepository);
        mGetAvailableItemInteractor.run();
    }

this là callback để thực hiện update UI. Vì Presenter implement Callback của Interator nên có thể dùng this ở đây.

mGetAvailableItemInteractor bản chất giống AsyncTask trong Android nhưng chúng ta vẫn cần phải cài đặt lại trong lớp use case. Lý do rất quen thuộc trong Clean Architecture : AsyncTask là một thành phần của Android.

Truy vấn dữ liệu thực sự được implement tại class  AndroidDBRepository. DB thật có thể dùng ContentProvider, Retrofit, SharePreferences,… Còn với ví dụ này nó chỉ là một lớp đơn giản

public class AndroidDBRepository implements ShopRepository {
    Context mContext;

    public AndroidDBRepository(Context context) {
        mContext = context;
    }
    @Override
    public boolean isConnectionSuccessful() {
        return true;
    }

    @Override
    public ArrayList<ShopItem> getAllAvailableItems() {
        try {
            Thread.sleep(3000); // Minh hoa delay cua qua trinh lay du lieu tư DB
            ArrayList<ShopItem> shopItems = new ArrayList<>();
            for (int i=0;i<10;i++) {
                shopItems.add(new ShopItem(i+"",i+"",i+"",(i+1)*1000));
            }
            return shopItems;


        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

5. Kết luận

Theo tôi Clean Architecture là phương pháp tốt nhất hiện nay. Tách riêng code là một cách để dễ dàng tập trung vào logic chính. Điểm cơ bản nhất của kiến trúc này các bạn cần nhớ là đẩy những thứ phụ thuộc vào framework càng xa càng tốt, và quy tắc phụ thuộc hướng vào phía trong. Chắc các bạn đang tự hỏi, hệ thống thật sẽ có rất nhiều use case, cấu trúc code sẽ như thế nào ? Có nhiều cách để giải quyết vấn đề này, nhưng cách hay làm nhất (và tôi đã từng gặp) là mỗi use case tương ứng một interactor, và có quy cách đặt tên riêng. Ví dụ : LoginInteractor, LogoutInteractor, BuyItemInteractor,…  Phần quản lý background sẽ hầu như không thay đổi đối với các interactor.

vncoder logo

Theo dõi VnCoder trên Facebook, để cập nhật những bài viết, tin tức và khoá học mới nhất!



Khóa học liên quan

Khóa học: Android

Học Kotlin cơ bản
Số bài học:
Lượt xem: 17058
Đăng bởi: Admin
Chuyên mục: Android

Học lập trình Flutter cơ bản
Số bài học:
Lượt xem: 55037
Đăng bởi: Admin
Chuyên mục: Android

Lập trình Android cơ bản
Số bài học:
Lượt xem: 22170
Đăng bởi: Admin
Chuyên mục: Android