个人网站:aijianli.site/可以免费在线制作简历,提供PDF下载,方便快捷。
公司业务需要,需要用户可以在前端自定义查询条件对数据库数据进行查询汇总。并且需要支持较为复杂的条件设置。因此公司前同事开发了一个前端选择数据库字段设置条件的组件,但是代码极其复杂和难以维护,在我接手后需要做新增功能时根本无从下手,因此对其进行了完全的重构。详见:《如何写出让同时崩溃的代码》 。而这篇是对其中核心部分代码的重构的分享。
一、条件格式
前后端通过指定统一的条件格式,由前端将条件设置转换成如下固定的格式,条件可以层层嵌套,层级不限,后端将条件解析成sql(这部分后端也是我来完成的,同样用了组合模式):
条件分为两种类型,条件组和单个条件,条件组可以包含多个条件,这样条件就可以无限嵌套,设置比较复杂的条件进行数据库查询。
js
{
type: String, // 条件类型 可选值 group(条件组)或 condition(具体条件)
setType: String, // 条件格式类型,可选值filedToValue、fieldToField 分别表示指标和值生成的条件,指标和指标生成的条件。type 为 condition 时使用
field: Object, // 字段信息
logic: String, // 当type 为 condition 时使用,可选值有 =、>、>=、<、<= 、<> 、like、null等,支持大部分数据库支持的逻辑符,当然这只是一个表示,后端需要更加约定解析成为相应
vlaue: String | Number | Object, // filedToValue时为一个值 String 或者 Number,fieldToField时是一个指标
relation: String, // 条件关系 可选值 or、and 表示当前条件与后一个条件的关系,当当前条件是最后一个或者是条件组中的最后一个是,relation不生效。
conditions: Array, // type为 group时使用,条件组中具体的条件列表
nagation: Boolean // 条件是否取反
}
最终格式如下:
js
[
{
type: "group",
relation: "or",
conditions: [
{
type: "condition",
field: {
name: "员工状态",
id: 12345678
},
logic: "=",
val: 1,
relation: "and"
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2011-07-09",
relation: "and"
}
]
},
{
type: "condition",
field: {
name: "入职时间",
id: 12345678
},
logic: "<",
val: "2001-07-09",
relation: "and"
}
]
二、前端设计
2.1 原本的设计
原本的代码是直接从 dom 中取数据,导致将 dom 设置的条件处理成约定的条件格式时极其复杂,很多的判断和 dom 操作,阅读性极差,可维护性极差。
2.2 使用组合模式设计
先上类图吧。
以上的类设计将条件数据和 dom 分开,使用 ConditionGroup 和 ConditionData 来进行数据的存储,页面上的操作与 ConditionGroup 或 ConditionData 进行关联,这样的好处就是在生成约定格式的条件时,不需要任何 dom 的操作,只需要关注于将 ConditionGroup 或 ConditionData 中存的数据处理成条件格式即可。并且条件数据的每个实现类单独实现自身的 getCondition方法即可,外部生成条件时只需要直接调用对象的 getCondition 方法,而不需要关心具体的实现。另外如果增加了新的条件类型,直接写新的实现类即可,也就是开闭原则。
2.3 代码实现
父类
将所有类都共有的属性抽到父类中,其中 id 和 dataId 是用于和 dom 进行数据绑定的,这里不多赘述。type、relation、nagation 是所有条件或者条件组都有的属性。由于ES6 没有抽象方法和抽象类的概念,此父类中不会定义抽象方法
js
/**
* 公共的父类
*/
class AbstractConditionData{
constructor(id,dataId,type,relation = 'and',nagation = false){
this.id = id;
this.dataId = dataId;
this.type = type;
this.relation = relation;
this.nagation = nagation;
}
}
条件数据类
条件数据类抽象出单个条件的公共属性,这里只是简单将 setType 抽象出来。
js
import AbstractConditionData from './AbstractConditionData.js'
/**
* 条件数据的公共父类
*/
class ConditionData extends AbstractConditionData {
/**
* 继承此类的子类,必定是单个条件,因此条件类型固定为 condition
*/
constructor(id, dataId, relation = "and", nagation = false, setType) {
super(id, dataId, "condition", relation, nagation);
this.setType = setType;
}
}
指标和值生成的条件实现
js
import ConditionData from "./ConditionData.js";
/**
* 指标和值创建的条件类
*/
class FieldToValueConditionData extends ConditionData {
constructor(id, dataId, relation = "and", nagation = false) {
super(id, dataId, relation, nagation, "filedToValue");
// dom 中变化时会将数据保存到下面的对象中
// firstItem 是用户设置条件的字段信息
// loginItem 是逻辑符项
// secondItem 是保存值的项
this.firstItem = {};
this.loginItem = {};
this.secondItem = {};
}
/**
* 实现一下生成固定格式的条件
*/
getCondition() {
let condition = {
type: this.type,
relation: this.relation,
nagation: this.nagation,
logic: this.loginItem.value,
field: {},
value: null
}
// 只保留有用的内容,保存所有信息json会比较大
// 只是简单实现,省略掉了验证
condition.field.id = this.firstItem.id;
condition.field.name = this.firstItem.name;
condition.field.type = this.firstItem.type;
condition.value = this.secondItem.value;
return condition;
}
}
指标和指标生成的条件实现
js
import ConditionData from "./ConditionData.js";
/**
* 指标和值创建的条件类
*/
class FieldToFieldConditionData extends ConditionData {
constructor(id, dataId, relation = "and", nagation = false) {
super(id, dataId, relation, nagation, "filedToField");
// dom 中变化时会将数据保存到下面的对象中
// firstItem 是用户设置条件的字段信息
// loginItem 是逻辑符项
// secondItem 是保存值的项
this.firstItem = {};
this.loginItem = {};
this.secondItem = {};
}
/**
* 实现一下生成固定格式的条件
*/
getCondition() {
let condition = {
type: this.type,
relation: this.relation,
nagation: this.nagation,
logic: this.loginItem.value,
field: {},
value: {}
}
// 只保留有用的内容,保存所有信息json会比较大
// 只是简单实现,省略掉了验证
condition.field.id = this.firstItem.id;
condition.field.name = this.firstItem.name;
condition.field.type = this.firstItem.type;
condition.value.id = this.secondItem.id;
condition.value.name = this.secondItem.name;
condition.value.type = this.secondItem.type;
return condition;
}
}
条件组类
js
/**
* 条件组的实现
*/
import AbstractConditionData from './AbstractConditionData.js'
class ConditionGroup extends AbstractConditionData {
constructor(id, dataId, relation, nagation) {
super(id, dataId, "group", relation, nagation);
this.children = [];
}
/**
* 往条件组中加一个条件
* condition: AbstractConditionData
*/
addCondtion(condition) {
this.children.push(condition)
}
/**
* 实现一下生成固定格式的条件
*/
getCondition() {
let conditionGroup = {
type: this.type,
relation: this.relation,
nagation: this.nagation,
conditions: []
}
// 这里只是展示简单的实现,并没有做一下校验和判断
this.children.forEach(condition => {
conditionGroup.conditions.push(condition.getCondition());
})
return conditionGroup;
}
}
三、说明
从类图可以看出,实际情况是组合模式和简单工厂模式组合使用,并且条件中的项是聚合了 AbstractItem ,这些非核心的设计这里就不展开了,本文只是简单介绍了组合模式在前端的使用,dom 部分没有写在其中,大家知道如何用组合模式就好了。
啰嗦一句,面向对象的编程方式确实可以让代码更易于维护,当然必须是要做好抽象和封装,并且需要熟悉面向对象的程序设计原则:单一职责原则、开闭原则、里氏替换原则、依赖倒换原则等。在此基础上再学学设计模式,知道哪些场景下可以使用哪些设计原则来解决问题就更好啦!
前端也是需要设计模式、数据结构与算法才能写出更好的代码的。