從零開始實現放置遊戲(八)——實現掛機戰鬥(6)程式碼重構

丶謙信發表於2019-07-08

  前幾張,我們主要實現了升級經驗、人物等級屬性、地圖、地圖怪物,這四種配置的增刪查改以及Excel匯入功能。我們主要以地圖怪物為例,因此在文章末尾提供的原始碼中只實現了地圖怪物這部分的邏輯功能。

  如果你照貓畫虎,把4種配置功能的邏輯全部實現的話,就會發現,增刪查改的程式碼基本相同,除了SQL語句和模型物件不同,其他地方變化不大。

  本章我們利用泛型模板,對整個系統就行重構。在重構結束後,你就會發現寫程式碼簡直就是特喵的藝術

後端重構

idlewow-core

  我們從最底層開始,首先重構位於core模組中的資料訪問層。目前看來,基本上所有的模型物件,都應包含增刪查改、批量新增、列表查詢這些基本方法。那我們把這些方法抽象到一個單獨的Mapper和Manager裡。

  新建com.idlewow.common包,再該包下新建介面類BaseMapper:

package com.idlewow.common;

import com.idlewow.common.model.QueryParam;

import java.util.List;

public interface BaseMapper<T> {
    /**
     * 新增記錄
     * @param t
     */
    int insert(T t);

    /**
     * 批量新增記錄
     * @param list
     * @return
     */
    int batchInsert(List<T> list);

    /**
     * 更新記錄
     * @param t
     */
    int update(T t);

    /**
     * 刪除記錄
     * @param id
     */
    int delete(String id);

    /**
     * 根據id查詢
     * @param id
     * @return
     */
    T find(String id);

    /**
     * 根據條件查詢總數
     * @param queryParam
     * @return
     */
    int count(QueryParam queryParam);

    /**
     * 根據條件查詢列表
     * @param queryParam
     * @return
     */
    List<T> list(QueryParam queryParam);
}
BaseMapper.java

  再在該包下,新建一個抽象類BaseManager,程式碼如下:

package com.idlewow.common;

import com.idlewow.common.model.PageList;
import com.idlewow.common.model.QueryParam;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

public abstract class BaseManager<T> {
    @Autowired
    public BaseMapper<T> baseMapper;

    public void insert(T t) {
        int effected = baseMapper.insert(t);
        if (effected == 0) {
            throw new RuntimeException("sql effected 0 rows");
        }
    }

    public void batchInsert(List<T> list) {
        int splitSize = 100;
        int index = 0;
        int total = list.size();
        while (index <= total) {
            int end = index + splitSize;
            if (end > total) {
                end = total;
            }

            List<T> sublist = list.subList(index, end);
            int effected = baseMapper.batchInsert(sublist);
            if (effected == 0) {
                throw new RuntimeException("sql effected 0 rows");
            }

            index += splitSize;
        }
    }

    public void update(T t) {
        int effected = baseMapper.update(t);
        if (effected == 0) {
            throw new RuntimeException("sql effected 0 rows");
        }
    }

    public void delete(String id) {
        int effected = baseMapper.delete(id);
        if (effected == 0) {
            throw new RuntimeException("sql effected 0 rows");
        }
    }

    public T find(String id) {
        T t = (T)baseMapper.find(id);
        return t;
    }

    public PageList<T> list(QueryParam queryParam) {
        PageList<T> pageList = new PageList<>();
        int count = baseMapper.count(queryParam);
        List<T> list = baseMapper.list(queryParam);
        pageList.setTotalCount(count);
        pageList.setData(list);
        pageList.setPageParam(queryParam.getPageParam());
        return pageList;
    }
}
BaseManager.java

  還是以地圖怪物為例,我們重構MapMobMapper和MapMobManager,只要讓他們繼承BaseMapper和BaseManager即可,程式碼如下:

package com.idlewow.mob.mapper;

import com.idlewow.common.BaseMapper;
import com.idlewow.mob.model.MapMob;

public interface MapMobMapper extends BaseMapper<MapMob> {
}
MapMobMapper.java
package com.idlewow.mob.manager;

import com.idlewow.common.BaseManager;
import com.idlewow.mob.model.MapMob;
import org.springframework.stereotype.Component;

@Component
public class MapMobManager extends BaseManager<MapMob> {
}
MapMobManager.java

  重構後的Mapper和Manager直接繼承基類的增刪查改方法,無需在各個業務中一遍又一遍的書寫重複程式碼。

idlewow-rms

  在rms模組中,主要對controller中的重複程式碼進行重構。在前幾章,我們抽象出過一個BaseController,在裡面實現了一些最基礎的方法。這裡,我們再抽象出一個CrudController來實現資料的增刪查該;在CrudController的基礎上,再抽象出一個ExcelController來實現Excel的批量匯入。

  在com.idlewow.rms.controlelr包下新建抽象類CrudContoller,程式碼如下:

package com.idlewow.rms.controller;

import com.idlewow.common.BaseManager;
import com.idlewow.common.model.BaseModel;
import com.idlewow.common.model.CommonResult;
import com.idlewow.common.model.PageList;
import com.idlewow.common.model.QueryParam;
import com.idlewow.util.validation.ValidateGroup;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

public abstract class CrudController<T extends BaseModel, Q extends QueryParam> extends BaseController {
    private final String path = this.getClass().getAnnotation(RequestMapping.class).value()[0];

    @Autowired
    BaseManager<T> baseManager;

    @RequestMapping("/list")
    public Object list() {
        return this.path + "/list";
    }

    @ResponseBody
    @RequestMapping(value = "/list", method = RequestMethod.POST)
    public Object list(@RequestParam(value = "page", defaultValue = "1") int pageIndex, @RequestParam(value = "limit", defaultValue = "10") int pageSize, Q q) {
        q.setPage(pageIndex, pageSize);
        PageList<T> pageList = baseManager.list(q);
        return this.parseTable(pageList);
    }

    @RequestMapping("/add")
    public Object add() {
        return this.path + "/add";
    }

    @ResponseBody
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public Object add(@RequestBody T t) {
        try {
            CommonResult commonResult = this.validate(t, ValidateGroup.Create.class);
            if (!commonResult.isSuccess())
                return commonResult;

            t.setCreateUser(this.currentUserName());
            baseManager.insert(t);
            return CommonResult.success();
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            return CommonResult.fail();
        }
    }

    @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
    public Object edit(@PathVariable String id, Model model) {
        T t = baseManager.find(id);
        model.addAttribute(t);
        return this.path + "/edit";
    }

    @ResponseBody
    @RequestMapping(value = "/edit/{id}", method = RequestMethod.POST)
    public Object edit(@PathVariable String id, @RequestBody T t) {
        try {
            if (!id.equals(t.getId())) {
                return CommonResult.fail("id不一致");
            }

            CommonResult commonResult = this.validate(t, ValidateGroup.Update.class);
            if (!commonResult.isSuccess())
                return commonResult;

            t.setUpdateUser(this.currentUserName());
            baseManager.update(t);
            return CommonResult.success();
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            return CommonResult.fail();
        }
    }

    @ResponseBody
    @RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
    public Object delete(@PathVariable String id) {
        try {
            baseManager.delete(id);
            return CommonResult.success();
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            return CommonResult.fail();
        }
    }
}
CrudController.java

  這個CrudController起到了一個模板的作用,可以說非常的精髓。首先,利用泛型約束,解決了不同業務資料模型、查詢引數不同的問題。然後,通過反射獲取不同業務controller的Url對映,解決了不同業務跳轉頁面路徑不同的問題。具體業務的controller直接繼承此類,無需再寫任何程式碼,即可實現增刪查改。

  下面我們再實現Excel匯入功能的模板類,在該包內新建一個抽象類ExcelController,程式碼如下:

package com.idlewow.rms.controller;

import com.idlewow.common.model.BaseModel;
import com.idlewow.common.model.CommonResult;
import com.idlewow.common.model.QueryParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.util.Iterator;
import java.util.List;

public abstract class ExcelController<T extends BaseModel, Q extends QueryParam> extends CrudController<T, Q> {
    @ResponseBody
    @RequestMapping(value = "/importExcel", method = RequestMethod.POST)
    public Object importExcel(HttpServletRequest request) {
        try {
            ServletContext servletContext = request.getServletContext();
            String uploadPath = servletContext.getRealPath("/upload");
            File dir = new File(uploadPath);
            if (!dir.exists()) {
                dir.mkdir();
            }

            CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(servletContext);
            if (multipartResolver.isMultipart(request)) {
                MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request;
                Iterator<String> iter = multiRequest.getFileNames();
                while (iter.hasNext()) {
                    MultipartFile file = multiRequest.getFile(iter.next());
                    if (file.getSize() > 0) {
                        String fileName = file.getOriginalFilename();
                        String extension = fileName.substring(fileName.lastIndexOf("."));
                        if (!extension.toLowerCase().equals(".xls") && !extension.toLowerCase().equals(".xlsx")) {
                            throw new Exception("不支援的文件格式!請上傳.xls或.xlsx格式的文件!");
                        }

                        String destFileName = fileName + "_" + System.currentTimeMillis() + extension;
                        File destFile = new File(uploadPath, destFileName);
                        file.transferTo(destFile);
                        List<T> dataList = this.loadExcelData(destFile.getPath());
                        this.saveExcelData(dataList);
                        if (destFile.exists() && !destFile.delete()) {
                            logger.error("刪除臨時檔案失敗!" + destFile.getAbsolutePath());
                        }
                    }
                }
            }

            return CommonResult.success();
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            return CommonResult.fail();
        }
    }

    protected abstract List<T> loadExcelData(String excelPath) throws Exception;

    protected void saveExcelData(List<T> dataList) {
        this.baseManager.batchInsert(dataList);
    }
}
ExcelController.java

  在這個類中,我們將Excel匯入功能分解成3個方法。importExcel,對應前端點選事件,儲存上傳的臨時檔案;saveExcelData,儲存解析出的資料,即呼叫mapper的批量新增方法;這兩個方法都是通用的,直接在ExcelController中實現即可。只有loadExcelData,解析Excel資料這個方法,不同業務的實現不同,我們把它定義成抽象方法,等待各個業務自己實現。

  好了,基類已經定義好了,我們讓MapMobController繼承ExcelController即可,程式碼如下:

package com.idlewow.rms.controller;

import com.idlewow.common.constant.DataDict;
import com.idlewow.map.model.WowMap;
import com.idlewow.mob.model.MapMob;
import com.idlewow.query.model.MapMobQueryParam;
import com.idlewow.util.poi.PoiUtil;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.ehcache.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;


@Controller
@RequestMapping("/manage/map_mob")
public class MapMobController extends ExcelController<MapMob, MapMobQueryParam> {
    @Autowired
    protected Cache mapCache;

    protected List<MapMob> loadExcelData(String excelPath) throws Exception {
        FileInputStream fileInputStream = new FileInputStream(excelPath);
        XSSFWorkbook workbook = new XSSFWorkbook(fileInputStream);
        Sheet sheet = workbook.getSheet("怪物");
        List<MapMob> mapMobList = new ArrayList<>();
        // 處理當前頁,迴圈讀取每一行
        String createUser = this.currentUserName();
        for (int rowNum = 2; rowNum <= sheet.getLastRowNum(); rowNum++) {
            XSSFRow row = (XSSFRow) sheet.getRow(rowNum);
            String mapName = PoiUtil.getCellValue(row.getCell(1));
            String mobName = PoiUtil.getCellValue(row.getCell(2));
            String faction = PoiUtil.getCellValue(row.getCell(3));
            String mobClass = PoiUtil.getCellValue(row.getCell(4));
            String mobType = PoiUtil.getCellValue(row.getCell(5));
            Integer level = Integer.valueOf(PoiUtil.getCellValue(row.getCell(6)));
            Integer hp = Integer.valueOf(PoiUtil.getCellValue(row.getCell(7)));
            Integer damage = Integer.valueOf(PoiUtil.getCellValue(row.getCell(8)));
            Integer amour = Integer.valueOf(PoiUtil.getCellValue(row.getCell(9)));
            WowMap wowMap = (WowMap)mapCache.get(mapName);
            MapMob mapMob = new MapMob();
            mapMob.setMapId(wowMap.getId());
            mapMob.setMapName(wowMap.getName());
            mapMob.setName(mobName);
            mapMob.setFaction(DataDict.Faction.getByDesc(faction).getCode());
            mapMob.setMobClass(DataDict.MobClass.getByDesc(mobClass).getCode());
            mapMob.setMobType(DataDict.MobType.getByDesc(mobType).getCode());
            mapMob.setLevel(level);
            mapMob.setHp(hp);
            mapMob.setDamage(damage);
            mapMob.setAmour(amour);
            mapMob.setCreateUser(createUser);
            mapMobList.add(mapMob);
        }

        fileInputStream.close();
        return mapMobList;
    }
}
MapMobController.java

  現在業務Controller裡,只有一個獨立實現的loadExcelData方法,再也不用重複書寫增刪查改了。(注意:這個類裡多了個快取物件mapCache,是我用來快取地圖資料的。pom中新增了對應包,具體可在原始碼中檢視。用法比較簡單,可搜EhCache。)

前端重構

  除了後端程式碼冗餘外,其實很容易發現,前端的程式碼重複部分也很多,尤其是寫ajax請求的部分,其實每次變化的只有url地址,或者請求引數等。我們直接在js裡定義一個類來實現增刪查改等這些ajax請求的通用部分。

  在/webapp/js/helper.js中,定義一個類CRUD,程式碼如下:

…………
…………

var CRUD = function () {
};

CRUD.prototype = {
    list: function (cols, url) {
        var table = layui.table;
        table.render({
            elem: '#datatable'
            , url: url
            , method: 'post'
            , cellMinWidth: 80
            , cols: cols
            , page: {
                layout: ['limit', 'count', 'prev', 'page', 'next', 'skip'] //自定義分頁佈局
                , limits: [10, 20, 30, 40, 50]
                , groups: 3 //只顯示 1 個連續頁碼
                , first: '首頁'
                , last: '尾頁'
            }
        });
    },
    upload: function (url, extension) {
        layui.upload.render({
            elem: '#btnSelectFile',
            url: url,
            accept: 'file',
            exts: extension,
            auto: false,
            bindAction: '#btnImport',
            done: function (result) {
                if (result.code === 1) {
                    layer.alert(result.message, {icon: 6},
                        function () {
                            layui.layer.closeAll();
                            layui.table.reload('datatable');
                        });
                } else {
                    layer.alert(result.message, {icon: 5});
                }
            }
        });
    },
    search: function (data) {
        var table = layui.table;
        table.reload('datatable', {
            where: data,
            page: {
                curr: 1
            }
        });
    },
    add: function (url) {
        var form = layui.form;
        form.on('submit(add)',
            function (data) {
                $.ajax({
                    url: url,
                    type: 'post',
                    contentType: "application/json; charset=utf-8",
                    data: JSON.stringify(data.field),
                    success: function (result) {
                        if (result.code === 1) {
                            layer.alert(result.message, {icon: 6},
                                function () {
                                    xadmin.close();
                                    xadmin.father_reload();
                                });
                        } else {
                            layer.alert(result.message, {icon: 5});
                        }
                    },
                    error: function () {
                        layer.alert("請求失敗", {icon: 5});
                    }
                });
            });
    },
    edit: function (url) {
        var form = layui.form;
        form.on('submit(edit)',
            function (data) {
                $.ajax({
                    url: url + '/' + data.field.id,
                    type: 'post',
                    contentType: "application/json; charset=utf-8",
                    data: JSON.stringify(data.field),
                    success: function (result) {
                        if (result.code === 1) {
                            layer.alert(result.message, {icon: 6},
                                function () {
                                    xadmin.close();
                                    xadmin.father_reload();
                                });
                        } else {
                            layer.alert(result.message, {icon: 5});
                        }
                    },
                    error: function () {
                        layer.alert("請求失敗", {icon: 5});
                    }
                });
            });
    },
    remove: function (obj, url) {
        layer.confirm('確認要刪除嗎?', function () {
            $.ajax({
                url: url,
                type: 'post',
                success: function (result) {
                    if (result.code === 1) {
                        $(obj).parents("tr").remove();
                        layer.msg('刪除成功', {icon: 1, time: 1000});
                    } else {
                        layer.alert("刪除失敗", {icon: 5});
                    }
                },
                error: function () {
                    layer.alert("請求失敗", {icon: 5});
                }
            });
        });
    }
};

window.crud = new CRUD();

…………
…………
Helper.js

  然後,在/webapp/js/wow/map_mob中,修改add.js, edit.js 和 list.js 如下:

layui.use(['form', 'layer'],
    function () {
        var form = layui.form;
        form.verify({});
        crud.add('/manage/map_mob/add');
    });
add.js
layui.use(['form', 'layer'],
    function () {
        var form = layui.form;
        form.render();
        form.verify({});
        crud.edit('/manage/map_mob/edit/');
    });
edit.js
layui.use(['upload', 'table', 'form'], function () {
    var cols = [[
        {field: 'id', width: 50, title: 'id'}
        , {field: 'name', title: '怪物名稱'}
        , {field: 'mapName', title: '地圖名稱'}
        , {
            field: 'faction', title: '陣營', templet: function (d) {
                return enumUtil.faction(d.faction);
            }
        }
        , {
            field: 'mobClass', title: '怪物種類', templet: function (d) {
                return enumUtil.mobClass(d.mobClass);
            }
        }
        , {
            field: 'mobType', title: '怪物型別', templet: function (d) {
                return enumUtil.mobType(d.mobType);
            }
        }
        , {field: 'level', title: '等級'}
        , {field: 'hp', title: '生命值'}
        , {field: 'damage', title: '傷害'}
        , {field: 'amour', title: '護甲'}
        , {
            title: '操作', width: 150, templet: function (d) {
                return '<button class="layui-btn layui-btn-xs"  onclick="xadmin.open(\'編輯怪物\',\'edit/' + d.id + '\', 500, 500)" type="button"><i class="layui-icon">&#xe642;</i>編輯</button>' +
                    '<button class="layui-btn-danger layui-btn layui-btn-xs"  onclick="remove(this, \'' + d.id + '\')" type="button"><i class="layui-icon">&#xe640;</i>刪除</button>';
            }
        }
    ]];

    crud.list(cols, '/manage/map_mob/list');
    crud.upload('/manage/map_mob/importExcel', 'xls|xlsx');
});

function search() {
    var data = {
        name: $('input[name="name"]').val(),
        levelStart: $('input[name="levelStart"]').val(),
        levelEnd: $('input[name="levelEnd"]').val(),
        faction: $('select[name="faction"]').val(),
        mobClass: $('select[name="mobClass"]').val(),
        mobType: $('select[name="mobType"]').val()
    };

    crud.search(data);
}

function reset(){
    $('#queryForm').reset();
}

function remove(obj, id) {
    crud.remove(obj, '/manage/map_mob/delete/' + id);
}
list.js

  可以看到,重構後的js,和後端一樣,簡潔多了。

執行效果

  這裡,由於改動了core模組,需要先對專案編譯打包。再執行rms模組,即可正常啟動專案。啟動後,效果和之前一樣,只是程式碼變得簡潔多了,這裡就不再截圖了。

小結

  本章終於把冗餘的程式碼進行了重構,整個程式碼瞬間提升了幾層逼格,變得乾淨多了。

  原始碼下載地址:https://idlestudio.ctfile.com/fs/14960372-386521083

  說明:我的程式碼風格就是不套用設計模式,在不確定最終效果時,不做過多提前設計,先實現了再說,實現了再慢慢重構,還有就是幾乎不註釋。

  最近因為有其他的事情,所以停更了一週。寫到這裡其實發現這一大章題目似乎叫RMS系統的初步實現更好。

  寫程式碼和寫文章其實差別還有點大。自己寫程式碼的時候,想到哪寫到哪,各個模組並行隨緣開發。但寫文章就必須按一定順序來,否則容易讓人困惑。所以文章裡的程式碼都是單獨拉一個分支重新整理的。

  後面希望自己能堅持下去,一週更一次,用一兩年把主線更完。

 

相關文章