--- 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 所繪製的圖 > ![](https://i.imgur.com/p7MCR1a.png) * 從上圖可以看到 1 個 Controller 可以對應多個 View,其包裹成一個 Tool 並多對多個 Model,而使用者也可以直接操控 Model * Editor 是一個特殊的 Controller,其目的在於當你需要修改一個 View 時,會從 View 獲取一個臨時的 Controller 對象,這個臨時對象就是 `Editor` (可以當成小的 Controller),**MVC 建議使用 Editer 完成對 View & Model 的操作**(但是沒流行 🤣) 下圖是 `MVC-Editor` 結構 > ![](https://i.imgur.com/KgO8rkr.png) ### 經典 MVC * 下圖是經典 MVC 結構,它去除 `Editor` 並簡化為三個層級 > ![](https://i.imgur.com/XzD40az.png) | 區塊 | 介紹 | 重點 | | -------- | -------- | - | | 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 自動刷新) > ![](https://i.imgur.com/xnLsu01.png) ### MVC - 中間層設計 Application Model * 強化隔離 Model,以往 Model 會有受到 Controller (控制)、View (觀察) 的影響,現在**透過一個 `Application Model` 模塊,來加強隔離 `Model`** > 也就是多了一個中間層設計 * **`Application Model` 作用主要是在 Model & View 之間加了一層緩衝,++將複雜的邏輯放置 Application Mode++** > ![](https://i.imgur.com/f3gShi6.png) :::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 架構 > > ![](https://i.imgur.com/tELeJdP.png) * 可以看到 Controller 已經成為一個仲介,而 **View & Model 完全隔離**,**與 MVP 架構已經相當的接近**,但仍規劃在 MVC 架構的範疇可能是因為 View(`JSP`) 仍可以跳過 Controller 去訪問 Model 資料 > 像是 Web 的 `Model 1` 設計,就可以不用 `Controller` 層,說得更清楚一點是 `JSP` 取代了 `Controller & View` 層 > > ![image](https://hackmd.io/_uploads/ByXyJohsp.png) :::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 > ![](https://i.imgur.com/OxE5x6w.png) 添加典型的觀察者,讓 Model 可以通知 View 自動更新 > ![](https://i.imgur.com/hdD3dfP.png) :::danger * 要注意不要讓 Controller 的責任變得過於負重,這會導致 Controller 的職責模糊掉 ::: ## MVC 使用 ### 普通圖片加載 - 無 MVC * Java UI 的設計概念如下,並且程式不區分 MVC 架構 > ![](https://i.imgur.com/KRLEzmi.png) ```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(); } } } ``` **--實作結果--** > ![reference link](https://i.imgur.com/V6oAKof.png) ### 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 事件,其概念圖如下 > ![](https://i.imgur.com/haXTVxZ.png) ```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 ::: > ![](https://i.imgur.com/GZPiRem.png) ```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()); } } ``` **--實作結果--** > ![](https://i.imgur.com/IJZCCJ6.png) ### 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); } } ``` **--實作結果--** > ![](https://i.imgur.com/0RH8ELc.png) ## Appendix & FAQ :::info ::: ###### tags: `框架模式`