iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0
Software Development

深入淺出設計模式 - 使用 C++系列 第 18

[Day 18] 複合模式的王者 — MVC (Model-View-Controller)

  • 分享至 

  • xImage
  •  

介紹

我們通常會一起使用很多種模式,並且在同一個設計解決方案裡面結合它們
深入淺出設計模式, 2nd (p.494)

  • 前面的章節中我們已經大致介紹了 14 種經典的設計模式,今天我們來介紹一種最經典的複合模式 (融合了好幾種設計模式) — MVC
  • MVC (Model-View-Controller) 是一種經典的軟體設計模式,此模式主要由三個主要組件組成:
    1. 模型 (Model): 負責數據的儲存和業務邏輯 (Business Logic)
    2. 視圖 (View): 負責數據的展示 (Render)
    3. 控制器 (Controller): 負責接受用戶輸入,更新模型,再更新視圖 (Flow)
  • MVC 常見於 Web 前後端應用中 (示意圖,轉自 [2])
    • https://ithelp.ithome.com.tw/upload/images/20230930/20138643y0UdiS1qiF.png
      1. 當 Client 輸入 URL,連到網站時,會先遇到的是路徑對照檔 (Route)
      2. 這個路徑對照檔記錄著網站對外開放的 URL/Action 對照,平台框架 (.NET MVC, Rails...) 會根據使用者輸入的網址及參數,比對這個路徑對照表的資料,告訴你應該去找哪個 Controller 上的哪個 Action (或在對照表裡查不到對應的資料,就會 Return HTTP 404 Not Found Error
      3. 在 Controller 上通常會有一個以上的 Action,這些 Action 說穿了就是 Function 而已,這個 Action 會決定要做什麼事 (通常會再把真正要做的操作交由 Model 來實現)
      4. 例: 這個 Action 要查閱 「所有的商品列表」,接著它就會去請 Model 幫忙查資料
      5. Model 本身並不是資料庫,但可以幫忙把 Client 對 Model 的請求轉換成資料庫看得懂的資料庫查詢語言 (SQL)
      6. 透過 SQL,Model 可以跟資料庫取得你想要的資料。
      7. Model 把這包資料 Return 回 Controller/Action
      8. Controller/Action 拿到資料了,可以再做一些對應的格式整理...等,接著傳遞給 View
      9. 資料跟 View 的畫面結合,更新完畫面狀態 (例: 列出新商品),最後透過 Controller 回傳呈現 (Render) 給 Client 看
  • 可以發現 MVC 模式主要能拆解成以下幾種互動分工:
    1. 用戶操作: 用戶通過 GUI 對 Controller 發出請求
    2. 業務邏輯處理: Controller 會命令 Model 進行數據處理
    3. 數據更新: Model 處理完後,將更新的數據發送到 View
    4. 呈現數據: View 會根據更新的數據重新渲染界面

[補充] 設計模式是了解 MVC 的關鍵所在

學習MVC的秘密: 它只是將一些模式擺在一起罷了
深入淺出設計模式, 2nd (p.522)

  • Model 使用觀察者 (Observer) 模式
    • 當 Model 的數據變更時,所有依賴 (觀察) 它的 View 都會自動更新
  • Controller 是 View 的策略 (Strategy) 模式
    • Controller 充當一種策略,讓 View 可以在不同的操作或用戶互動下有不同的行為。換句話說,它允許動態地將 View 的操作委派給不同的方法或模型,達成靈活切換和擴展
  • View 使用組合 (Composite) 模式來實作使用者介面 (UI)
    • 使用者介面通常由多個 UI 元件(如按鈕、文本框、列表等)組成。組合模式允許我們將這些元件以樹狀結構組織起來,使得單個 UI 元件和元件的組合可以被一致地對待

MVC 的優缺點

優點

  • 分離關注點 (Separation of Concerns): 由於 MVC 將數據、用戶界面和業務邏輯分開,各部分獨立開發和測試變得容易
  • 高度模組化 (High Modularity): 模型、視圖和控制器各自都是模組化的,這有助於多人團隊的分工 (可同時開發不同模組)
  • 重用性和可維護性 (Reusability and Maintainability): 由於高度解耦,很容易重用代碼和進行維護
  • 可測試性 (Testability): 可以獨立地測試模型、視圖和控制器,提高了代碼的可測試性

缺點

  • 複雜性 (Complexity): 對於簡單的應用,MVC 可能帶來不必要的複雜性 (Over Design)
  • 效能開銷 (Performance Overhead): 由於多層的交互,可能會有一定的效能開銷,說明如下:
    • 記憶體使用 (Memory Usage): 由於需要維護多個組件和實例,可能會消耗更多的記憶體
    • 多層資料轉換 (Multiple Data Translations): 在模型、視圖和控制器之間的資料交換可能需要多次的資料格式轉換,會導致更多的延遲 (Latency)
  • 狀態管理 (State Management) 不易: 在一些應用中,狀態管理可能變得複雜,尤其是在 Web 應用中

合適應用場景

  • Web Applications: 如 Ruby on Rails, Django, .NET...等
  • 企業級應用 (Enterprise-level Applications): 如 Java EE
  • 桌面應用 (Desktop Applications): 如 Windows Forms, WPF...等
  • 移動應用 (Mobile Applications): 如 Android, iOS 等

範例1: 新增學生編號

在這個例子中,我們將使用 C++ 來實現一個簡單的 MVC 架構,用於新增班級學生的編號:

  • Model: 負責儲存學生編號的資料
  • View: 負責顯示相關資訊
  • Controller: 負責管理 Model 和 View 之間的交互
// Model
class StudentModel {
public:
    void addStudentID(int id) {
        studentIDs.push_back(id);
    }

    std::vector<int> getStudentIDs() {
        return studentIDs;
    }

private:
    std::vector<int> studentIDs;
};

// View
class StudentView {
public:
    void printStudentDetails(std::vector<int> studentIDs) {
        std::cout << "Student IDs: ";
        for (int id : studentIDs) {
            std::cout << id << " ";
        }
        std::cout << std::endl;
    }
};

// Controller
class StudentController {
public:
    StudentController(StudentModel* model, StudentView* view) : model(model), view(view) {}

    void addStudentID(int id) {
        model->addStudentID(id);
        view->printStudentDetails(model->getStudentIDs());
    }

private:
    StudentModel* model;
    StudentView* view;
};

// Client (User)
int main() {
    StudentModel model;
    StudentView view;
    StudentController controller(&model, &view);

    controller.addStudentID(1);
    controller.addStudentID(2);
    controller.addStudentID(3);
}

Output:

Student IDs: 1 
Student IDs: 1 2 
Student IDs: 1 2 3 

範例2: MVC 應用於 C++ QT

https://ithelp.ithome.com.tw/upload/images/20230930/2013864309PnsVaWq0.jpg

組成說明

  • Model
    • QSqlTableModel *model
      • 在這裡,QSqlTableModel 用於與 MySQL 數據庫交互,存取 "books" 表的資料
      • 這個模型儲存了 ISBN、書名、版本、出版社、版權、價格、作者等相關信息
      • 數據的讀取、新增、刪除操作都是通過這個模型來完成
  • View
    • Ui::Widget *ui
      • UI(使用者介面)是用於展示的部分
      • 包含一個表格視圖 (ui->tableView) 來展示數據庫中的資料
      • 其他 UI 元件,如按鈕和文字框,用於新增或刪除資料
  • Controller
    • Widget 類別自身
      • 負責 UI 和模型(QSqlTableModel)之間的互動
      • 通過設定信號-槽(Signal-Slot)機制來控制數據和視圖
        • 當點擊 "新增" 按鈕(ui->addButton)時,會調用 addnew() 方法來新增數據
        • 當點擊 "刪除" 按鈕(ui->deleteButton)時,會調用 remove() 方法來刪除數據
        • 當點擊 "提交" 按鈕(ui->submitButton)時,會調用 save() 方法來儲存變更

檔案: widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QtSql>
#include <QtGui>

namespace Ui {
   class Widget;
}

class Widget : public QWidget
{
   Q_OBJECT

   public:
      explicit Widget(QWidget *parent = 0);
      ~Widget();
      void populateDataItem();

   public slots:
      void addnew();
      void remove();
      void save();

   private:
      Ui::Widget *ui;
      QSqlTableModel *model;
      QSqlDatabase db;

      bool initDatabase();
      void closeDatabase();
};
#endif   // WIDGET_H

檔案: widget.c

#include "widget.h"
#include "ui_widget.h"

#include <QMessageBox>

Widget::Widget(QWidget *parent) : QWidget(parent),
   ui(new Ui::Widget)
{
   ui->setupUi(this);
   initDatabase();
   populateDataItem();
   connect(ui->deleteButton, SIGNAL(clicked(bool)),this,
      SLOT(remove()));
   connect(ui->addButton, SIGNAL(clicked(bool)),this,
      SLOT(addnew()));
   connect(ui->submitButton, SIGNAL(clicked(bool)),this,
      SLOT(save()));
}

void Widget::populateDataItem(){
   model=new QSqlTableModel(0, db);
   model->setTable("books");
   model->setEditStrategy(QSqlTableModel::OnManualSubmit);
   model->select();
   model->setHeaderData(0, Qt::Horizontal, tr("ISBN"));
   model->setHeaderData(1, Qt::Horizontal, tr("Title"));
   model->setHeaderData(2, Qt::Horizontal, tr("Edition"));
   model->setHeaderData(3, Qt::Horizontal, tr("Publisher"));
   model->setHeaderData(4, Qt::Horizontal, tr("Copyright"));
   model->setHeaderData(5, Qt::Horizontal, tr("Price"));
   model->setHeaderData(6, Qt::Horizontal, tr("Authors"));

   ui->tableView->setModel(model);
   ui->tableView->setAlternatingRowColors(true);
}

bool Widget::initDatabase(){
   db=QSqlDatabase::addDatabase("QMYSQL","MyLibrary");
   db.setHostName("localhost");
   db.setDatabaseName("test");
   db.setUserName("user1");
   db.setPassword("secret");
   return db.open();
}

void Widget::closeDatabase(){
   db.close();
}

Widget::~Widget()
{
   closeDatabase();
   delete model;
   delete ui;
}

void Widget::addnew(){
   int row=0;
   model->insertRows(row,1);
   model->setData(model->index(row,0),ui->edIsbn->text());
   model->setData(model->index(row,1),ui->edTitle->text());
   model->setData(model->index(row,2),ui->edEdition->text());
   model->setData(model->index(row,3),ui->edPublisher->text());
   model->setData(model->index(row,4),ui->edCopyright->text());
   model->setData(model->index(row,5),ui->dspinPrice->value());
   model->setData(model->index(row,6),ui->edAuthors->text());
}

void Widget::remove(){
   int row=ui->tableView->currentIndex().row();
   if(QMessageBox::question(0,"Delete", "Record no. "
                            +QString::number(row+1)
                            +" will be deleted. Are you sure?",
                            QMessageBox::No,QMessageBox::Yes)==
                            QMessageBox::Yes){
      model->removeRow(row);
   }
}

void Widget::save(){
   bool flag=model->submitAll();
   if(flag==false)
      QMessageBox::critical(0,"Failed", "cannot save changes.");
   else
      QMessageBox::information(0,"success",
      "changes saved successfully.");
}

檔案: main.cpp (User Demo)

#include "widget.h"
#include <QApplication>
#include <QStyleFactory>

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   QApplication::setStyle(QStyleFactory::create("fusion"));
   Widget w;
   w.show();
   return a.exec();
}

Output:
https://ithelp.ithome.com.tw/upload/images/20230930/20138643waNnpnZlcm.png

[補充]: MVC 架構 Web 專案常使用的物件命名

https://ithelp.ithome.com.tw/upload/images/20230930/20138643RYz4Esae0V.png

  • VO (Value Object):
    • 用於呈現時的資料包裝,並且將實體的資料 (PO) 抽象適合當前程式運作的物件,他可以很單純如同 PO 一樣對應資料庫的屬性,但他也可以包含多個 PO 組裝成一個較為複雜的資料物件
  • DTO (Data Transfer Object):
    • 傳輸用的物件,假設程式向資料庫提取了 PO 資料物件,我必須將我的資料傳往其他系統或是服務時 (跨系統或跨服務的數據交換) 則可用 DTO 進行再包裝,通常 DTO 的資訊都會比 PO 少,經過簡化與過濾
  • BO (Business Object):
    • 用於業務層開發的物件,和 PO/VO 差別在於 BO 包含複雜的業務邏輯,而不再是單純的資料存取或儲存物件
  • PO (Persistent Object):
    • 因為 ORM 框架的誕生所以才有 PO 的概念,可以簡單地將它視為資料庫表格對應的 (Java/C#...) 物件
  • DAO (Data Access Object):
    • 用於 ORM(例: Hibernate) 將資料從資料庫提取的邏輯物件,其中邏輯主要包含如何提取資料庫的資料 (SQL查詢結果) 並將資料包裝成 PO

Reference

  1. https://tw.alphacamp.co/blog/mvc-model-view-controller
  2. https://railsbook.tw/chapters/10-mvc
  3. https://blog.csdn.net/qq_24127015/article/details/105391900
  4. https://www.codeguru.com/cplusplus/implementing-an-mvc-model-with-the-qt-c-framework/
  5. https://hackmd.io/@MonsterLee/HJyAdgRBB

上一篇
[Day 17] 控制與物件的接觸 — 代理模式 (Proxy Pattern)
下一篇
[Day 19] 模式動物園 — 23種模式的總結及補充
系列文
深入淺出設計模式 - 使用 C++37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言