分類
Architectural Pattern Techniques

[架構師][技巧] 技術雷達

甚麼是技術雷達?

技術雷達是一種評估工具,用來判斷軟體領域中各種技術並對這些技術應採取什麼行動的方法。(來源為 ThoughtWorks,參閱這裡)

象限

技術雷達本身是一個切割成4等份的圓形,這4等份各為技巧(左上),工具(左下),語言框架(右上),平台(右下),代表技術種類(在官方網站中稱為象限)。

技術種類如下:

1.工具
軟體開發的各種工具,範圍很廣,包含IDE,資料庫,版控等等。

2.語言框架
大部分是開源的程式語言,函式庫,框架等等

3.技巧
可強化軟體開發的實務技巧,包含各種設計實踐(模式/原則/開發法/建議/演算法)

4.平台
可在其上建構技術的多樣平台,包含作業系統,微服務,分散式系統

要注意的是象限並不是雷達的重點,只是放置技術的分類。

圓環

在圓形中從圓心由內而外分為四層,各為採納(第1層)(最內層),試驗(第2層),評估(第3層),暫停(第4層)(最外層),代表採取行動。(在官方網站中稱為圓環)。

採取行動如下:

1.採納
已被證實成熟,應該立即使用的技術,但要注意並不代表應該在任何情況下都使用,必須經過評估。

2.試驗
可以使用的技術,但還未被充分證明。重點在知道如何使用這項技術,可以在一些無關緊要的專案實施。

3.評估
可以了解並釐清未來是否會對組織造成影響。進行Survey,開發探索,研究計畫,會議,但還不需要實施在專案中。

4.暫緩
可能是技術不夠成熟或被錯誤使用,別在專案中引入。

項目

出現在雷達上的單一圓點就是項目,代表目前出現的軟體技術。項目位置會不斷變化代表其應該採取的行動

在 ThoughtWorks 實際的技術雷達如下

Radar From ThoughtsWork

透過這四層及四等份就可列出應該對什麼技術採取什麼行動,畫成表格如下

技術種類\進行動作採納試驗評估暫停
技巧應該採用的技巧應該試驗的技巧應該評估的技巧應該暫停的技巧
工具應該採用的工具應該試驗的工具應該評估的工具應該暫停的工具
語言框架應該採用的語言框架應該試驗的語言框架應該評估的語言框架應該暫停的語言框架
平台應該採用的平台應該試驗的平台應該評估的平台應該暫停的平台

建立雷達的策略

ThoughtWorks 每半年會更新一次雷達,基本上可以根據該雷達來挑選想採取的行動和技術,也有提供建立私人雷達的方式,參閱這裡

另外每個不同的組織都有不同的商業目標,要發展的軟體策略也不盡相同,因此最好自行建立組織本身需要的技術雷達。

對於架構師而言,廣度應該優先於深度(但不代表深度不重要),在加入項目的策略上應該盡量讓項目平均分佈在四個象限,而不是讓項目全集中在某個象限或某個象限只有一個項目。

也要特別注意別輕易引入暫緩的項目。若採納的項目和自己專業有關但還很陌生,也請花一些時間進行了解(測試式學習是一個好的開始)。

分類
Android Architectural Pattern

Model in todo-mvp

概述

透過todo-mvp來說明MVP中的Model
todo-mvp 是 Android 官方用來說明 MVP Pattern的範例,參考 https://github.com/googlesamples/android-architecture
todo-mvp 裡的 Model 為TaskRepository,TaskRepository繼承TasksDataSource。
TaskDataSource實際上是一個interface,其中2個內部介面LoadTasksCallback和GetTasksCallback用來作callback使用,在內部介面的onTasksLoaded方法用來當取得task成功之後把task傳回呼叫點的用途,而onDataNotAvailable方法用來當取得task失敗後的後續處理。
其餘在TasksDataSource介面的方法都是存取資料的共用方法,只要是Model都要實作這些方法。
TasksDataSource.java

public interface TasksDataSource {
    interface LoadTasksCallback {
        void onTasksLoaded(List<Task> tasks);
        void onDataNotAvailable();
    }
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    void getTasks(@NonNull LoadTasksCallback callback);
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    void saveTask(@NonNull Task task);
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    void clearCompletedTasks();
    void refreshTasks();
    void deleteAllTasks();
    void deleteTask(@NonNull String taskId);
}

接著看Presenter如何關聯 Model以及使用Model。把焦點放在AddEditTaskPresenter。
1.AddEditTaskPresenter(Presenter)本身不會持有任何資料,資料放在Model中。
2.Presenter會通知Model去改變資料。
3.Presenter會持有Model和View的變數並在建構式初始化他們。
4.Presenter會在建構式初始化Model,接著在需要改變資料的位置去操縱Model改變資料,Model改變資料後Presenter再通知View重新載入資料。
AddEditTaskPresenter.java

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {
    @NonNull
    private final TasksDataSource mTasksRepository;
    @NonNull
    private final AddEditTaskContract.View mAddTaskView;
    @Nullable
    private String mTaskId;
    private boolean mIsDataMissing;
    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     * @param shouldLoadDataFromRepo whether data needs to be loaded or not (for config changes)
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mIsDataMissing = shouldLoadDataFromRepo;
        mAddTaskView.setPresenter(this);
    }
    @Override
    public void start() {
        if (!isNewTask() && mIsDataMissing) {
            populateTask();
        }
    }
    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }
    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
        mIsDataMissing = false;
    }
    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }
    @Override
    public boolean isDataMissing() {
        return mIsDataMissing;
    }
    private boolean isNewTask() {
        return mTaskId == null;
    }
    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }
    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }
}

注意雖然Model的變數型態為TasksDataSource(interface),但在Presenter建構式傳入的其實是TaskRepository(繼承自TasksDataSource)。
AddEditTaskActivity.java

...
        mPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskView);
...

Injection.java

public class Injection {
    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(context));
    }
}

因此我們需要看的是TasksRepository的內容。
TasksRepository.java

public class TasksRepository implements TasksDataSource {
    private static TasksRepository INSTANCE = null;
    private final TasksDataSource mTasksRemoteDataSource;
    private final TasksDataSource mTasksLocalDataSource;
    /**
     * This variable has package local visibility so it can be accessed from tests.
     */
    Map<String, Task> mCachedTasks;
    /**
     * Marks the cache as invalid, to force an update the next time data is requested. This variable
     * has package local visibility so it can be accessed from tests.
     */
    boolean mCacheIsDirty = false;
    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }
    /**
     * Returns the single instance of this class, creating it if necessary.
     *
     * @param tasksRemoteDataSource the backend data source
     * @param tasksLocalDataSource  the device storage data source
     * @return the {@link TasksRepository} instance
     */
    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
                                              TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }
    /**
     * Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance
     * next time it's called.
     */
    public static void destroyInstance() {
        INSTANCE = null;
    }
    /**
     * Gets tasks from cache, local data source (SQLite) or remote data source, whichever is
     * available first.
     * <p>
     * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if all data sources fail to
     * get the data.
     */
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        checkNotNull(callback);
        // Respond immediately with cache if available and not dirty
        if (mCachedTasks != null && !mCacheIsDirty) {
            callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            return;
        }
        if (mCacheIsDirty) {
            // If the cache is dirty we need to fetch new data from the network.
            getTasksFromRemoteDataSource(callback);
        } else {
            // Query the local storage if available. If not, query the network.
            mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
                @Override
                public void onTasksLoaded(List<Task> tasks) {
                    refreshCache(tasks);
                    callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
                }
                @Override
                public void onDataNotAvailable() {
                    getTasksFromRemoteDataSource(callback);
                }
            });
        }
    }
    @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.saveTask(task);
        mTasksLocalDataSource.saveTask(task);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), task);
    }
    @Override
    public void completeTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.completeTask(task);
        mTasksLocalDataSource.completeTask(task);
        Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), completedTask);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        checkNotNull(taskId);
        completeTask(getTaskWithId(taskId));
    }
    @Override
    public void activateTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.activateTask(task);
        mTasksLocalDataSource.activateTask(task);
        Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), activeTask);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        checkNotNull(taskId);
        activateTask(getTaskWithId(taskId));
    }
    @Override
    public void clearCompletedTasks() {
        mTasksRemoteDataSource.clearCompletedTasks();
        mTasksLocalDataSource.clearCompletedTasks();
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        Iterator<Map.Entry<String, Task>> it = mCachedTasks.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Task> entry = it.next();
            if (entry.getValue().isCompleted()) {
                it.remove();
            }
        }
    }
    /**
     * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it
     * uses the network data source. This is done to simplify the sample.
     * <p>
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if both data sources fail to
     * get the data.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);
        Task cachedTask = getTaskWithId(taskId);
        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }
        // Load from server/persisted if needed.
        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }
            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }
                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }
    @Override
    public void refreshTasks() {
        mCacheIsDirty = true;
    }
    @Override
    public void deleteAllTasks() {
        mTasksRemoteDataSource.deleteAllTasks();
        mTasksLocalDataSource.deleteAllTasks();
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
    }
    @Override
    public void deleteTask(@NonNull String taskId) {
        mTasksRemoteDataSource.deleteTask(checkNotNull(taskId));
        mTasksLocalDataSource.deleteTask(checkNotNull(taskId));
        mCachedTasks.remove(taskId);
    }
    private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
        mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                refreshCache(tasks);
                refreshLocalDataSource(tasks);
                callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            }
            @Override
            public void onDataNotAvailable() {
                callback.onDataNotAvailable();
            }
        });
    }
    private void refreshCache(List<Task> tasks) {
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
        for (Task task : tasks) {
            mCachedTasks.put(task.getId(), task);
        }
        mCacheIsDirty = false;
    }
    private void refreshLocalDataSource(List<Task> tasks) {
        mTasksLocalDataSource.deleteAllTasks();
        for (Task task : tasks) {
            mTasksLocalDataSource.saveTask(task);
        }
    }
    @Nullable
    private Task getTaskWithId(@NonNull String id) {
        checkNotNull(id);
        if (mCachedTasks == null || mCachedTasks.isEmpty()) {
            return null;
        } else {
            return mCachedTasks.get(id);
        }
    }
}

TasksRepository實現3層緩存,首先第1層緩存為記憶體也就是Map<String, Task> mCachedTasks;
第2層緩存為本地端資料來源,也就是private final TasksDataSource mTasksLocalDataSource;
因為該變數的型態也是TasksDataSource為interface,因此不會被資料來源的實現綁住,也就是說若想更換不同的資料庫,也只要新增TasksDataSource的子類別繼承TasksDataSource即可(OCP)。
第3層緩存為遠端資料來源,為 private final TasksDataSource mTasksRemoteDataSource;
變數型態也是TasksDataSource,也可以簡單替換遠端來源的實現(OCP),如volley, okhttp, retrofit等等。
若以儲存資料來說,在順序性來說沒有分別,這3層都會儲存資料,如下面的TasksRepository.saveTask方法的實作內容

    @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.saveTask(task);
        mTasksLocalDataSource.saveTask(task);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), task);
    }

若是讀取資料,則會先從第1層緩存記憶體(mCachedTasks)去讀取資料,若資料存在就直接回傳,若資料不存在,則從第2層緩存本地端資料庫(mTasksLocalDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料。
若還是沒有資料則從第3層緩存遠端網路(mTasksRemoteDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料,若資料不存在則顯示該資料不存在訊息。
如下方的TasksRepository.getTask方法內容

    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);
        Task cachedTask = getTaskWithId(taskId);
        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }
        // Load from server/persisted if needed.
        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }
            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }
                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

接著來看看在TasksRepository建構式,存取權限為私有,代表只能透過該纇別內部呼叫。

    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }

呼叫該建構式的位置為getInstance()方法,會透過其方法的參數設定tasksRemoteDataSource和tasksLocalDataSource。

    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
                                              TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }

而getInstance方法的呼叫者為Injection類別的ProvideTasksRepository方法。

public class Injection {
    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        ToDoDatabase database = ToDoDatabase.getInstance(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(new AppExecutors(),
                        database.taskDao()));
    }
}

可以看到provideTasksRepository方法內即為FakeTasksRemoteDataSource.getIntance()和TasksLocalDataSource.getInstance()分別代表遠端資料來源和本地端資料來源。
接著看看FakeTasksRemoteDataSource類別。其內容相當簡單,儲存資料的方式是透過一個Map<String, Task> TASKS_SERVICE_DATA 來儲存資料。
FakeTasksRemoteDataSource.java

public class FakeTasksRemoteDataSource implements TasksDataSource {
    private static FakeTasksRemoteDataSource INSTANCE;
    private static final Map<String, Task> TASKS_SERVICE_DATA = new LinkedHashMap<>();
    // Prevent direct instantiation.
    private FakeTasksRemoteDataSource() {}
    public static FakeTasksRemoteDataSource getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new FakeTasksRemoteDataSource();
        }
        return INSTANCE;
    }
    @Override
    public void getTasks(@NonNull LoadTasksCallback callback) {
        callback.onTasksLoaded(Lists.newArrayList(TASKS_SERVICE_DATA.values()));
    }
    @Override
    public void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback) {
        Task task = TASKS_SERVICE_DATA.get(taskId);
        callback.onTaskLoaded(task);
    }
    @Override
    public void saveTask(@NonNull Task task) {
        TASKS_SERVICE_DATA.put(task.getId(), task);
    }
    @Override
    public void completeTask(@NonNull Task task) {
        Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);
        TASKS_SERVICE_DATA.put(task.getId(), completedTask);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        // Not required for the remote data source.
    }
    @Override
    public void activateTask(@NonNull Task task) {
        Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());
        TASKS_SERVICE_DATA.put(task.getId(), activeTask);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        // Not required for the remote data source.
    }
    @Override
    public void clearCompletedTasks() {
        Iterator<Map.Entry<String, Task>> it = TASKS_SERVICE_DATA.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Task> entry = it.next();
            if (entry.getValue().isCompleted()) {
                it.remove();
            }
        }
    }
    public void refreshTasks() {
        // Not required because the {@link TasksRepository} handles the logic of refreshing the
        // tasks from all the available data sources.
    }
    @Override
    public void deleteTask(@NonNull String taskId) {
        TASKS_SERVICE_DATA.remove(taskId);
    }
    @Override
    public void deleteAllTasks() {
        TASKS_SERVICE_DATA.clear();
    }
    @VisibleForTesting
    public void addTasks(Task... tasks) {
        for (Task task : tasks) {
            TASKS_SERVICE_DATA.put(task.getId(), task);
        }
    }
}

 
最後看看 TasksLocalDataSource 類別。
該類別有TaskDao以及AppExecutors 變數,其中TaskDao提供存取Task的介面,為使用Room的寫法。關於Room可以參考這篇
而AppExecutors主要負責Executor的執行。
TasksLocalDataSource.java

public class TasksLocalDataSource implements TasksDataSource {
    private static volatile TasksLocalDataSource INSTANCE;
    private TasksDao mTasksDao;
    private AppExecutors mAppExecutors;
    // Prevent direct instantiation.
    private TasksLocalDataSource(@NonNull AppExecutors appExecutors,
            @NonNull TasksDao tasksDao) {
        mAppExecutors = appExecutors;
        mTasksDao = tasksDao;
    }
    public static TasksLocalDataSource getInstance(@NonNull AppExecutors appExecutors,
            @NonNull TasksDao tasksDao) {
        if (INSTANCE == null) {
            synchronized (TasksLocalDataSource.class) {
                if (INSTANCE == null) {
                    INSTANCE = new TasksLocalDataSource(appExecutors, tasksDao);
                }
            }
        }
        return INSTANCE;
    }
    /**
     * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if the database doesn't exist
     * or the table is empty.
     */
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                final List<Task> tasks = mTasksDao.getTasks();
                mAppExecutors.mainThread().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (tasks.isEmpty()) {
                            // This will be called if the table is new or just empty.
                            callback.onDataNotAvailable();
                        } else {
                            callback.onTasksLoaded(tasks);
                        }
                    }
                });
            }
        };
        mAppExecutors.diskIO().execute(runnable);
    }
    /**
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if the {@link Task} isn't
     * found.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                final Task task = mTasksDao.getTaskById(taskId);
                mAppExecutors.mainThread().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (task != null) {
                            callback.onTaskLoaded(task);
                        } else {
                            callback.onDataNotAvailable();
                        }
                    }
                });
            }
        };
        mAppExecutors.diskIO().execute(runnable);
    }
    @Override
    public void saveTask(@NonNull final Task task) {
        checkNotNull(task);
        Runnable saveRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.insertTask(task);
            }
        };
        mAppExecutors.diskIO().execute(saveRunnable);
    }
    @Override
    public void completeTask(@NonNull final Task task) {
        Runnable completeRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.updateCompleted(task.getId(), true);
            }
        };
        mAppExecutors.diskIO().execute(completeRunnable);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        // Not required for the local data source because the {@link TasksRepository} handles
        // converting from a {@code taskId} to a {@link task} using its cached data.
    }
    @Override
    public void activateTask(@NonNull final Task task) {
        Runnable activateRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.updateCompleted(task.getId(), false);
            }
        };
        mAppExecutors.diskIO().execute(activateRunnable);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        // Not required for the local data source because the {@link TasksRepository} handles
        // converting from a {@code taskId} to a {@link task} using its cached data.
    }
    @Override
    public void clearCompletedTasks() {
        Runnable clearTasksRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteCompletedTasks();
            }
        };
        mAppExecutors.diskIO().execute(clearTasksRunnable);
    }
    @Override
    public void refreshTasks() {
        // Not required because the {@link TasksRepository} handles the logic of refreshing the
        // tasks from all the available data sources.
    }
    @Override
    public void deleteAllTasks() {
        Runnable deleteRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteTasks();
            }
        };
        mAppExecutors.diskIO().execute(deleteRunnable);
    }
    @Override
    public void deleteTask(@NonNull final String taskId) {
        Runnable deleteRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteTaskById(taskId);
            }
        };
        mAppExecutors.diskIO().execute(deleteRunnable);
    }
    @VisibleForTesting
    static void clearInstance() {
        INSTANCE = null;
    }
}

整體來說因為Model提供了三層緩存,複雜度反而比Presenter和View還高。
從裡面的設計也可以看到設計原則(OCP)等等。
以上便是 todo-mvp 的 Model 。


以下為MVP相關內容

MVP Pattern in Android

分類
Android Architectural Pattern

MVP Pattern in Android

概述

(關於 MVC Pattern in Android 可以參考這篇,本篇是套用 MVP 於 Android 的描述和實作)
MVP 將架構分為 3 個部分,分別為 Model(模型層),View(視圖層),Presenter(展示層)
View(視圖層): 負責與使用者互動並顯示來自展示層的資料。
Presenter(展示層): 負責處理視圖邏輯和互動邏輯。
Model(模型層): 負責管理業務邏輯,提供網路和資料庫的監聽和修改介面。

設計理念

當 Model 傳輸資料到 Presenter 時,Presenter 會封裝視圖邏輯後再把資料傳遞給 View。和 MVC 最大的不同為 MVP 將視圖邏輯從 Model 移動到 Presenter。而 Model 只保留業務邏輯,從而分離視圖邏輯和業務邏輯,且 View 和 Model 完全分離不會有任何相依關係。
View 和 Presenter 只透過介面的方法互相溝通,不依賴具體方法。
Presenter 不依賴於 Android 的 UI 元件,也就是在 Presenter 不會出現 Toast, Button, Context 等等,Presenter 處理完邏輯之後再呼叫 View 顯示內容。

Contract 介面

在 Google 所提供 MVP 範例中可以看到 View 和 Presenter 的介面互相對應,為了描述其相對應的關係,每對 View 和 Presenter 的介面都會放置於其 Contract 介面。e.g.,

public interface AddEditTaskContract {
    interface View extends BaseView<Presenter> {
        void showEmptyTaskError();
        void showTasksList();
        void setTitle(String title);
        void setDescription(String description);
        boolean isActive();
    }
    interface Presenter extends BasePresenter {
        void saveTask(String title, String description);
        void populateTask();
    }
}

BaseView 和 BasePresetner 介面定義了 View 層和 Presenter 層的公共接口,在 BaseView 會提供 setPresenter 方法以及泛型來讓 View 指定對應的 Presenter。e.g.,

public interface BaseView<T> {
    void setPresenter(T presenter);
}

BasePresenter 則是提供公共的啟動方法 start。e.g.,

public interface BasePresenter {
    void start();
}

View

在 MVP 架構中,View 和 Presenter 互相對應,View 用來顯示 Presenter 的資料。先將使用者輸入事件轉發給 Presenter,當 Presenter 處理完邏輯後,再呼叫 View 顯示內容。

  1. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
  2. View和Presenter的互相關連是透過 setPresenter 方法,該方法會在 Presenter 的建構式呼叫。
  3. View 通常提供更新畫面的方法讓 Presenter 在執行完邏輯後呼叫。

在 Google 範例中 View 比較特別,它不是 Activity,而是一個自定義類別,該類別會使用自定義的 xml,xml 中會包含其它的 UI 元件。e.g.,
AddEditTaskView.java(View)

public class AddEditTaskView extends ScrollView implements AddEditTaskContract.View {
    private TextView mTitle;
    private TextView mDescription;
    private AddEditTaskContract.Presenter mPresenter;
    private boolean mActive;
    public AddEditTaskView(Context context) {
        super(context);
        init();
    }
    public AddEditTaskView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        inflate(getContext(), R.layout.addtask_view_content, this);
        mTitle = (TextView) findViewById(R.id.add_task_title);
        mDescription = (TextView) findViewById(R.id.add_task_description);
        mActive = true;
    }
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mActive = true;
    }
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mActive = false;
    }
    @Override
    public void showEmptyTaskError() {
        Snackbar.make(mTitle,
                getResources().getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show();
    }
    @Override
    public void showTasksList() {
        Activity activity = getActivity(this);
        activity.setResult(Activity.RESULT_OK);
        activity.finish();
    }
    @Override
    public void setTitle(String title) {
        mTitle.setText(title);
    }
    @Override
    public void setDescription(String description) {
        mDescription.setText(description);
    }
    @Override
    public boolean isActive() {
        return mActive;
    }
    @Override
    public void setPresenter(AddEditTaskContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }
    // TODO: This should be in the view contract
    public String getTitle() {
        return mTitle.getText().toString();
    }
    // TODO: This should be in the view contract
    public String getDescription() {
        return mDescription.getText().toString();
    }
}

在 init 方法中 R.layout.addtask_view_content = addtask_view_content.xml e.g.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">
        <EditText
            android:id="@+id/add_task_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/title_hint"
            android:singleLine="true"
            android:textAppearance="@style/TextAppearance.AppCompat.Title" />
        <EditText
            android:id="@+id/add_task_description"
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:gravity="top"
            android:hint="@string/description_hint" />
    </LinearLayout>
</merge>

簡單的說 View 是 Activity 內的一個 UI 元件,會以變數的方式存在於 Activity中。該變數的初始化也會使用 findViewById 方式初始化,初始化的位置在onCreate 中,而 View 和 Presenter 的相互關聯在 Presenter 的建構式。e.g.,
AddEditTaskActivity.java(這是 Activity 不是 View)

public class AddEditTaskActivity extends AppCompatActivity {
    public static final int REQUEST_ADD_TASK = 1;
    public static final String ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID";
    private AddEditTaskPresenter mPresenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);
        // Set up the toolbar.
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        checkNotNull(actionBar, "actionBar cannot be null");
        actionBar.setDisplayHomeAsUpEnabled(true);
        actionBar.setDisplayShowHomeEnabled(true);
        final AddEditTaskView addEditTaskView =
                (AddEditTaskView) findViewById(R.id.add_edit_task_view);
        checkNotNull(addEditTaskView, "addEditTaskView not found in layout");
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab_edit_task_done);
        checkNotNull(fab, "fab not found in layout");
        fab.setImageResource(R.drawable.ic_done);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /*
                 * TODO:
                 * View listeners should simply report the event to the presenter.
                 * In this case: mPresenter.onSavePressed()
                 */
                mPresenter.saveTask(addEditTaskView.getTitle(), addEditTaskView.getDescription());
            }
        });
        String taskId = null;
        if (getIntent().hasExtra(ARGUMENT_EDIT_TASK_ID)) {
            taskId = getIntent().getStringExtra(ARGUMENT_EDIT_TASK_ID);
            actionBar.setTitle(R.string.edit_task);
        } else {
            actionBar.setTitle(R.string.add_task);
        }
        mPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskView);
    }
    @Override
    protected void onResume() {
        super.onResume();
        mPresenter.start();
    }
    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return true;
    }
    @VisibleForTesting
    public IdlingResource getCountingIdlingResource() {
        return EspressoIdlingResource.getIdlingResource();
    }
}

Presenter

  1. Presenter 其內部會持有 View 和 Model 的引用變數。
  2. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
  3. Presenter的建構式會傳入 View 和 Model 並進行初始化。
  4. Presenter不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, TextView, Toast, Context 等等,只會處理業務邏輯,但可以依賴 RESULT_OK 之纇的整數常數。
  5. Presenter 會有一個 Model 的變數並從該變數來操作資料。
  6. Presenter 通常是處理完邏輯之後,再呼叫 View 的介面更新 UI。

e.g., AddEditTaskPresenter.java

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {
    @NonNull
    private final TasksDataSource mTasksRepository;
    @NonNull
    private final AddEditTaskContract.View mAddTaskView;
    @Nullable
    private String mTaskId;
    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mAddTaskView.setPresenter(this);
    }
    @Override
    public void start() {
        if (!isNewTask()) {
            populateTask();
        }
    }
    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }
    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
    }
    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }
    private boolean isNewTask() {
        return mTaskId == null;
    }
    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }
    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }
}

TasksDataSource mTasksRepository 就是 Model。在建構式中傳入 View 和 Model,並呼叫 View 的 setPresenter 綁定 Presenter 自己。

從 updateTask 方法可以看到 先呼叫 Model 改變資料(mTasksRepository.saveTask)
接著再呼叫 View 顯示內容(mAddTaskView.showTasksList)

e.g.,

    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }

Model

Model 為 TaskRepository,TaskRepository 繼承 TasksDataSource,而 TaskDataSource 實際上是一個 interface,其中 2 個內部介面 LoadTasksCallback 和 GetTasksCallback 用來作 callback 使用,內部介面的 onTasksLoaded 方法用來當取得 task 成功之後把 task 傳回呼叫點的用途,而 onDataNotAvailable 方法用來當取得 task 失敗後的後續處理。
其餘在 TasksDataSource 介面的方法都是改變資料的共用方法,只要是 Model 都要實作這些方法。
TasksDataSource.java

public interface TasksDataSource {
    interface LoadTasksCallback {
        void onTasksLoaded(List<Task> tasks);
        void onDataNotAvailable();
    }
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    void getTasks(@NonNull LoadTasksCallback callback);
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    void saveTask(@NonNull Task task);
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    void clearCompletedTasks();
    void refreshTasks();
    void deleteAllTasks();
    void deleteTask(@NonNull String taskId);
}

關於Model的具體介紹參考這篇

總結

  1. View 和 Model 不會有依賴
  2. View 和 Presenter 都是透過介面互相呼叫,而不是透過具體方法
  3. Presenter 不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, Toast, EditText, Context 等等
  4. View 會提供 Presenter 處理完邏輯後顯示內容的方法

接下來使用 MVP 改寫 MVC Pattern in Android 的亂數產生器

Contract

首先建立 MVPContract 介面,該介面中一樣包含 View 和 Presenter 兩個互相對應的內部介面,View 介面一開始可能只有 setPresenter 方法隨後跟著具體子類別的完成再逐漸提取方法到介面內。e.g.,

public interface MVPContract {
  interface View {
    void setPresenter(Presenter presenter);
    void setRollNumber(int randomNumber);
  }
  interface Presenter {
    void rollIt();
  }
}

View

View 的部分直接讓 Activity 去繼承 MVPContract.View,並在內持有 MVPContract.Presenter 的變數並複寫 setPresenter 方法。
關於 MVP View 的重點為

  1. 不可依賴 Model
  2. 和 Presenter 只透過介面的方法交互,不可和實體方法交互

e.g.,

public class MVPActivity extends AppCompatActivity implements OnClickListener, MVPContract.View {
  //Main UI
  private Button mRollIt;
  private TextView mRollNumber;
  private MVPContract.Presenter mPresenter;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.mvp_activity);
    initData();
    initUI();
  }
  private void initData() {
    mPresenter = new MVPPresenter(this, new MVPModel());
  }
  private void initUI() {
    mRollIt = findViewById(R.id.mvp_roll_it);
    mRollIt.setOnClickListener(this);
    mRollNumber = findViewById(R.id.mvp_show_roll_number);
  }
  @Override
  public void onClick(View view) {
    int uiID = view.getId();
    switch (uiID) {
      case R.id.mvp_roll_it:
        mPresenter.rollIt();
        break;
    }
  }
  @Override
  public void setPresenter(MVPContract.Presenter presenter) {
    mPresenter = presenter;
  }
  @Override
  public void setRollNumber(int randomNumber) {
    mRollNumber.setText(""+randomNumber);
  }
}

Presenter

建立 MVPPresenter 繼承 MVPContract.Presenter,在建構式內關連 View 和 Model。
關於 MVP Presenter 的重點為

  1. 和 View 只透過介面方法交互,不可和實體方法交互
  2. 不可依賴 Android 的 UI 元件,如 Button,EditText,Context 等等
public class MVPPresenter implements MVPContract.Presenter {
  private MVPContract.View mView;
  private MVPModel mModel;
  public MVPPresenter(MVPContract.View view, MVPModel model) {
    mView = view;
    mModel = model;
    mView.setPresenter(this);
  }
  @Override
  public void rollIt() {
    mModel.rollOnce();
    mView.setRollNumber(mModel.getRandomNumber());
  }
}

Model

最後是 Model,Model 並沒有變化。

public class MVPModel {
  private int mRandomNumber;
  public void rollOnce() {
    mRandomNumber = new Random().nextInt(100);
  }
  public int getRandomNumber() {
    return mRandomNumber;
  }
}

以上為MVP Pattern in Android的範例。


以下為MVP相關內容

Model in todo-mvp

分類
Android Architectural Pattern

MVC Pattern in Android

概述

(關於 MVC 在 POSA 的說明可以參考這篇,本篇是套用 MVC 於 Android 的描述和實作)
MVC 將架構分為 3 個部分,分別為 Model(模型層),View(視圖層),Controller(控制層)
View(視圖層):負責與使用者互動並顯示來自模型層的資料。
Controller(控制層):負責根據來自視圖層的互動訊息,指揮模型層更新資料或指揮視圖層重新載入模型層的資料。
Model(模型層):負責管理視圖邏輯和業務邏輯,提供網路和資料庫的監聽和修改介面。

設計理念

通常視圖邏輯發生修改的機會高於業務邏輯發生修改的機會,因此希望當這兩者發生變化時不會影響彼此(業務邏輯發生變化時不會影響視圖邏輯,視圖邏輯不會影響業務邏輯)。
在 MVC 架構中,View 依賴於 Model 和 Controller,Controller 依賴 View 和 Model,Model 完全獨立不依賴其它兩者。
View 接受使用者輸入事件並轉發給 Controller ,再根據 Controller 的指令顯示 Model 的資料。
Model 提供資料並更新資料,View 的顯示內容就是來自 Model 提供的資料,Controller 也會更新 Model 的資料。
Controller 根據來自 View 的使用者輸入事件,發出指令給 Model 更新資料並通知 View 重新載入 Model 的資料。
根據 Controller 更新 Model 資料的方式,可以分為主動和被動。因此 MVC 也可分為主動模式(Active)和被動模式(Passive)。

被動(Passive)模式

在被動模式中 ,Controller 是唯一可以更改 Model 資料的角色,而 View 只能根據 Controller 的指令被動從 Model 取得資料並顯示內容。
Controller 控制整個流程的更新過程,根據輸入事件被動的通知 Model 更改資料再通知 View 顯示資料。
流程如下:
使用者觸發 View 的輸入事件,事件轉發給 Controller,Controller 通知 Model 更改資料。當 Model 資料更改完成後,Controller 再通知 View 取得 Model 的資料並顯示內容。

被動模式實作

以簡單的亂數產生器作為範例。
首先從 View 開始。
View 就是 Activity,Activity 只有 1 個 Button 用來產生亂數,以及 1 個 TextView 用來顯示亂數。
View 會持有 Model 和 Controller 的參考,Model 參考用來取得資料並顯示內容,而 Controller 參考用來轉發使用者輸入事件。

public class MVCPassiveActivity extends AppCompatActivity implements OnClickListener {
  private Button mRollIt;
  private TextView mRollNumber;
  private MVCPassiveController mController;
  private MVCPassiveModel mModel;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.mvc_passive_activity);
    initData();
    initUI();
  }
  private void initData() {
    mModel = new MVCPassiveModel();
    mController = new MVCPassiveController(this, mModel);
  }
  private void initUI() {
    mRollIt = findViewById(R.id.mvc_passive_roll_it);
    mRollIt.setOnClickListener(this);
    mRollNumber = findViewById(R.id.mvc_passive_show_roll_number);
  }
  @Override
  public void onClick(View view) {
    int uiID = view.getId();
    switch (uiID) {
      case R.id.mvc_passive_roll_it:
        mController.rollIt();
        break;
    }
  }
  public void update() {
    mRollNumber.setText("" + mModel.getRandomNumber());
  }
}

需要注意的是在 initData 方法中初始化 Model 以及 Controller。而在 MVCPassiveController 的建構式中會去關連 View 和 Model。
在 onClick 方法中會去轉發使用者輸入事件給 Controller,而 Controller 的 rollIt 方法就是去通知 Model 更改資料。
最後在 update 方法就是從 Model 取得資料並顯示內容。
接著是 Controller ,Controller 會持有 View 和 Model 的參考並在建構式中關連起來。

public class MVCPassiveController {
  private MVCPassiveActivity mView;
  private MVCPassiveModel mModel;
  public MVCPassiveController(MVCPassiveActivity view, MVCPassiveModel model) {
    mView = view;
    mModel = model;
  }
  public void rollIt() {
    mModel.rollOnce();
    mView.update();
  }
}

在 rollIt 方法首先通知 Model 更改資料,接著再通知 View 取得 Model 的資料並顯示內容。
最後是 Model,Model 完全獨立不依賴於其它層,唯一需要注意的是實際上的 Model 應該是 repository 其中會有取得本地端和遠端資料的元件,本篇的範例相對簡單的多。

public class MVCPassiveModel {
  private int mRandomNumber;
  public void rollOnce() {
    mRandomNumber = new Random().nextInt(100);
  }
  public int getRandomNumber() {
    return mRandomNumber;
  }
}

主動(Active)模式

主動模式會應用觀察者模式(Observer Pattern) 來控制整個流程。首先 Model 就是主題(Subject),而 View 和 Controller 則都是觀察者(Observer)。
當 Model 發生變化,其註冊的觀察者(View & Controller)都會自動收到通知,Controller 仍然是通知 Model 資料變化的角色,和被動模式的差別是 Controller 不再需要通知 View 更新資料,因為當 Model 的資料發生變化時,View 會自動收到通知。
流程如下:
使用者觸發 View 的輸入事件,事件轉發給 Controller,Controller 通知 Model 更改資料。
當 Model 資料更改完成後,觀察者們(View & Controller)都會自動收到通知並進行相關處理。

主動模式實作

也是以簡單的亂數產生器作為範例。
首先建立觀察者介面,觀察者必須繼承該介面,並實作介面中的 update 方法,該方法的參數就是更新資料後的 Model。

public interface IObserver {
  public void update(MVCActiveModel mvcActiveModel);
}

接著實作 Model,Model 就是主題,會持有 1 個觀察者們的 list。對該 Model 有興趣的觀察者都會註冊成為該主題的觀察者。

public class MVCActiveModel {
  private ArrayList<IObserver> mObservers = new ArrayList<>();
  private int mRandomNumber;
  public void rollOnce() {
    mRandomNumber = new Random().nextInt(100);
    notifyObservers();
  }
  public int getRandomNumber() {
    return mRandomNumber;
  }
  public void addObserver(IObserver observer) {
    mObservers.add(observer);
  }
  public void removeObserver(IObserver observer) {
    if (mObservers.contains(observer)) {
      mObservers.remove(observer);
    }
  }
  private void notifyObservers() {
    int sizeOfObservers = mObservers.size();
    for (int i = 0; i < sizeOfObservers; ++i) {
      mObservers.get(i).update(this);
    }
  }
}

notifyObservers 方法就是通知所有的觀察者,主題(Model)的資料已經發生變化,觀察者會收到通知,並自行決定如何處理已變化的資料。
而 rollOnce 方法就是改變資料並通知觀察者們的觸發點。
接著是 Controller,Controller 也是觀察者之一,因此需要繼承 IObserver。
而在建構式中會將自己註冊到 Model 的 Observer list 中。

public class MVCActiveController implements IObserver {
  private MVCActiveActivity mView;
  private MVCActiveModel mModel;
  public MVCActiveController(MVCActiveActivity view, MVCActiveModel model) {
    mView = view;
    mModel = model;
    mModel.addObserver(this);
  }
  public void rollIt() {
    mModel.rollOnce();
  }
  @Override
  public void update(MVCActiveModel model) {
  }
}

最後是 View,View 也是觀察者所以需要繼承 IObserver,並實作 update 方法,在該方法中取得更新後的 Model 並顯示其資料內容。
而在 initData 方法內會將自己註冊為 Model 的觀察者。

public class MVCActiveActivity extends AppCompatActivity implements OnClickListener, IObserver {
  //Main UI
  private Button mRollIt;
  private TextView mRollNumber;
  private MVCActiveController mController;
  private MVCActiveModel mModel;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.mvc_active_activity);
    initData();
    initUI();
  }
  private void initData() {
    mModel = new MVCActiveModel();
    mModel.addObserver(this);
    mController = new MVCActiveController(this, mModel);
  }
  private void initUI() {
    mRollIt = findViewById(R.id.mvc_active_roll_it);
    mRollIt.setOnClickListener(this);
    mRollNumber = findViewById(R.id.mvc_active_show_roll_number);
  }
  @Override
  public void onClick(View view) {
    int uiID = view.getId();
    switch (uiID) {
      case R.id.mvc_active_roll_it:
        mController.rollIt();
        break;
    }
  }
  @Override
  public void update(MVCActiveModel model) {
    mRollNumber.setText("" + model.getRandomNumber());
  }
}

變化

如果希望 Controller 完全不包含 Android 的視圖元素,可以建立一個 IView 介面並讓Activity 繼承它,而 Controller 持有對象的型別從 Activity 改為 IView,而原本對 Activity的操作都改為對 IView 的操作,如下
IView.java

public interface IView {
  public void update();
}

MVCPassiveActivity.java

public class MVCPassiveActivity extends AppCompatActivity implements OnClickListener, IView {
...
  @Override
  public void update() {
    mRollNumber.setText("" + mModel.getRandomNumber());
  }
}

MVCPassiveController.java

public class MVCPassiveController {
  private IView mView;
  public MVCPassiveController(IView view, MVCPassiveModel model) {
    mView = view;
    ...
  }
  public void rollIt() {
    ...
    mView.update();
  }
}

以上為MVC Pattern in Android。


以下為 MVP 相關內容

Model in todo-mvp

MVP Pattern in Android

分類
Architectural Pattern

MVC Pattern(Model View Controller)

定義

分為三個部分,模型(Model)包含核心功能和數據。視圖(View)展示訊息。控制器(Controller)處理使用者輸入。
View 和 Controller 共同構成使用者介面(UI)。變更傳播機制確保了使用者介面和模型之間的一致性。

主要設計思想

一般來說修改使用者介面的機會往往大於修改業務邏輯的機會,在修改業務邏輯時不必變動使用者介面的代碼(關注點分離)。

因此需要把使用者介面和業務邏輯分離。
1.相同的訊息在不同介面有不同的顯示。
2.應用程式的顯示和行為必須立即反應數據的改變。
3.使用者介面應該易於改變。
4.可以移植使用者介面而不影響核心功能。

解決方案

MVC 將應用程序分為 個部份,輸入處理輸出。
Model 封裝了數據以及核心功能。
View 向使用者顯示訊息,View Model 獲得數據,一個 Model 可能有多個 View
每個 View 都有一個相關的 ControllerController 接受輸入,該輸入通常為外部輸入e.g.,滑鼠,鍵盤事件,
而事件被轉換為 Model View 的請求。使用者僅透過 Controller 與系統交互。
MVC 的分離讓同一個 Model 可以有多個 View。如果使用者透過一個 View 的 Controller 去改變了 Model,那麼所有相依於該 Model 的 View 都應該反應這種變化。
因此一旦 Model 的數據發生變化,Model 要通報所有 View。而 View 從 Model 取得新數據並顯示變化。這可以透過 Observer Pattern 來達成。

結構

1.Model 包含核心功能,封裝相應數據並提供可存取數據的函式讓外部存取,這些數據讓View 來使用。
變更傳播機制維護一個註冊表。所有的 View 和 Controller 註冊他們有關變更的待通知的需求。Model 狀態的改變觸發變更傳播機制。
2.View 向使用者顯示訊息,不同的 View 用不同的方式呈現 Model 的訊息。
每個 View 定義一個被變更播機制觸發的更新過程。當更新過程被調用時,View 就會從Model 取得並顯示新的數據。
初始化階段,所有的 View 都與 Model 相關,並向變更傳播機制註冊。每個 View 將建立一個對應的 ControllerView 和 Controller 的關係是一對一的。
View 通常會提供讓 Controller 操作其顯示的功能。這對不影響 Model 狀態的動作是有必要的。如捲動畫面。
3.Controller 接受使用者的輸入事件,事件如何被發送到 Controller 是取決於使用者平台。Controller 接受事件後會轉發為對 View 或是 Model 的請求。
如果 Controller 的行為依賴於 Model,那麼 Controller 也必須向 Model 註冊自己並實現更新過程。

動態特性

1.初始化步驟:
建立 Model 實體,初始化 Model 內部數據。
建立 View 實體,需要取得 Model 的實體作為本身初始化參數。
View 加入 Model 的變更傳佈機制。
View 初始化 Controller , View 向 Controller 的初始化過程傳入 Model 和其本身當作參數。
Controller 加入 Model 的變更傳播機制。
應用程式開始處理輸入事件。
2.如何處理輸入:
Controller 接受使用者輸入並轉換事件,啟動 Model 的服務過程。
Model 執行請求並改變了內部數據。
Model 調用更新過程,通知所有註冊的 View 和 Controller
每個有註冊的 View 向 Model 取得更新的數據並顯示。
每個有註冊的 Controller 向 Model 更新的數據並執行相對應動作。

實作:

步驟16為基本實現,步驟79為增加靈活的實用性。
1.建立 Model 實現核心功能。
除了核心功能外,Model 會持有 observer 的集合,以及加入和刪除 observer 方法。
最後加上通知所有的 observer 狀態更新的方法。

public class RandomModel
{
    private static final int RANDOM_NUMBER_MAX = 10000;
    private int mRandomNumber;
    private ArrayList<IObserver> mObservers = new ArrayList<IObserver>();
    public void rollOnce()
    {
        mRandomNumber = new Random().nextInt(RANDOM_NUMBER_MAX);
        notifyObservers();
    }
    public int getRandomNumber()
    {
        return mRandomNumber;
    }
    public void addObserver(IObserver observer)
    {
        mObservers.add(observer);
    }
    public void removeObserver(IObserver observer)
    {
        if(mObservers.contains(observer)){
            mObservers.remove(observer);
        }
    }
    private void notifyObservers()
    {
        int sizeOfObservers = mObservers.size();
        for (int i = 0; i < sizeOfObservers ; ++i) {
            mObservers.get(i).update(this);
        }
    }
}

2.實現變更傳播機制
建立 observer 介面,observer 會有一個 update 方法,該方法會讓所有的 observer 子類別複寫。

public interface IObserver {
    public void update(RandomModel model);
}

3.建立 View
View 除了提供所有的可見元素之外,還必須實作 observer 介面並複寫 update 方法,在update 方法中藉由 Model 取得新數據以更新相關的可見元素。
另外在 View 的建構式中必須註冊變更傳播機制,以及建立 Controller

public class RandomView implements ActionListener, IObserver {
    private static final int VIEW_HEIGHT = 65;
    private static final int VIEW_WIDTH = 400;
    private static final String VIEW_TITLE = "RandomView";
    private static final String NAME_OF_ROLL_BUTTON = "Roll it";
    private RandomModel mRandomModel;
    private RandomController mRandomController;
    private JFrame mViewFrame;
    private JPanel mViewPanel;
    private JButton mButtonRollOnce;
    private JTextField mTextField;
    public RandomView(RandomModel randomModel) {
        mRandomModel = randomModel;
        mRandomModel.addObserver(this);
        mRandomController = new RandomController(mRandomModel,this);
        initLayoutComponents();
    }
    public void initLayoutComponents() {
        mViewPanel = new JPanel(new GridLayout());
        mViewFrame = new JFrame(VIEW_TITLE);
        mButtonRollOnce = new JButton(NAME_OF_ROLL_BUTTON);
        mButtonRollOnce.addActionListener(this);
        mTextField = new JTextField();
        mViewFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mViewFrame.setSize(new Dimension(VIEW_WIDTH, VIEW_HEIGHT));
        mViewPanel.add(mButtonRollOnce);
        mViewPanel.add(mTextField);
        mViewFrame.add(mViewPanel);
        mViewFrame.setVisible(true);
    }
    @Override
    public void actionPerformed(ActionEvent event) {
        if (event.getSource() == mButtonRollOnce) {
            mRandomController.startRollOnce();
        }
    }
    @Override
    public void update(RandomModel model) {
        int randomNumber = model.getRandomNumber();
        mTextField.setText(String.valueOf(randomNumber));
        System.out.println("in RandomView update");
    }
}

4.實作 Controller
Controller 必須實作 observer 介面並複寫 update 方法。
Controller 會將使用者輸入轉換為對 Model 或是 View 的事件請求。

public class RandomController implements IObserver{
    private RandomModel mRandomModel;
    private RandomView mRandomView;
    public RandomController(RandomModel model, RandomView view){
        mRandomView = view;
        mRandomModel = model;
        mRandomModel.addObserver(this);
    }
    public void startRollOnce(){
        mRandomModel.rollOnce();
    }
    @Override
    public void update(RandomModel model) {
        System.out.println("in RandomController update");
    }
}

5.設計並實作 View 和 Controller 關係
通常 View 和 Controller 之間為一對一。

6.實現 MVC 的準備。

public class Main
{
    public static void main(String[] args){
        RandomModel randomModel = new RandomModel();
        RandomView randomView = new RandomView(randomModel);
        //otherView with tha same Model
        //OtherView otherView = new OtherView(RandomModel);
    }
}

7.建立動態 View
如果應用程序具有動態打開和關閉 View 的功能,那麼可以建立一個用來管理 View 的元件。

8.可更換 Controller
View 可支援更換不同的 Controller,這種情況通常是對同一個 View 的不同操作。
e.g.,預設的 Controller 予許使用者進行操作,但在一些情況下禁止使用者進行任何操作(read only),為此可以在 View 中建立替換 Controller 的方法。

public void setController(OtherController controller){
        mRandomController = controller;
}

9.層次化 View 和 Controller
MVC 實現了可重用的 View 和 Controller,對於經常層次化使用的使用者接口元素(button, menu, editor)也是如此,
如果應用程序的使用者接口主要靠結合事先定義好的 View 對象來建立,那麼可以用組合模式來建立層次式靜態 View
如果同時啟動多個 View,那麼可能就有好幾個 Controller 同時關心事件,如果事件以某種順序分配到所有的 Controller 來處理,可以使用責任鏈模式管理事件的委託。

變體

Document View Pattern
View 結合了 Controller 的任務,兩者合併 。

已知應用

MFC
ET++

效果

優點:
1.同一 Model 的多個 View
2.同步化 View
3.可更換的 View 和 Controller
缺點:
增加複雜性
View 和 Controller 的緊密連結
View 的效率可能低落

分類
Architectural Pattern

Layered Architecture Pattern(分層架構模式)

定義:

能夠被分解為子任務組,其中每個子任務處於一個特定的抽象層次上。
 

範例:

網路協議(iso 7)
與採用單一模組來相比,分層方法是另一個更好的設計方式,優點有
1.幫助團隊開發,支持增量編碼和測試。
2.使用半獨立元件讓後期的元件交換更容易。
3.更好的實現技術可以透過簡單的重寫代碼分別加入。
 

環境:

一個需要分解的大系統。
 

問題:

系統的明顯特色是混合低層與高層的問題且高層依賴低層操作。
系統的一部份處理低層問題,如硬體(傳感器輸入,從文件中讀取或線路讀取電子信號)。另一部份處理高層問題如,使用者介面。
層之間的通信有兩個方向,一個為從高層到低層的請求,另一個為低層到高層的通知,通知可以是對請求的應答或事件的輸入數據。
系統往往需要一些與其垂直相交的水平層。這些操作處在同一個抽象層,但彼此之間很大的部份是獨立的。
關注點:
1.後期代碼的改動應該不影響整個系統。應該被限制在一個元件內且不影響其他元件。
2.介面應該是穩定的,甚至可以用標準來限定。
3.系統各個元件應該可以被替換。元件可以被別的實現方法來替代且不影響系統其他部份。
4.之後可能會建立其他系統,這些系統具有和目前設計一樣的底層問題。
5.相似職責應該分組以提高可理解性和可維護性。每個元件應該是具有內聚力的。
 

解決方案:

將系統分為適當層次,從最底層(1)開始,按照適當順序放置,將抽象層j放在j-1層的上層,直到最高層(n)
不需要說明某層j是不是需要分解為更複雜的子系統。重要的是在某層中所有用到的元件必須工作在同一層
 

結構:

Layer主要特徵為第j層的服務只被j+1層使用。每個獨立層都要防止被較高層直接訪問。
 

動態特性:

1.從上往下:客戶向 層n 發出請求,層無法完成該請求,於是調用層 n-1 相應的子任務,直到第1層,第1層完成後向上層反饋回去直到層n,從上而下通常稱為請求。
2.從下往上:一個驅動設備(1)探測到輸入之後,把輸入轉換成內部格式並報告給第2層。
由下而上通常稱為通知。
3.描述請求只達到某層的範圍,如果層n-1 可以達到需求,一個來自於頂層(n)的請求可能只需
達到相鄰層即可。
4.彼此能互相通信的層nan bn 彼此是獨立的2個層,一個請求從 an發出,沿著 a
n -1 移動直到 a 1a的最底層),接著傳遞給 b 1 (b的最底層)再沿著 b 層往上直到 b
n完成請求的回應沿著反方向傳遞。
 

實現:

以下的方法並非對所有的案例都是最好的,有時候採用從下而上或是溜溜球法會更好,所有的步驟並不是必須的,要看實際應用來判斷。
1.定義抽象準則來將任務分解成層,抽象準則往往來自平台概念上的區隔,如特定領域,概念複雜度等等。
e.g., 一個棋類遊戲可分為
遊戲整體策略
中盤戰術
基本移動
遊戲基本單位
e.g., 一般軟件可分為
用戶可見元素
特定應用模組
公共服務
操作系統介面
操作系統
硬體
 
2.根據抽象準則定義抽象層數。每個抽象層次對應模式中的一層,有時候抽象層次到層的映射是不明顯的
在確定是否針對特定點分為2個層或是合併為1個層時需要多加注意,過多的層會增加開銷,過少的層會導致不好的設計。
3.命名每個層並指定任務。
最高層的任務是整個系統的任務,所有其他層的任務只作為最高層的助手。
4.指定服務。
最重要的原則為層間必須嚴格的彼此分離。不可以有跨層存取的情況。
把較多的服務放在高層往往比低層來的好。
5.細化分層
重複步驟14。在考慮隱含層及其服務之前往往不可能精確定義抽象準則。
通常是錯誤定義組件及其服務,後來再根據他們的使用關係強加上一層結構。
e.g., 一個新元件可能要求不只一個層的服務,違反了嚴格分層的原則。溜溜球法:重複交替從上而下以及從下而上的設計過程。
6.為每個層指定一個接口
j 對層j+1 應該是一個黑盒設計,建立一個接口以提供所有層j 的服務並把該接口封裝在一個 facade 對象中。
7.建立獨立層
(獨立層為特定的某層)
如果一個獨立層很複雜,則它應該被分成幾個獨立元件。
8.指定相鄰層之間的通訊
層間通訊最常用的為推模組(push model)。當層j 請求層 j-1 的服務時,任何要求的訊息都要作為服務調用的一部分來傳輸。相反的拉模組(pull model)也很常用,使用的場合是底層自行從高層獲取可利用訊息,但拉模組會引入附加層和相鄰更高層的相依關係,要避免這層關係可以使用 callback
9.分離鄰接層
高層關心相鄰的底層,但底層並不關心用戶的身份。代表單向連接,當修改層j的服務,層j的改變可以忽略層j+1 的存在,上述的情況發生在從上而下。
若是從下而上,可以使用callback,高層要註冊底層的callback 方法。
10.設計錯誤處理策略
layer 來說錯誤處理比較麻煩,因為一個錯誤可以在它出現的層裡處理也可以送到高層。送到高層對於低層而言必須將錯誤轉換成對高層有意義的描述。根據經驗,盡可能在最低層處理錯誤。這可以防止高層被大量的錯誤以及錯誤處理代碼干擾。至少應該試著把相似錯誤類型歸類於更一般的錯誤類型,並僅傳播這些更一般的錯誤類型。
 

9.變體

1.鬆散分層系統(Relaxed Layered System)
層之間的約束較少,每個層可以使用比它低層的所有服務,而不僅是相鄰層。
可以增加使用的靈活性和性能,但代價是維護性。適用於要求性能高的系統。
2.通過繼承分層(Layering Through Inheritance):
底層作為基本類別,高層繼承自底層。
優點是高層可以根據需要修改底層的服務,缺點為高層和底層透過繼承綁定,底層若有修改高層必須重新編譯(脆弱基類問題)
 

10.已知使用

1.虛擬機虛擬機可以說是一種低層次的實現,它將低層次的細節或各類的硬體與高層次分離開來。
2.API:  API可以說是一個封裝底層常用功能的層。
3.訊息系統(Information System)IS 一開始往往使用2層式體系結構,底層是一個數據庫存有公司的特殊數據,頂層為用戶介面。

用戶介面數據庫

因為用戶邏輯會摻雜商業邏輯,因此可以再分一層為商業邏輯層

用戶介面商業邏輯
數據庫

但越來越多的數據庫需要支援,因此再新增一層領域層來對不同數據庫增加支援

用戶介面商業邏輯
領域層
數據庫

4. Windows NT
該操作系統是根據微核模式來建構。NT執行程序元件對應微核模式的微核元件。NT執行程序是一個鬆散分層系統。

系統服務層:子系統和NT執行系統之間的接口層。資源管理器層:包含對象管理器,安全引用監視器,過程管理器,I/O管理器,虛擬儲存管理器等等。
內核層:一些基本功能,中斷和意外處理,多處理器同步,線程調度和分配。
HAL(硬體抽象層):隱藏不同處理器之間的硬體差異。
硬體層:硬體

 

11.效果:

分層體系模式的優點
1.加強層的復用:如果一個層定義良好的抽象介面,可在多個環境中重用。
2.增加標準化:定義清楚的層可以促進標準化的開發。
3.限制相依性:層之間的改變被限制在該層中,不會影響其他的層,也增加了可移植性,測試性。
單層可被簡易的替換,可採用 Adapter pattern 或是 Bridge Pattern
4.增加可替換性:但如果想對2個介面以及服務不完全相配的層之間切換,則必須在2個層之間加上建立一個隔離層。
 
層的缺點:
1.更改行為的重複:如果不得不在許多層上做相當數量的重複工作以適應局部變動。
2.降低效率:分層體系效率通常低於整塊模組的效率。如果最高層有很大的程度依賴最底層,所有數據都必須通過中間層的轉換。
3.不必要的工作:如果底層的服務重複執行了多餘的工作,而這些工作並非高層所需。
4.難以確認分層的正確度:層數太少的分層不能完全發揮分層體系的優勢(可重用性,可更改性,可移植性)。