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

Related Articles

Đăng bởi : Admin | Lượt xem : 2214 | 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 thiết kế xây dựng dựa trên tư tưởng ” độc lập ” phối hợp với những nguyên tắc phong cách thiết kế hướng đối tượng người dùng ( đại diện thay mặt tiêu biểu vượt trội là Dependency Inversion ). Độc lập ở đây nghĩa là việc project không bị phụ thuộc vào vào framework và những công cụ sử dụng trong quy 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 vào. Các layer bên trong không nên biết bất kể điều gì về những layer bên ngoài. Điều này có nghĩa là nó có quan hệ nhờ vào 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 hoàn toàn có thể có tùy ý số lượng những 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ó nghĩa vụ và trách nhiệm quy đổi những Mã Sản Phẩm thành những Mã Sản Phẩm layer thấp hơn trước khi những layer thấp hơn hoàn toàn có thể sử dụng đến chúng. Tại sao việc quy đổi Mã Sản Phẩm là thiết yếu ? Ví dụ, những business logic Mã Sản Phẩm của bạn hoàn toàn có thể không thích hợp cho việc hiển thị chúng so với người dùng cuối, bạn hoàn toàn có thể sẽ phải tích 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 để hoàn toàn có thể thuận tiện hiển thị UI. Sau đó, hãy sử dụng một lớp converter ở outer layer để quy đổi những business Model của bạn sao cho thích hợp với ViewModel. Tóm lại là quy đổi Model để tương thích với công dụ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 quyền lợi 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 hoàn toàn có thể hiểu được mục tiêu và chính sách hoạt động giải trí của ứng dụng )
  • Linh hoạt – biểu lộ ở năng lực độc lập, không phụ thuộc vào 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 quyền lợi 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ó vận dụng
  • Indirect – quá nhiều interface ?
  • Cồng kềnh – biểu lộ ở việc có quá nhiều class so với những project cùng tiềm năng ( tuy nhiên những class được thêm vào đều có chủ ý và phân phối đúng pháp luật khi tiến hành kiến trúc )

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

  • Trình độ của team ? Khi năng lực team thành viên hạn chế và họ không thích kiểu kiến trúc này ( hoàn toàn có thể do thói quen hoặc do ” lười ” thay đổi ) thì đương nhiên áp đặt lên đầu họ những tư tưởng ở mục # 1 chỉ kéo hiệu suất của team đi xuống .
  • Cần duy trì và tăng cấp mạng lưới hệ thống kể cả khi người làm ra nó đã rời khỏi công ty ?
  • Cần duy trì mạng lưới hệ thống không thay đổi, không phụ thuộc vào vào sự sinh-tử của framework ?

4. Ví dụ về Clean Architecture

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

Cấu trúc

Cấu trúc phổ cập 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 – giải quyết và xử lý những sự kiện từ UI ( như thể click, touch ) và Giao hàng những 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 list những hàng còn trong kho. Hệ thống kiểm tra trong cơ sở tài liệu và trả về list hiệu quả. ( Có thông tin lỗi liên kết nếu có )

Tôi sẽ kiến thiết 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 hoàn toàn có thể được sử dụng ở những 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 shopItems);
        void onLoadListItemFail(String failMessage);
    }
    void run();
    void notifyWhenLoadListItemFail();
    void notifyWhenLoadListItemSuccessful(ArrayList 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 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 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 yếu tố ở đây là lớp use case không được sử dụng những 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 getAllAvailableItems();
}

Class MainThread “ đóng giả ” một handler của Android để triển khai truy vấn DB hoặc tiếp xúc với UI. Vì trong lớp này không hề 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 mạng lưới hệ thống thực tiễn viết theo Clean Architecture, và thấy rằng phần quản trị Thread trong core domain được viết khá phức tạp. Vì số lượng giới hạn bài viết và điều đó chưa thực sự quan trọng so với những bạn lúc nà, nên tôi chỉ dùng một class MainThread đơn thuầ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ư vậy .

Middle layer và Outer layer

Hai lớp này được tiến hành 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 tương 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 lập mối link 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 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 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 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 tài liệu thực sự được implement tại class AndroidDBRepository. DB thật hoàn toàn có thể dùng ContentProvider, Retrofit, SharePreferences, … Còn với ví dụ này nó chỉ là một lớp đơn thuần

public class AndroidDBRepository implements ShopRepository {
    Context mContext;
    public AndroidDBRepository(Context context) {
        mContext = context;
    }
    @Override
    public boolean isConnectionSuccessful() {
        return true;
    }
    @Override
    public ArrayList getAllAvailableItems() {
        try {
            Thread.sleep(3000); // Minh hoa delay cua qua trinh lay du lieu tư DB
            ArrayList shopItems = new ArrayList();
            for (int i=0;i

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.

More on this topic

Comments

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Advertismentspot_img

Popular stories