PS:
開閉原則
定義和背景
開閉原則(Open-Closed Principle, OCP),也稱為開放封閉原則,是面向對象設計中的一個基本原則。該原則強調軟件中的模塊、類或函數應該對擴展開放,對修改封閉。這意味著一個軟件實體應該在不修改現有代碼的基礎上,能夠適應新的變化和需求。核心思想
開閉原則的核心思想是:
- 對擴展開放:當新的需求或變化出現時,可以通過擴展現有代碼來適應新的情況,而不是修改現有的代碼。
- 對修改封閉:一旦類或模塊設計完成,就應該能夠獨立完成其工作,不再對其進行任何修改。(能夠兼容之前的版本)
針對先上線的程序而言
比如后端接口從A改成了B。(前端接口也要修改)
如果后端先上線:前端調用就會出錯,找不到A接口
如果前端先上線,前端調用依然會報錯,找不到B接口。因為此時后端還沒上線。
正確的做法:后端對之前的接口進行兼容,如果兼容則同時存在A和B接口
實現方式
實現開閉原則的方式主要包括:
- 抽象編程:通過面向對象的繼承和多態機制,實現對抽象體的繼承和方法的覆寫,從而實現新的擴展方法。
- 依賴抽象:類依賴于固定的抽象,而不是具體的實現,這樣可以保證類的穩定性。
相關設計原則
開閉原則與其他設計原則密切相關,包括:
- 里氏替換原則:子類對象能夠替代父類對象出現的地方,并且保證原有邏輯行為不變。
- 接口隔離原則:接口調用方和使用者只關心自己相關的接口,不依賴于不需要的接口。
- 依賴反轉原則:高模塊不直接依賴低模塊,而是通過抽象來互相依賴。
- 單一職責原則:一個類或模塊只負責完成一個職責或功能。
一、圖書列表展示功能
1.1 實現分頁功能
提到展示圖書列表,就不得不提到分頁了
分頁時,數據是如何展示的呢
第1頁:顯示1-10 條的數據
第2頁:顯示11-20 條的數據
第3頁:顯示 21-30 條的數據
以此類推…
要想實現這個功能,從數據庫中進行分頁查詢,我們要使用LIMIT 關鍵字,格式為:limit 開始索引每頁顯示的條數(開始索引從0開始)。
select * from book_info where status <> 0 limit 0,10;select * from book_info where status <> 0 limit 10,10;select * from book_info where status <> 0 limit 20,10;
我們發現只有開始索引在改變。每頁顯示的條數是固定的。
開始索引的計算公式:開始索引 = (當前頁碼 - 1) * 每頁顯示條數。
因此:
1.前端發起查詢請求時,需要向服務器端傳遞的參數。
currentPage 當前頁碼 :默認值為1
pageSize 每頁顯示條數 默認值為10
注:
為了項目更好的擴展性,通常不設置固定值,而是是以參數的形式來進行傳遞
擴展性: 軟件系統具備面對未來需求變化而進行擴展的能力。比如當前需求一頁顯示10條,后期需求改為一頁顯示20條,
后端代碼不需要任何修改。
2. 后端響應時,需要響應給前端的數據。
**records :**所查詢到的數據列表(存儲到List集合中)
count:總記錄數(用于告訴前端顯示多少頁,
顯示頁數為:(count+ pageSize -1)/pageSize
翻頁請求和響應部分, 我們通常封裝在兩個對象中
1.1.1 翻頁請求對象PageRequest
創建PageRequest
前端進行請求
1.會請求當前頁 和 每頁顯示的個數。
2.由上面兩個數據計算出offset,用作參數傳遞給SQL語句
package com.qiyangyang.springbook.demos.model;
import lombok.Data;
@Data
public class PageRequest {private Integer currentPage = 1;//當前頁private Integer pageSize = 10;//每頁顯示個數private Integer offset;/*** 從多少條記錄開始查詢* @return*/public Integer getOffset() {return (currentPage-1) * pageSize;}
}
1.1.2 翻頁響應對象
package com.qiyangyang.springbook.demos.model;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.List;/*** 定義一個泛型類,就是,這個地方不定義具體類型* 我們在進行對象的生成的時候,它才有具體的類型。* @param <T>*/
//上面兩個注解用來創建構造方法
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult<T> {/*** 返回的結果也是一個泛型* 不定義具體類型,在對象的創建才會有具體類型*/private List<T> records; //當前頁數據private Integer count; //所有記錄數private PageRequest pageRequest; //小駝峰,用來返回給前端當前頁數// 這里將整個對象告訴result。用來給前端獲取多少頁}
返回結果中, 使用泛型來定義記錄的類型
后端定義參數。
offset(起始序號)和limit(顯示多少條)
MySQL語句
前端根據總記錄數,來顯示分了多少頁。
1.2使用枚舉來處理status/stateCN字段。 (可借閱/不可借閱)
1.創建enums文件夾
2.創建BookStatusEnums類
package com.qiyangyang.springbook.demos.enums;/*** 枚舉類* 可以列舉出來的,是一個有限的個數,我們將他們定義成枚舉類* 方便定義類似* 根據狀態設置描述* if(bookInfo.getStatus() == 1){* bookInfo.setStateCN("可借閱");* } else if (bookInfo.getStatus() == 2) {* bookInfo.setStateCN("不可借閱");* }else {* bookInfo.setStateCN("無效");* }*/ public enum BookStatusEnums {DELETE(0,"無效"), //刪除NORMAL(1,"可借閱"), //有效的FORBIDDEN(2,"不可借閱"), //禁止;private int code;private String desc;BookStatusEnums(int code, String desc) {this.code = code;this.desc = desc;}/*** 我們將這個封裝成一個方法* 根據code獲取描述** @return*/public static BookStatusEnums getDescByCode(int code){switch (code){case 0: return BookStatusEnums.DELETE;case 1: return BookStatusEnums.NORMAL;case 2: return BookStatusEnums.FORBIDDEN;}return BookStatusEnums.DELETE;}public int getCode() {return code;}public String getDesc() {return desc;} }
稍后在業務層我們會對這個方法進行調用
1.3約定前后端交互接口
接口定義:
url:/book/getListByPagecurrentPage=1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
參數:
當前頁數
返回結果:
當前頁的數據+總記錄數(決定前端顯示多少頁數)
我們約定,瀏覽器給服務器發送一個
/book/getListByPage 這樣的 HTTP 請求,
通過 currentPage 參數告訴服務器,當前請求為第幾頁的數據,
后端根據請求參數,返回對應頁的數據
第一頁可以不傳參數, currentPage默認值為1。
1.4實現服務器代碼
1.4.1控制層:
完善 BookController
@RequestMapping("getListByPage")public PageResult<BookInfo> getListByPage(PageRequest pageRequest){log.info("查詢列表信息,pageRequest:{}",pageRequest);if(pageRequest.getCurrentPage()< 1){return null;}//這里返回null。會導致前端不知道是沒有數據為null。還是當前頁錯誤返回null//先不管,后續改進/*** 通過Service來去調用數據庫*/return bookService.getListByPage(pageRequest);}
1.4.2 業務層 :
完善 BookService
public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {/*** 1.查詢記錄的總數* 2.查詢當前頁的數據*/Integer count = bookInfoMapper.count();//bookInfos來接收查詢到的數據List<BookInfo> bookInfos = bookInfoMapper.queryListByPage(pageRequest);for(BookInfo bookInfo : bookInfos){/*** 根據book狀態設置描述(stateCN)*/ bookInfo.setStateCN(BookStatusEnums.getDescByCode(bookInfo.getStatus()).getDesc());}return new PageResult<>(bookInfos,count);}
1.4.3數據層:
完善 BookInfoMapper
/*** 查詢總數* @return*///count(1):返回滿足條件的記錄數(即行數)。count(1) 和 count(*) 基本等效,都是用于統計記錄數。@Select(("select count(1) from book_info where status <> 0"))Integer count();//希望把新添加的圖書放到下面,因此order by id desc降序。@Select("select * from book_info where status <> 0 order by id desc limit #{offset},#{pageSize}")List<BookInfo> queryListByPage(PageRequest pageRequest);
1.5校驗后端
不用傳參也行,因為我們默認currentPage 為1。且pageSize為5。
我們發現返回正確。
總記錄數也返回正確。為46。
我們發現后端接口沒有問題。
1.6實現前端代碼
1.6.1顯示圖書數據的內容
將前端標簽中的內容,也就是
<tbody>//這里的內容我們用findHtml變量拼接并傳送到這個標簽里了</tbody>
我們寫在ajax中使用findHtml變量進行拼接。并用如下方法傳送到這個標簽中。
$("tbody").html(findHtml); //塞到tbody這個標簽里面
success: function (result) {var books = result.records;console.log(books); //如果前端沒有報錯,那么我們打印日志。觀察后端返回結果對不對var findHtml = ""; //用這個變量來拼接HTMLfor (var book of books) {//拼接html。假如后端返回10個tr那么直接for循環拼接在這里面。findHtml//我們用單引號拼接,因為里面有雙引號findHtml += '<tr>';findHtml += '<td><input type="checkbox" name="selectBook" value="' +book.id +'" id="selectBook" class="book-select"></td>';findHtml += "<td>" + book.id + "</td>";findHtml += "<td>" + book.bookName + "</td>";findHtml += "<td>" + book.author + "</td>";findHtml += "<td>" + book.count + "</td>";findHtml += "<td>" + book.price + "</td>";findHtml += "<td>" + book.publish + "</td>";findHtml += "<td>" + book.stateCN + "</td>";findHtml += "<td>";findHtml += '<div class="op">';findHtml +='<a href="book_update.html?bookId=' + book.id + '">修改</a>';findHtml +='<a href="javascript:void(0)" onclick="deleteBook('+book.id +')">刪除</a>';findHtml += "</div>";findHtml += "</td>";findHtml += "</tr>";}$("tbody").html(findHtml); //塞到tbody這個標簽里面?
1.6.2處理翻頁信息
我們需要在前端head標簽中引入jquery 和 paginator
相當于引入插件
<script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript" src="js/bootstrap.min.js"></script><script src="js/jq-paginator.js"></script><div class="demo"><ul id="pageContainer" class="pagination justify-content-center"></ul></div>//處理翻頁信息console.log(result);console.log(result.pageRequest);//翻頁信息$("#pageContainer").jqPaginator({totalCounts: result.count, //總記錄數pageSize: 10, //每頁的個數visiblePages: 5, //可視頁數currentPage: result.pageRequest.currentPage, //當前頁碼first:'<li class="page-item"><a class="page-link">首頁</a></li>',prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一頁</a></li>',next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一頁</a></li>',last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一頁</a></li>',page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}</a></li>',//頁面初始化和頁碼點擊時都會執行onPageChange: function (page, type) {console.log("第" + page + "頁, 類型:" + type);if(type == "change"){location.href = "book_list.html?currentPage="+page;}},});
1.7校驗前后端
成功實現圖書列表顯示以及翻頁功能。
二、修改圖書列表功能
2.1約定前后端交互接口
1.進入修改頁面,需要顯示當前 Id 圖書的信息
[請求]
/book/queryBookByIdbookId=25
[參數]
bookId
[響應]
{
“id”: 25,
“bookName”: “圖書21”,
“author”: “作者2”,
“count”: 999,
“price”: 222.00,
“publish”: “出版社1”,
“status”: 2,
“statusCN”: null,
“createTime”: “2023-09-04T04:01:27.000+00:00”,
“updateTime”: “2023-09-05T03:37:03.000+00:00”
}
根據圖書ID,獲取當前圖書的信息
2.點擊修改按鈕,修改圖書信息
[請求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[參數]
id=1&bookName=圖書1&author=作者1&count=23&price=36&publish=出版社1&status=1
[響應] true/false
我們約定,
瀏覽器給服務器發送一個 /book/updateBook 這樣的HTTP請求,
form表單的形式來 提交數據
服務器返回處理結果,
返回"修改成功"修改圖書成功,否則,返回失敗信息.
2.2實現服務器端代碼
2.2.1控制層:
@RequestMapping("/queryBookById")public BookInfo queryBookById(Integer bookId){log.info("查詢圖書信息,bookId:"+bookId);if(bookId == null || bookId<=0){return new BookInfo();}return bookService.queryBookById(bookId);}@RequestMapping("/updateBook")//先使用boolean類型返回。后續我們還會再進行完善。public boolean upDateBook(BookInfo bookInfo){log.info("修改圖書信息, updateBook{}:",bookInfo);if(!StringUtils.hasLength(bookInfo.getBookName())|| !StringUtils.hasLength(bookInfo.getAuthor())|| !StringUtils.hasLength(bookInfo.getPublish())|| bookInfo.getCount() <=0|| bookInfo.getPrice()==null){return false;}try {Integer result = bookService.updateBook(bookInfo);if(result <= 0){return false;}}catch (Exception e){log.error("更新圖書失敗");return false;}return true;}
2.2.2 業務層 :
public BookInfo queryBookById(Integer bookId) {return bookInfoMapper.queryBookById(bookId);}public Integer updateBook(BookInfo bookInfo) {return bookInfoMapper.updateBook(bookInfo);}
2.2.3數據層:
/*** 根據Id查詢圖書信息* @param id* @return*/@Select("select * from book_info where status <> 0 and id = #{id}")BookInfo queryBookById(Integer id);/*** 根據Id修改圖書信息*/Integer updateBook(BookInfo bookInfo);
根據 Id 修改圖書信息 我們使用的是XML方式實現的SQL
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.qiyangyang.springbook.demos.mapper.BookInfoMapper"><update id="updateBook">update book_info<set><if test="bookName != null">book_name = #{bookName},</if><if test="author != null">author = #{author},</if><if test="count != null">count = #{count},</if><if test="price != null">price = #{price},</if><if test="publish != null">publish = #{publish},</if><if test="status != null">status = #{status},</if></set>where id = #{id}</update> </mapper>
2.3校驗后端
我發現這個接口都校驗成功。
2.3.1校驗queryBookById接口
2.3.2校驗updateBook接口
修改成功!
2.4實現前端代碼
注意:前端傳遞數據的時候記得加上id。
<script type="text/javascript" src="js/jquery.min.js"></script><script>//查詢當前ID圖書$.ajax({type: "get",url: "book/queryBookById"+location.search,success:function(book){if(book!=null){$("#bookId").val(book.id);$("#bookName").val(book.bookName);$("#bookAuthor").val(book.author);$("#bookStock").val(book.count);$("#bookPrice").val(book.price);$("#bookPublisher").val(book.publish);$("#bookStatus").val(book.status);}}});//更新當前Id圖書function update() {$.ajax({type: "get",url: "/book/updateBook",data:$("#updateBook").serialize(),//提交整個表單success:function(result){if(result == true){alert("更新成功");location.href = "book_list.html"}else{alert("更新失敗");}}});}</script>
2.5整體測試
比如我們要將圖書ID為135的圖書
作者 修改為 洋洋
數量 修改為 888
價格 修改為 666
出版社 修改為 人民出版社
可借閱 修改為 不可借閱
修改成功!!!!!
三、邏輯刪除圖書
刪除圖書分為
邏輯刪除(update):
從邏輯上進行刪除,數據并沒有真實刪除
物理刪除(delete語句):
數據真實刪除。
但數據并沒有真實清空,只是數據庫上看不到了。
硬件存儲上還是存在的
刪除并歸檔(操作交為復雜):insert into… select…語句
1.刪除(delete or update)
2.歸檔(把已經刪除的數據存儲下來)
3.1約定前后端交互接口
邏輯刪除的話,
依然是更新邏輯,我們可以直接使用修改圖書的接口
[請求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[參數]
id=1&status=0
[響應]
true / false
3.2實現服務器代碼
3.2.1控制層:
由于我們在upadate接口中只需要傳遞 id 和 status 兩個參數。
因此我們需要修改控制層中的校驗參數的步驟
@RequestMapping("/updateBook")//先使用boolean類型返回。后續我們還會再進行完善。public boolean upDateBook(BookInfo bookInfo){log.info("修改圖書信息, updateBook{}:",bookInfo);if(!StringUtils.hasLength(bookInfo.getBookName())|| !StringUtils.hasLength(bookInfo.getAuthor())|| !StringUtils.hasLength(bookInfo.getPublish())|| bookInfo.getCount() <=0|| bookInfo.getPrice()==null){return false;}try {Integer result = bookService.updateBook(bookInfo);if(result <= 0){return false;}}catch (Exception e){log.error("更新圖書失敗");return false;}return true;}
修改為
@RequestMapping("/updateBook")//先使用boolean類型返回。后續我們還會再進行完善。public boolean updateBook(BookInfo bookInfo){log.info("修改圖書信息, updateBook{}:",bookInfo);if(bookInfo.getId()<0){return false;}try {Integer result = bookService.updateBook(bookInfo);if(result <= 0){return false;}}catch (Exception e){log.error("更新圖書失敗");return false;}return true;}
3.2.2 業務層 :
同之前的updateBook一樣
public Integer updateBook(BookInfo bookInfo) {return bookInfoMapper.updateBook(bookInfo);}
3.2.3數據層:
同之前的update一樣
/*** 根據Id修改圖書信息*/Integer updateBook(BookInfo bookInfo);<update id="updateBook">update book_info<set><if test="bookName != null">book_name = #{bookName},</if><if test="author != null">author = #{author},</if><if test="count != null">count = #{count},</if><if test="price != null">price = #{price},</if><if test="publish != null">publish = #{publish},</if><if test="status != null">status = #{status},</if></set>where id = #{id}</update>
校驗后端接口
134號圖書 status 成功修改為0
3.3實現前端代碼
function deleteBook(id) {//刪除圖書var isDelete = confirm("確認刪除?");if (isDelete) {$.ajax({type: "post",url: "/book/updateBook",data:{id: id,status: 0},success:function(result){if(result == true){alert("刪除成功");location.href = "book_list.html";}}});}}
3.4整體校驗
刪除132號圖書三體
成功刪除!!!!!!!!!!!
四、批量邏輯刪除
4.1定義前后端交互接口
請求:
/book/batchDeleteBook
參數:
響應:
true/false
4.2實現服務器端代碼
4.2.1控制層:
注意加上:注解@RequestParam
@RequestMapping("/batchDelete")public boolean batchDelete(@RequestParam List<Integer> ids){log.info("批量刪除數據,ids:{}",ids);try {Integer result = bookService.batchDelete(ids);if(result <= 0){return false;}}catch (Exception e){log.info("批量刪除失敗,id:{},e:{}", ids, e);return false;}return true;}
4.2.2 業務層 :
public Integer batchDelete(List<Integer> ids) {return bookInfoMapper.batchDelete(ids);}
4.2.3數據層:
注意加上注解:
@Param(“ids”)
Integer batchDelete(@Param("ids") List<Integer> ids);<update id="batchDelete">update book_infoset status = 0where id in<foreach collection="ids" open="(" close=")" item="id" separator=",">#{id}</foreach></update>
4.3后端校驗
后端操作成功
4.4實現前端代碼
function batchDelete() {var isDelete = confirm("確認批量刪除?");if (isDelete) {//獲取復選框的idvar ids = [];$("input:checkbox[name='selectBook']:checked").each(function () {ids.push($(this).val());});console.log(ids);$.ajax({type: "post",url: "/book/batchDelete?ids="+ids,success:function(result){if(result == true){alert("批量刪除成功");location.href = "book_list.html";}else{alert("刪除失敗,請聯系管理員!");}}});}}
4.5整體測試
測試成功!!!!
五、強制登錄
這個功能的實現我們下一篇文章再講哦!!!!
到這里其實這個圖書管理系統的功能就基本實現完成了。
不過對于這個圖書管理系統。
我們沒有進行登錄也可以進行操作。
因此我們下一篇文章會詳細講解強制登錄功能。
并且后續會講到統一功能!!!!!!!!!!!!!!!