MVC 三層架構案例詳細講解

2023-05-17 18:00:35

MVC 三層架構案例詳細講解

@

每博一文案

多讀書,書中有,你對生活,困難所解不開的答案
比如:《殺死一隻是更鳥》中提到的
對應我們:我們努力中考,高考,升本,考研,每天都在努力學習,但是某天突然想到萬一沒有考上的話,那現在的努力又有什麼意義呢?
答案:在《殺死一隻是更鳥》裡有這樣一段話:
> 勇敢是,當你還未開始,你就知道自己會輸,可你依然要去做,而且無論如何都要把它堅持到底,你很少能贏,但有時也會。努力的這個過程本身就是有意義,能夠獲得理想的結果當然很好,但如果失敗了也沒關係。因為你的勇敢,從未辜負你的青春,而黎明的光亮,總有一刻,會照亮穿梭於黑暗之中的自己。況且,你還不一定會輸呢。

1. MVC 概述

MVC開始是存在於桌面程式中的,M是指業務模型,V是指使用者介面,C則是控制器,使用MVC的目的是將M和V的實現程式碼分離,從而使同一個程式可以使用不同的表現形式。比如一批統計資料可以分別用柱狀圖餅圖來表示。C存在的目的則是確保M和V的同步,一旦M改變,V應該同步更新。 [1-2]

模型-檢視-控制器(MVC)是[Xerox PARC](https://baike.baidu.com/item/Xerox PARC/10693263?fromModule=lemma_inlink)在二十世紀八十年代為程式語言Smalltalk-80發明的一種軟體設計模式,已被廣泛使用。後來被推薦為Oracle旗下Sun公司[Java EE](https://baike.baidu.com/item/Java EE/2180381?fromModule=lemma_inlink)平臺的設計模式,並且受到越來越多的使用ColdFusionPHP的開發者的歡迎。模型-檢視-控制器模式是一個有用的工具箱,它有很多好處,但也有一些缺點。

2. MVC設計思想

MVC(Model View Controller)是軟體工程中的一種軟體架構模式,它把軟體系統分為模型檢視控制器三個基本部分。用一種業務邏輯、資料、介面顯示分離的方法組織程式碼,將業務邏輯聚集到一個部件裡面,在改進和個性化客製化介面及使用者互動的同時,不需要重新編寫業務邏輯。

MVC 主要的核心就是:分層:希望專人幹專事,各司其職,職能分工要明確,這樣可以讓程式碼耦合度降低,擴充套件力增強,元件的可複用性增強

MVC 從字面意思我們就可以看到:是分為了三層的,M(Mode 模型),V(View 檢視),C(Controller 控制器)

M即model模型:是指模型表示業務規則。在MVC的三個部件中,模型擁有最多的處理任務。被模型返回的資料是中立的,模型與資料格式無關,這樣一個模型能為多個檢視提供資料,由於應用於模型的程式碼只需寫一次就可以被多個檢視重用,所以減少了程式碼的重複性

V即View檢視:是指使用者看到並與之互動的介面。比如由html元素組成的網頁介面,或者軟體的使用者端介面。MVC的好處之一在於它能為應用程式處理很多不同的檢視。在檢視中其實沒有真正的處理髮生,它只是作為一種輸出資料並允許使用者操作的方式。

C即controller控制器:是指控制器接受使用者的輸入並呼叫模型和檢視去完成使用者的需求,控制器本身不輸出任何東西和做任何處理。它只是接收請求並決定呼叫哪個模型構件去處理請求,然後再確定用哪個檢視來顯示返回的資料。

M(Model :資料/業務) V (View :檢視/展示) C (Controller : 控制層)

C(是核心,是控制器,是司令官)

M(處理業務/處理資料的一個祕書)

V(負責頁面展示的一個祕書)

MVC(一個司令官,排程兩個祕書,去做這件事),僅僅只做事務上的排程,而不做其他的操作

優點:

  1. 耦合性低,方便維護,可以利於分工共同作業
  2. 重用性高

缺點:

  1. 使得專案架構變得複雜,對開發人員要求高

3. 三層架構

三層架構(3-tier architecture) 通常意義上的三層架構就是將整個業務應用劃分為:介面層[表示層](User Interface layer)、業務邏輯層(Business Logic Layer)、資料存取層(Data access layer)。

區分層次的目的即為了「高內聚低耦合」 的思想。在軟體體系架構設計中,分層式結構是最常見,也是最重要的一種結構。


三層架構每層之間的邏輯關係:

三層架構的優點

  1. 開發人員可以只關注整個結構中的其中某一層;
  2. 可維護性高,可延伸性高
  3. 可以降低層與層之間的依賴;
  4. 有利於標準化;
  5. 利於各層邏輯的複用

三層架構的缺點:

  1. 降低了系統的效能。如果不採用分層式結構,很多業務可以直接造訪資料庫,以此獲取相應的資料,如今卻必須通過中間層來完成
  2. 有時會導致級聯的修改,這種修改尤其體現在自上而下的方向。如果在表示層中需要增加一個功能,為保證其設計符合分層式結構,可能需要在相應的業務邏輯層和資料存取層中都增加相應的程式碼
  3. 增加了開發成本

4. MVC 與 三層架構的關係:

MVC的也可以被說成是 MVC三層架構,說白了,它們其實都是一個東西,只是在一些細節上有稍微的不同,大致設計思想都是一樣的:「高內聚,低耦合」。

其實,無論是MVC還是三層架構,都是一種規範,都是奔著"高內聚,低耦合"的思想來設計的。三層中的UI和Servlet來分別對應MVC中的View和Controller,業務邏輯層是來組合資料存取層的原子性功能的。

5. 案例舉例:使用者賬戶轉賬

如下我們,實現一個使用者賬戶轉賬操作的一個案例:

準備工作:建立表,建立資料



CREATE DATABASE mvc;

USE mvc;

SHOW TABLES;

CREATE TABLE t_act (
   id BIGINT PRIMARY KEY AUTO_INCREMENT,
   actno VARCHAR(255) NOT NULL,
   balance DECIMAL(10,2) 
);



INSERT INTO t_act(actno,balance)
VALUES('act001',50000.00),('act002',0.00);

SELECT *
FROM t_act;

5.1 M(Model :資料/業務處理層)

javaBean :Account 封裝資料

賬戶實體類,封裝賬戶資訊的

  • 一般是一張表一個。
  • pojo 物件
  • 有的人也會把這種專門封裝資料的物件,稱為:"bean物件" (javabean物件,咖啡豆)
  • 有的人也會把這種專門封裝資料的物件,稱為領域模型物件,domain物件
  • 不同的程式設計師不同的習慣
package com.RainbowSea.bank.mvc;

import java.io.Serializable;
import java.util.Objects;


/**
 * 賬戶實體類,封裝賬戶資訊的
 * 一般是一張表一個。
 * pojo 物件
 * 有的人也會把這種專門封裝資料的物件,稱為:"bean物件" (javabean物件,咖啡豆)
 * 有的人也會把這種專門封裝資料的物件,稱為領域模型物件,domain物件
 * 不同的程式設計師不同的習慣。
 */
public class Account implements Serializable {  // 這種普通的簡單的物件被成為pojo物件
    // 注意我們這裡定義的資料型別,使用參照資料型別
    // 因為我們資料庫中可能存在 null 值,而基本資料型別是不可以儲存 null值的

    private Long id = null;  // id
    private String actno;  // 賬號
    private Double balance; // 餘額

    // 反序列化
    private static final long serialVersionUID = 1L;

    public Account() {
    }


    public Account(Long id, String actno, Double balance) {
        this.id = id;
        this.actno = actno;
        this.balance = balance;
    }


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Account)) return false;
        Account account = (Account) o;
        return Objects.equals(getId(), account.getId()) && Objects.equals(getActno(), account.getActno()) && Objects.equals(getBalance(), account.getBalance());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getActno(), getBalance());
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", actno='" + actno + '\'' +
                ", balance=" + balance +
                '}';
    }
}

DB連線資料庫的工具:

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mvc
user=root
password=MySQL
package com.RainbowSea.bank.utils;


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ResourceBundle;

public class DBUtil {

    // resourceBundle 只能讀取到 properties 字尾的檔案,注意不要加檔案字尾名
    private static ResourceBundle resourceBundle = ResourceBundle.getBundle("resources/jdbc");
    private static String driver = resourceBundle.getString("driver");
    private static String url = resourceBundle.getString("url");
    private static String user = resourceBundle.getString("user");
    private static String password = resourceBundle.getString("password");


    // DBUtil 類載入註冊驅動
    static {
        try {
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
           e.printStackTrace();
        }

    }


    // 將構造器私有化,不讓建立物件,因為工具類中的方法都是靜態的,不需要建立物件
    // 為了防止建立物件,故將構造方法私有化
    private DBUtil() {

    }

    /**
     * 這裡沒有使用資料庫連線池,直接建立連線物件
     */
    public static Connection getConnection()  {
        Connection connection = null;
        try {
            connection = DriverManager.getConnection(url, user, password);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return  connection;
    }


    /**
     * 資源的關閉
     * 最後使用的最先關閉,逐個關閉,防止存在沒有關閉的
     */
    public static void close(Connection connection , PreparedStatement preparedStatement, ResultSet resultSet) {

        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }


        if (preparedStatement!=null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }


        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

對應Account資料表的DAO操作工具類

AccountDao 是負責Account 資料的增上改查

什麼是DAO ?

  • Data Access Object (資料存取物件)
  • DAO實際上是一種設計模式,屬於 JavaEE的設計模式之一,不是 23種設計模式
  • DAO只負責資料庫表的CRUD ,沒有任何業務邏輯在裡面
  • 沒有任何業務邏輯,只負責表中資料增上改查的物件,有一個特俗的稱謂:DAO物件

為什麼叫做 AccountDao 呢?

  • 這是因為DAO是專門處理t_act 這張表的
  • 如果處理t_act 表的話,可以叫做:UserDao
  • 如果處理t-student表的話,可以叫做 StudentDao

主要定義如下:增刪改查方法()

int insert() ;
int deleteByActno();
int update() ;
Account selectByActno();
List<Account> selectAll();
package com.RainbowSea.bank.mvc;


import com.RainbowSea.bank.utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;

/**
 * AccountDao 是負責Account 資料的增上改查
 * <p>
 * 1. 什麼是DAO ?
 * Data Access Object (資料存取物件)
 * 2. DAO實際上是一種設計模式,屬於 JavaEE的設計模式之一,不是 23種設計模式
 * 3.DAO只負責資料庫表的CRUD ,沒有任何業務邏輯在裡面
 * 4.沒有任何業務邏輯,只負責表中資料增上改查的物件,有一個特俗的稱謂:DAO物件
 * 5. 為什麼叫做 AccountDao 呢?
 * 這是因為DAO是專門處理t_act 這張表的
 * 如果處理t_act 表的話,可以叫做:UserDao
 * 如果處理t-student表的話,可以叫做 StudentDao
 * <p>
 * int insert() ;
 * int deleteByActno();
 * int update() ;
 * Account selectByActno();
 * List<Account> selectAll();
 */
public class AccountDao {


    /**
     * 插入資料
     *
     * @param account
     * @return
     */
    public int insert(Account account) {
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "insert into t_act(actno,balance) values(?,?)";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, account.getActno());
            preparedStatement.setDouble(2, account.getBalance());
            count = preparedStatement.executeUpdate();


        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, null);
        }


        return count;

    }


    /**
     * 通過Id刪除資料
     *
     * @param id
     * @return
     */
    public int deleteById(String id) {
        Connection connection = DBUtil.getConnection();
        int count = 0;
        PreparedStatement preparedStatement = null;
        try {
            String sql = "delete from t_act where id = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, id);
            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, null);
        }

        return count;

    }


    /**
     * 更新資料
     *
     * @param account
     * @return
     */
    public int update(Account account) {
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        int count = 0;

        try {
            String sql = "update t_act set balance = ?, actno = ? where id = ?";
            preparedStatement = connection.prepareStatement(sql);

            //注意設定的 set型別要保持一致。
            preparedStatement.setDouble(1, account.getBalance());
            preparedStatement.setString(2, account.getActno());
            preparedStatement.setLong(3, account.getId());

            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, null);
        }

        return count;
    }


    /**
     * 通過 actno 查詢賬戶資訊
     *
     * @param actno
     * @return
     */
    public Account selectByActno(String actno) {
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        Account account = new Account();

        try {
            String sql = "select id,actno,balance from t_act where actno = ?";
            preparedStatement = connection.prepareStatement(sql);

            //注意設定的 set型別要保持一致。
            preparedStatement.setString(1, actno);

           resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                Long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                // 將結果集封裝到java 物件中
                account.setActno(actno);
                account.setId(id);
                account.setBalance(balance);

            }

        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, resultSet);
        }

        return account;
    }


    /**
     * 查詢所有的賬戶資訊
     *
     * @return
     */
    public List<Account> selectAll() {
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        List<Account> list = null;

        try {
            String sql = "select id,actno,balance from t_act";
            preparedStatement = connection.prepareStatement(sql);

            resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                String actno = resultSet.getString("actno");
                Long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                // 將結果集封裝到java 物件中
                Account account = new Account(id,actno,balance);

                // 新增到List集合當中
                list.add(account);

            }

        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, resultSet);
        }

        return list;
    }


}

對指定的資料表的資料進行service 業務邏輯處理操作:

service 翻譯為:業務。

  • AccountService 專門處理Account業務的一個類
  • 在該類中應該編寫純業務程式碼。(只專注域業務處理,不寫別的,不和其他程式碼混合在一塊)
  • 只希望專注業務,能夠將業務完美實現,少量bug.
  • 業務類一般起名:XXXService,XXXBiz...
package com.RainbowSea.bank.mvc;


/**
 * service 翻譯為:業務。
 * AccountService 專門處理Account業務的一個類
 * 在該類中應該編寫純業務程式碼。(只專注域業務處理,不寫別的,不和其他程式碼混合在一塊)
 * 只希望專注業務,能夠將業務完美實現,少量bug.
 * <p>
 * 業務類一般起名:XXXService,XXXBiz...
 */
public class AccountService {

    // 這裡的方法起名,一定要體現出,你要處理的是什麼業務:
    // 我們要提供一個能夠實現轉賬的業務的方法(一個業務對應一個方法)
    // 比如:UserService StudentService OrderService

    // 處理Account 轉賬業務的增刪改查的Dao
    private AccountDao accountDao = new AccountDao();

    /**
     * 完成轉賬的業務邏輯
     *
     * @param fromActno 轉出賬號
     * @param toActno   轉入賬號
     * @param money     轉賬金額
     */
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
        // 查詢餘額是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {
            throw new MoneyNotEnoughException("對不起,餘額不足");
        }

        // 程式到這裡說明餘額充足
        Account toAct = accountDao.selectByActno(toActno);

        // 修改金額,先從記憶體上修改,再從硬碟上修改
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);


        // 從硬碟資料庫上修改
        int count = accountDao.update(fromAct);
        count += accountDao.update(toAct);

        if(count != 2) {
            throw new AppException("賬戶轉賬異常,請聯絡管理員");
        }

    }
}

例外處理類:

package com.RainbowSea.bank.mvc;


/**
 * 餘額不足異常
 */
public class AppException extends Exception{

        public AppException() {

        }

        public AppException(String msg) {
            super(msg);
        }

}

package com.RainbowSea.bank.mvc;


/**
 * 餘額不足異常
 */
public class MoneyNotEnoughException extends Exception{
    public MoneyNotEnoughException() {

    }

    public MoneyNotEnoughException(String msg) {
        super(msg);
    }
}

5.2 C (Controller : 控制層)

僅僅負責排程 M業務處理層,V檢視顯示層,而不做其他操作。

package com.RainbowSea.bank.mvc;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;


/**
 * 賬戶小程式
 * AccountServlet 是一個司令官,他負責排程其他元件來完成任務。
 *
 */
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet { // AccountServlet 作為一個 Controller 司令官

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
            IOException {

        // 獲取資料
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));

        // 呼叫業務方法處理業務(排程Model處理業務,其中是對應資料表的 CRUD操作)
        AccountService accountService = new AccountService();
        try {
            accountService.transfer(fromActno,toActno,money);
            // 執行到這裡說明,成功了,
            // 展示處理結束(排程 View 做頁面展示)

            response.sendRedirect(request.getContextPath()+"/success.jsp");
        } catch (MoneyNotEnoughException e) {
            // 執行到種類,說明失敗了,(餘額不足
            // 展示處理結束(排程 View 做頁面展示)
            response.sendRedirect(request.getContextPath()+"/error.jsp");

        } catch (AppException e) {
            // 執行到種類,說明失敗了,轉賬異常
            // 展示處理結束(排程 View 做頁面展示)
            response.sendRedirect(request.getContextPath()+"/error.jsp");

        }

        // 頁面的展示 (排程View做頁面展示)


    }
}

5.3 V (View :檢視/展示)

index.jsp 轉賬頁面:


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <title>銀行賬號轉賬</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/transfer" method="post">
  轉出賬戶: <input type="text" name="fromActno" /> <br>
  轉入賬戶: <input type="text" name="toActno" /> <br>
  轉賬金額: <input type="text" name="money" /><br>
  <input type="submit" value="轉賬" />
</form>
</body>
</html>

success轉賬成功的頁面顯示:


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>轉賬成功</title>
</head>
<body>

<h3>轉賬成功</h3>
</body>
</html>

error 轉賬失敗的頁面顯示:


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>轉賬失敗</title>
</head>
<body>
<h3>轉賬失敗</h3>
</body>
</html>

雖然上述:程式碼成功實現的了使用者轉賬的操作,但是並沒有進行事務的處理。

如下是運用 TreadLocal 進行事務的處理: