---
title: 'MVC 框架 - 歷史、經典、網頁端'
disqus: kyleAlien
---
MVC 框架 - 歷史、經典、網頁端
===
## OverView of Content
如有引用參考請詳註出處,感謝 :smile:
:::success
* **框架 `Framework` & 設計 `Design` ?**
**框架注重於程式碼的重用(行為)**,而**設計通常是針對結構的重用(設計)**,兩者都想當重要,兩者的關係是框架大於設計,設計比較想是解決事情的一種手段,對具體問題提出解決方法
:::
:::info
* **`MVC` 是屬於`Framework` 還是 `Architecture`**?
MVC 既包含 `Framework` 的概念,也可以視為一種 `Architecture`,但如果以軟體設計的角度下去看,它更偏向是 `Architecture`
* **Framework 角度**: MVC 可以被視為一種框架,因為它提供了一種組織和結構化代碼的方式,並定義了如何將應用程式的不同組件分離和組織起來。
* **Architecture 角度**: 同時,MVC 也可以被視為一種軟體架構,因為它定義了應用程式中各個組件的角色和相互作用。這種組織方式有助於確保程式碼的可維護性和可擴展性。
:::
[TOC]
## 框架 - 概述
* 首先要先了解何謂框架,**它與設計模式的差別在哪**
1. **框架**: 是對程式碼的重用 (大智慧~ 裡面可能有很多設計 & 邏輯)
2. **設計模式**: 結構、設計理念的重用 (小技巧~ 但很重要)
* 軟體開發有 ++三種級別++ 的重用
1. **內部重用**:如同設計模式,有公共使用的抽象模塊
2. **代碼重用**:屬於不同 Module 使用的公用模塊
3. **應用框架重用**:專用領域提供通用的或現成的基礎架構,以獲得最高級別的重用性
:::info
* 補充
常用的框架除了 MVC 以外還有 `MVVC`、`MTV`、`CBD`、`ORM`、`MVVM`... 等等框架
:::
## MVC 概述
MVC 是簡稱,全名是 `Model - View - Controller`,它是 `Reenskaug` 所提出,而回推到最根本,它是稱為 MVCE (Editor)
**==MVC 是一種框架,而非一個設計==**,**MVC 目的是為了++分離 View 層 & 資料 Data 層++的耦合**
> 以往會在 View 判斷 Data 在決定顯示的 View
### 原始 MVCE
* 下圖是 `Reenskaug` 為了解釋 MVC 所繪製的圖
> 
* 從上圖可以看到 1 個 Controller 可以對應多個 View,其包裹成一個 Tool 並多對多個 Model,而使用者也可以直接操控 Model
* Editor 是一個特殊的 Controller,其目的在於當你需要修改一個 View 時,會從 View 獲取一個臨時的 Controller 對象,這個臨時對象就是 `Editor` (可以當成小的 Controller),**MVC 建議使用 Editer 完成對 View & Model 的操作**(但是沒流行 🤣)
下圖是 `MVC-Editor` 結構
> 
### 經典 MVC
* 下圖是經典 MVC 結構,它去除 `Editor` 並簡化為三個層級
> 
| 區塊 | 介紹 | 重點 |
| -------- | -------- | - |
| View | View & Model 會保持一致,View 還會通過在 Model 中的註冊事件刷新界面(Model 是被觀察者),較好的設計是 View 是被動的,透過 Controller 改變 Model 就可以刷新 View 界面 | 負責畫面相關邏輯 |
| Controller | 控制器由 View 根據用戶的觸發並響應來自 View 的互交(響應線),根據 View 的事件邏輯修改 Model,**Controller 並不關心 View 如何展示相關數據 & 狀態,而是透過修改 Model** | 必須知道所有的 Model,並選擇 |
| Model | 從上圖可以看到 Model & View 有一定的耦合,並且 **Model 可以透過觀察者機制通知 View 刷新**(虛線部份) | 不處理跟畫面相關的程式 |
下圖是觀察者 MVC (View 自動刷新)
> 
### MVC - 中間層設計 Application Model
* 強化隔離 Model,以往 Model 會有受到 Controller (控制)、View (觀察) 的影響,現在**透過一個 `Application Model` 模塊,來加強隔離 `Model`**
> 也就是多了一個中間層設計
* **`Application Model` 作用主要是在 Model & View 之間加了一層緩衝,++將複雜的邏輯放置 Application Mode++**
> 
:::warning
* 雖然 Application MVC 解決了經典 MVC 中的 View & Model 耦合嚴重問題,但在 GUI 系統中又會有不同的演變
:::
### Web 愛用的 MVC - Model 2
* **MVC 是在 Web 中才比較流行**,但是要注意的是 Model 並無法主動通知 View (經典 MVC 可以)
:::info
* 為何 View 無法監聽 Model ?
因為 Http 的特性關係,**Http 並沒有狀態機**(每個請求不保存狀態),**單一請求對應單一回應** (**`Request`/`Response` 模型**)
> 也就是 Model 無法直接響應到 View 上,必須要透過 Controller 通知 View 來刷新 View
>
> Http 伺服器不可能主動對客戶的遊覽器發送訊息,必須要客戶先主動發出才行
:::
* **Model2 作為一種 MVC 框架**,並將其用於 Web 開發之中,套用在 Java 中
| Model 角色 | View 角色 | Controller 角色 |
| -------- | -------- | -------- |
| JavaBean | JSP | Servlet(伺服器) |
> 由於 Model 處理完資料後無法直接透過 Http 協議回應給客戶,所以必須切開 View(客戶端網頁畫面) & Model 之間的連線
>
> 切開後有點類似 MVP 架構
>
> 
* 可以看到 Controller 已經成為一個仲介,而 **View & Model 完全隔離**,**與 MVP 架構已經相當的接近**,但仍規劃在 MVC 架構的範疇可能是因為 View(`JSP`) 仍可以跳過 Controller 去訪問 Model 資料
> 像是 Web 的 `Model 1` 設計,就可以不用 `Controller` 層,說得更清楚一點是 `JSP` 取代了 `Controller & View` 層
>
> 
:::success
* **Model2 與 MVP 的差異**
其差異主要是在 **MVP 裡捕獲用戶乎呼叫是在 View 階段** (View 承受所有使用者的響應,而 View 通常是個平台 GUI 框架完成);
而 **Web 的 Model2 捕捉用戶請求是在 Controller**,這是些都是 MVC 的演化~
:::
:::warning
* **真正的 MVC** ?
**==所謂真理是只有在特定領域條件下成立的教條理論==**,所以必須隨著時代演變不斷進步
:::
### GUI 框架對 MVC 影響
* 對於 GUI 系統來說,一般都會提供一個完善的 View 系統,套用在 View 框架中,**==GUI View 本身也可以獲取用戶觸及事件==,這顯得傳統的 Controller 有些多餘 (經典 MVC 是透過 Controller 來捕捉)**
```java=
// Android GUI 可以監聽用戶觸擊事件
findViewById(R.id.myText).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.e("Designed", "Hello GUI");
}
});
```
:::info
* 傳統 MVC View 特點
不(用)應該知道用戶的事件 (點擊、輸入事件),**傳統 MVC ==沒有== ++控件的概念++、++事件驅動++**
也就是 **`Controller` 必須實現控件的事件輸出 & 輸入**,然後再分法給 View 顯示給使用者看,所以典型 MVC 並沒有在 GUI 系統流行起來
:::
* 由於 GUI 把事件的觸發包裹到 View 中,使的 Controller 顯得更加的多餘
所以在有 GUI 框架的系統中會 **把傳統 MVC `Controller` 所負責的事情部分交給 View** (`Controller` 的點擊事件),`Controller` 根據事件做出響應決定是否改變 Model
下圖是 GUI 所改變的 MVC
> 
添加典型的觀察者,讓 Model 可以通知 View 自動更新
> 
:::danger
* 要注意不要讓 Controller 的責任變得過於負重,這會導致 Controller 的職責模糊掉
:::
## MVC 使用
### 普通圖片加載 - 無 MVC
* Java UI 的設計概念如下,並且程式不區分 MVC 架構
> 
```java=
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.*;
public class NoMvc {
public static void main(String[] args) {
// Frame 容器
JFrame frame = new JFrame();
// 根部局
JPanel root = new JPanel();
root.setLayout(new BorderLayout()); // LayoutManager
// JLabel 裝載圖片並至於 View 中心
JLabel label = new JLabel();
setLabelImage(label, new File("C:\\Users\\alien\\OneDrive\\圖片\\MyPic\\1.jpg")); // 預設圖片
root.add(label, BorderLayout.CENTER); // 加入 View
JPanel btnLayout = new JPanel();
btnLayout.setLayout(new FlowLayout());
// 監聽 Button 事件
JButton Step_1 = new JButton("Step_1");
Step_1.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setLabelImage(label, new File("C:\\Users\\alien\\OneDrive\\圖片\\MyPic\\1.jpg"));
}
});
btnLayout.add(Step_1);
JButton Step_2 = new JButton("Step_2");
Step_2.addActionListener(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setLabelImage(label, new File("C:\\Users\\alien\\OneDrive\\圖片\\MyPic\\2.png"));
}
});
btnLayout.add(Step_2);
root.add(btnLayout, BorderLayout.SOUTH); // 下方
// 將視圖加入容器中
frame.setContentPane(root);
frame.pack();
frame.setTitle("MVC");
frame.setVisible(true);
}
public static void setLabelImage(JLabel label, File file) {
try {
Image image = ImageIO.read(file);
label.setIcon(new ImageIcon(image));
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
**--實作結果--**
> 
### MVC 模式圖片加載
* MVC 的演化版本相當的多,以下會展示 `經典 MVC`、`改進 MVC` 兩者個應用,首先第一步必須先將程式碼做分離,找出 Model、View、Controller
| 類型 | 包含內容 |
| -------- | -------- |
| Model | File 文件、圖片,也就是加仔內容 |
| View | JFrame、JPanel、JButton 等等 GUI |
| Controller | 接收 View 點擊事件、處理完畢後傳遞給 Model...仲介者 |
1. 經典 MVC
特別注意接口 ^1^ Controller 觀察 View 事件、^2^ View 觀察 Model 事件,其概念圖如下
> 
```java=
/**
* Model 部分
*/
import java.io.File;
public class Model {
private File mFile;
private ModelCallBack mCallBack;
public static final String DEFAULT_PIC = "C:\\Users\\alien\\OneDrive\\圖片\\MyPic\\1.jpg";
public static final String TARGET_PIC = "C:\\Users\\alien\\OneDrive\\圖片\\MyPic\\2.png";
public interface ModelCallBack {
void onChanged(File file);
}
public Model() {
mFile = new File(DEFAULT_PIC);
}
public void setCallBack(ModelCallBack callBack) {
mCallBack = callBack;
}
public void LoadPic() {
try {
Thread.sleep(1500); // 模仿耗時操作
System.out.println("Load Pic Finish");
if(mCallBack != null) {
mFile = new File(TARGET_PIC);
mCallBack.onChanged(mFile);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void DefaultPic() {
try {
Thread.sleep(1500); // 模仿耗時操作
System.out.println("Load Default Pic Finish");
if(mCallBack != null) {
mFile = new File(DEFAULT_PIC);
mCallBack.onChanged(mFile);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public File getFile() {
return mFile;
}
}
/**
* View 部分
*/
package MVC;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
// 專門處理有關 View 的事物,並且 View 觀察 Model
public class View implements Model.ModelCallBack{
private ViewEvent mViewEvent;
private JLabel mLabel;
// 點擊事件接口
public interface ViewEvent {
void StepOne();
void StepTwo();
}
public View(Model model) {
// View 作為觀察者觀察 Model
model.setCallBack(this);
// Frame 容器
JFrame frame = new JFrame();
// 根部局
JPanel root = new JPanel();
root.setLayout(new BorderLayout()); // LayoutManager
// JLabel 裝載圖片並至於 View 中心
mLabel = new JLabel();
setImageView(model.getFile());
root.add(mLabel, BorderLayout.CENTER); // 加入 View
JPanel btnLayout = new JPanel();
btnLayout.setLayout(new FlowLayout());
JButton Step_1 = new JButton("Step_1");
Step_1.addActionListener(e -> mViewEvent.StepOne());
JButton Step_2 = new JButton("Step_2");
Step_2.addActionListener(e-> mViewEvent.StepTwo());
btnLayout.add(Step_1);
btnLayout.add(Step_2);
root.add(btnLayout, BorderLayout.SOUTH); // 下方
// 將視圖加入容器中
frame.setContentPane(root);
frame.pack();
frame.setTitle("MVC");
frame.setVisible(true);
}
public void setImageView(File file) {
try {
mLabel.setIcon(new ImageIcon(ImageIO.read(file)));
} catch (IOException e) {
e.printStackTrace();
}
}
public void setViewEvent(ViewEvent viewEvent) {
mViewEvent = viewEvent;
}
@Override
public void onChanged(File file) {
setImageView(file);
}
}
/**
* Controller 部分
* 接收 View 的點擊事件
*/
public class Controller implements View.ViewEvent {
private Model mModel;
public Controller(View view, Model model) {
view.setViewEvent(this);
mModel = model;
}
@Override
public void StepOne() {
System.out.println("Step 1 Btn Click");
// 控制 Module
mModel.LoadPic();
}
@Override
public void StepTwo() {
System.out.println("Step 2 Btn Click");
// 控制 Module
mModel.DefaultPic();
}
}
/**
* 使用
*/
public class StartMvc {
public static void main(String[] args) {
Model m = new Model();
View v = new View(m);
new Controller(v, m);
}
}
```
2. 改進 MVC:其實 **從上面還可以看出來 View & Model 之間的關係還是過多**,所以會**把 View 觀察 Model 調整到,Controller 觀察 Model,++差別在由 Controller 來操控 View 的更新++**
以下為概念圖,將控制責任交給 Controller
:::warning
目前就是改進為 MVP
:::
> 
```java=
/**
* Model 並沒有的改變
*/
package MVC;
import java.io.File;
public class Model {
private File mFile;
private ModelCallBack mCallBack;
public static final String DEFAULT_PIC = "C:\\Users\\alien\\OneDrive\\圖片\\MyPic\\1.jpg";
public static final String TARGET_PIC = "C:\\Users\\alien\\OneDrive\\圖片\\MyPic\\2.png";
public interface ModelCallBack {
void onChanged(File file);
}
public Model() {
mFile = new File(DEFAULT_PIC);
}
public void setCallBack(ModelCallBack callBack) {
mCallBack = callBack;
}
public void LoadPic() {
try {
Thread.sleep(1500); // 模仿耗時操作
System.out.println("Load Pic Finish");
if(mCallBack != null) {
mFile = new File(TARGET_PIC);
mCallBack.onChanged(mFile);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void DefaultPic() {
try {
Thread.sleep(1500); // 模仿耗時操作
System.out.println("Load Default Pic Finish");
if(mCallBack != null) {
mFile = new File(DEFAULT_PIC);
mCallBack.onChanged(mFile);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public File getFile() {
return mFile;
}
}
/**
* View 與 Model 的關係完全切割
*/
package ImproveMVC;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
// 專門處理有關 View 的事物,並且 View 觀察 Model
public class View {
private ViewEvent mViewEvent;
private JLabel mLabel;
// 點擊事件接口
public interface ViewEvent {
void StepOne();
void StepTwo();
}
public View() {
// Frame 容器
JFrame frame = new JFrame();
// 根部局
JPanel root = new JPanel();
root.setLayout(new BorderLayout()); // LayoutManager
// JLabel 裝載圖片並至於 View 中心
mLabel = new JLabel();
setImageView(new File(Model.DEFAULT_PIC));
root.add(mLabel, BorderLayout.CENTER); // 加入 View
JPanel btnLayout = new JPanel();
btnLayout.setLayout(new FlowLayout());
JButton Step_1 = new JButton("Step_1");
Step_1.addActionListener(e -> mViewEvent.StepOne());
JButton Step_2 = new JButton("Step_2");
Step_2.addActionListener(e-> mViewEvent.StepTwo());
btnLayout.add(Step_1);
btnLayout.add(Step_2);
root.add(btnLayout, BorderLayout.SOUTH); // 下方
// 將視圖加入容器中
frame.setContentPane(root);
frame.pack();
frame.setTitle("MVC");
frame.setVisible(true);
}
public void setImageView(File file) {
try {
mLabel.setIcon(new ImageIcon(ImageIO.read(file)));
} catch (IOException e) {
e.printStackTrace();
}
}
public void setViewEvent(ViewEvent viewEvent) {
mViewEvent = viewEvent;
}
}
/**
* Controller 負擔會加重一些,但為了隔離 View & Model 是值得的
*/
package ImproveMVC;
import java.io.File;
public class Controller implements Model.ModelCallBack, View.ViewEvent {
// 由 Controller 來控制
private View mView;
private Model mModel;
public Controller(View view, Model model) {
view.setViewEvent(this);
model.setCallBack(this);
mModel = model;
mView = view;
}
@Override
public void onChanged(File file) {
mView.setImageView(file);
}
@Override
public void StepOne() {
mModel.LoadPic();
}
@Override
public void StepTwo() {
mModel.DefaultPic();;
}
}
/**
* 使用者
*/
package ImproveMVC;
public class StartMvc {
public static void main(String[] args) {
new Controller(new View(), new Model());
}
}
```
**--實作結果--**
> 
### Android MVC
* Android 中使用到 MVC 思想的地方非常多,比如常見的 ListView & Adapter,把 ListView 看作 View 層,邏輯處理的 Controller 層則是在 Adapter,獲取的數據就是 Model 層; 把思維放大, Android 中的 ^1.^ Xml 布局、^2.^ View 類看作 View 層,Activiy 就可以看 Controller 層,Model 就是 res 或網路數據
| 名稱 | 代表 | 處理 |
| -------- | -------- | -------- |
| Model 層 | 單純的數據處理、網路操作、以及某些耗時任務的邏輯 | **讓 Controll 層進行搜索、操作** |
| View 層 | Android 的 xml、自定義 View、對 View 的操作 | **實作 view 的接口** |
| Controller 層 | Android 為我們提供的 Activity、fragment 都是一個 Controller,但也可以使用一個類將它分開寫 | **在 Model 層操作,並持有 View 接口的引用,間接更新 View,** |
:::warning
* 使用 View 接口將 Controller 與 Activity 分開比較好
> 在邏輯層 (只有介面的引用) 與 數據層 (純數據) 中的程式,不包含任何顯示部分,**即不會引用 Android Context 類,並支持單元測試,讓責任更清晰化**
:::
* MVC 有分為主動、被動模式,其**是指 Controller 層跟 View 層的互動關係**
* GOF (Gang of Four) 把 MVC 看作是 ++3 種設計模式++ 的組合
1. [**觀察者模式**](https://hackmd.io/@tGUW53vxShaIEpRyCmVZbg/S1XUoCX4L) (主)
2. [策略模式](https://hackmd.io/XCp8xyAdQ5Wd5GTwphFB6A?view)
3. [組合模式](https://hackmd.io/@tGUW53vxShaIEpRyCmVZbg/rkMzK3KD8)
```java=
/**
* Model 層並沒有太大的變化,主要是要注意使用 Handler
* 送訊息給主線程
*/
public class MyModel {
private Context mContext;
private Bitmap mBitmap;
private Handler mHandler;
private ModelListener mModelListener;
public interface ModelListener {
void ModelChange(Bitmap mBitmap);
}
public MyModel(Context context) {
mContext = context;
// 加載預設圖片
mBitmap = BitmapFactory.decodeResource(context.getResources(),
R.mipmap.target);
mHandler = new Handler();
}
public void setModelListener(ModelListener modelListener) {
mModelListener = modelListener;
}
public void loadImage() {
// 耗時操作
new Thread(new Runnable() {
@Override
public void run() {
// 模擬耗時操作
try {
Thread.sleep(1500);
mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.target2);
if(mModelListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mModelListener.ModelChange(mBitmap);
}
});
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public void defaultImage() {
// 耗時操作
new Thread(new Runnable() {
@Override
public void run() {
// 模擬耗時操作
try {
Thread.sleep(1500);
mBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.target);
if(mModelListener != null) {
mHandler.post(new Runnable() {
@Override
public void run() {
mModelListener.ModelChange(mBitmap);
}
});
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public Bitmap getBitmap() {
return mBitmap;
}
}
/**
* View 由 MyView 負擔,並包括 XML (由 inflater 服務分析加載)
*/
public class MyView {
private ImageView mImageView;
private ViewEventListener mViewEventListener;
private final Activity activity;
public interface ViewEventListener {
void loadBtnClick();
void clearBtnClick();
}
public MyView(Activity activity) {
this.activity = activity;
new MyController(this, this);
mImageView = activity.findViewById(R.id.imageView);
activity.findViewById(R.id.load_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mViewEventListener.loadBtnClick();
}
});
activity.findViewById(R.id.clear_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mViewEventListener.clearBtnClick();
}
});
}
public void setViewEventListener(ViewEventListener viewEventListener) {
mViewEventListener = viewEventListener;
}
@Override
public void setImageView(Bitmap bitmap) {
mImageView.setImageBitmap(bitmap);
}
public Context getContext() {
return activity.getContext();
}
}
/**
* Controller 讓 Activity 負責 (當然也可以另外寫開)
* 要注意小心 Controller 責任過重
*/
public class MyController implements MyView.ViewEventListener, MyModel.ModelListener {
public interface MyControllerListener {
void setImageView(Bitmap mBitmap);
}
private final MyControllerListener listener;
private final MyModel model;
public MyController(MyView view, MyControllerListener listener) {
this.listener = listener;
view.setViewEventListener(this);
model = new MyModel(view.getContext());
model.setModelListener(this);
}
@Override
public void loadBtnClick() {
model.loadImage();
}
@Override
public void clearBtnClick() {
model.defaultImage();
}
@Override
public void ModelChange(Bitmap mBitmap) {
if(mBitmap == null) {
return;
}
listener.setImageView(mBitmap);
}
}
```
**--實作結果--**
> 
## Appendix & FAQ
:::info
:::
###### tags: `框架模式`