使用Mybatis实现动态SQL操作
constructor与discriminator鉴别器
通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfer Object,DTO)以及绝大部分领域模型的要求。 但有些情况下你想使用不可变类。 通常来说,很少或基本不变的、包含引用或查询数 据的表,很适合使用不可变类。 构造方法注入允许你在初始化时 为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBeans 属 性来达到这个目的,但有一些人更青睐于构造方法注入。constructor 元素就是为此而生的。
我们来看看Student类中的构造器:
package org.zero01.pojo;
public class Student {
private int sid;
private String sname;
private int age;
private String sex;
private String address;
public Student(Integer sid, String sname, Integer age, String sex, String address) {
this.sid = sid;
this.sname = sname;
this.age = age;
this.sex = sex;
this.address = address;
}
... getter 略 ...
}
注:类中可以写或不写getter setter方法,也可以只写 getter 方法或 setter 方法。
为了将结果注入构造方法,MyBatis需要通过某种方式定位相应的构造方法。 在下面的例子中,MyBatis搜索一个声明了五个形参的的构造方法,以javaType属性中指定的值来进行构造方法参数的排列顺序:
<?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="org.zero01.dao.StudentMapper">
<resultMap id="stuMap" type="Student">
<constructor>
<idArg column="sid" javaType="Integer"/>
<arg column="sname" javaType="String"/>
<arg column="age" javaType="Integer"/>
<arg column="sex" javaType="String"/>
<arg column="address" javaType="String"/>
</constructor>
</resultMap>
<select id="selectAll" resultMap="stuMap">
select * from student
</select>
</mapper>
编写一个简单的测试用例,测试能否正常往构造器中注入数据:
package org.zero01.test;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.zero01.dao.StudentMapper;
import org.zero01.pojo.Student;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class TestStudent {
private SqlSession sqlSession;
private StudentMapper studentMapper;
@Before
public void startTest() throws IOException {
String confPath = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(confPath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession();
studentMapper = sqlSession.getMapper(StudentMapper.class);
}
@After
public void endTest() {
if (sqlSession != null) {
sqlSession.close();
}
}
@Test
public void testSelectAll() {
List<Student> studentList = studentMapper.selectAll();
for (Student student : studentList) {
System.out.println(new JSONObject(student));
}
}
}
控制台输出结果:
{"address":"湖北","sname":"One","sex":"男","age":15,"sid":1}
{"address":"杭州","sname":"Jon","sex":"男","age":16,"sid":2}
当你在处理一个带有多个形参的构造方法时,很容易在保证 arg 元素的正确顺序上出错。 从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。 为了通过名称来引用构造方法参数,你可以添加 @Param 注解,或者使用 '-parameters' 编译选项并启用 useActualParamName 选项(默认开启)来编译项目。 下面的例子对于同一个构造方法依然是有效的,尽管第三和第四个形参顺序与构造方法中声明的顺序不匹配:
<?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="org.zero01.dao.StudentMapper">
<resultMap id="stuMap" type="Student">
<constructor>
<idArg column="sid" javaType="Integer" name="sid"/>
<arg column="sname" javaType="String" name="sname"/>
<arg column="sex" javaType="String" name="sex"/>
<arg column="age" javaType="Integer" name="age"/>
<arg column="address" javaType="String" name="address"/>
</constructor>
</resultMap>
<select id="selectAll" resultMap="stuMap">
select * from student
</select>
</mapper>
如果类中存在名称和类型相同的属性,那么可以省略 javaType 。
剩余的属性和规则和普通的 id 和 result 元素是一样的。
属性 |
描述 |
---|---|
column |
数据库中的列名,或者是列的别名。一般情况下,这和 传递给 resultSet.getString(columnName) 方法的参数一样。 |
javaType |
一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列表)。 如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证期望的 行为。 |
jdbcType |
JDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。 只需要在可能执行插入、更新和删除的允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可能为 null 的值指定这个类型。 |
typeHandler |
我们在前面讨论过的默认类型处理器。使用这个属性,你可以覆盖默 认的类型处理器。这个属性值是一个类型处理 器实现类的完全限定名,或者是类型别名。 |
select |
用于加载复杂类型属性的映射语句的 ID,它会从 column 属性中指定的列检索数据,作为参数传递给此 select 语句。具体请参考 Association 标签。 |
resultMap |
ResultMap 的 ID,可以将嵌套的结果集映射到一个合适的对象树中,功能和 select 属性相似,它可以实现将多表连接操作的结果映射成一个单一的ResultSet。这样的ResultSet将会将包含重复或部分数据重复的结果集正确的映射到嵌套的对象树中。为了实现它, MyBatis允许你 “串联” ResultMap,以便解决嵌套结果集的问题。想了解更多内容,请参考下面的Association元素。 |
select |
构造方法形参的名字。从3.4.3版本开始,通过指定具体的名字,你可以以任意顺序写入arg元素。参看上面的解释。 |
discriminator鉴别器
有时一个单独的数据库查询也许返回很多不同 (但是希望有些关联) 数据类型的结果集。 鉴别器元素就是被设计来处理这个情况的, 还有包括类的继承层次结构。 鉴别器非常容易理解,因为它的表现很像 Java 语言中的 switch 语句。
我们先新增两个Student的子类,MaleStudent类:
package org.zero01.pojo;
public class MaleStudent extends Student{
}
FemaleStudent类:
package org.zero01.pojo;
public class FemaleStudent extends Student {
}
定义鉴别器指定了 column 和 javaType 属性。 column 是 MyBatis 查找比较值的地方。 JavaType 是需要被用来保证等价测试的合适类型(尽管字符串在很多情形下都会有用)。比如:
<?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="org.zero01.dao.StudentMapper">
<resultMap id="stuMap" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<result property="age" column="age"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<!-- column:指定判断的列名,javaType:column值对应的java类型 -->
<discriminator javaType="String" column="sex">
<!-- sex列的值为 “男” 时,就把结果集包装成MaleStudent对象 -->
<case value="男" resultType="MaleStudent"/>
<!-- sex列的值为 “女” 时,就把结果集包装成FemaleStudent对象 -->
<case value="女" resultType="FemaleStudent"/>
</discriminator>
</resultMap>
<select id="selectAll" resultMap="stuMap">
select * from student
</select>
</mapper>
修改测试方法如下:
@Test
public void testSelectAll() {
List<Student> studentList = studentMapper.selectAll();
for (Student student : studentList) {
System.out.println(new JSONObject(student));
System.out.println(student.getClass().getName() + "n");
}
}
运行成功后,控制台输出结果如下:
{"address":"湖北","sname":"One","sex":"男","age":15,"sid":1}
org.zero01.pojo.MaleStudent
{"address":"杭州","sname":"Jon","sex":"男","age":16,"sid":2}
org.zero01.pojo.MaleStudent
{"address":"湖南","sname":"Max","sex":"女","age":18,"sid":3}
org.zero01.pojo.FemaleStudent
{"address":"广州","sname":"Alen","sex":"女","age":19,"sid":4}
org.zero01.pojo.FemaleStudent
从控制台输出结果中,可以看到,我们成功通过鉴别器,将不同的性别的结果集数据封装到了不同的子类中。在case元素中,还可以使用resultMap属性引用某个结果集的映射器,以及可以直接在case元素中使用result等元素进行结果集的封装。例如:
<resultMap id="vehicleResult" type="Vehicle">
<id property="id" column="id" />
<result property="vin" column="vin"/>
<result property="year" column="year"/>
<result property="make" column="make"/>
<result property="model" column="model"/>
<result property="color" column="color"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultType="carResult">
<result property="doorCount" column="door_count" />
</case>
<case value="2" resultType="truckResult">
<result property="boxSize" column="box_size" />
<result property="extendedCab" column="extended_cab" />
</case>
<case value="3" resultType="vanResult">
<result property="powerSlidingDoor" column="power_sliding_door" />
</case>
<case value="4" resultType="suvResult">
<result property="allWheelDrive" column="all_wheel_drive" />
</case>
</discriminator>
</resultMap>
Mybatis动态SQL
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。虽然在以前使用动态 SQL 并非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射语句中的强大的动态 SQL 语言得以改进这种情形。
动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类,现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。MyBatis 3 只需要学习以下元素即可:
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
(1) if 元素:
if 元素通常要做的事情是根据条件动态生成 where 子句的一部分。比如:
<select id="findStudentById" resultType="Student" parameterType="Student">
select * from student
where sid = #{sid}
<if test="sname != null">
and sname = #{sname}
</if>
</select>
这条语句提供了一种可选的查找文本功能。如果没有传入“sname”,那么只会查询sid相匹配的记录;反之若传入了“sname”,那么就会增多一个“sname”字段的匹配条件(细心的读者可能会发现,“title”参数值是可以包含一些掩码或通配符的)。
和if语句一样,除了可以使用!=、<=、>=等运算符外,if元素也可以使用 and、or、not之类的运算符,如下:
<select id="findStudentById" resultType="Student" parameterType="Student">
select * from student
where sid = #{sid}
<if test="sname != null and sex !=null ">
and sname = #{sname}
</if>
</select>
(2)choose, when, otherwise元素:
有时候我们需要使用分支条件判断,针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。例如:
<select id="findStudentById" resultType="Student" parameterType="Student">
select * from student
where sid = #{sid}
<choose>
<when test="sname != null">
and sname = #{sname}
</when>
<when test="sex != null">
and sex = #{sex}
</when>
<when test="address != null">
and address = #{address}
</when>
<otherwise>
and age > 0
</otherwise>
</choose>
</select>
(2)trim, where, set元素:
前面几个例子已经合宜地解决了一个臭名昭著的动态 SQL 问题。现在回到“if”示例,这次我们将sid = #{sid}
也设置成动态的条件,看看会发生什么:
<select id="findStudentById" resultType="Student" parameterType="Student">
select * from student
where
<if test="sid != null">
sid = #{sid}
</if>
<if test="sname != null and sex !=null ">
and sname = #{sname}
</if>
</select>
如果这些条件没有一个能匹配上会发生什么?最终这条 SQL 会变成这样:
SELECT * FROM student
WHERE
这会导致查询失败。如果仅仅第二个条件匹配又会怎样?这条 SQL 最终会是这样:
SELECT * FROM student
WHERE and sname = #{sname}
显而易见,这个查询也会失败。这个问题不能简单地用条件句式来解决,如果你也曾经*这样写过,那么你很可能从此以后都不会再写出这种语句了。
好在 MyBatis 中有一个简单的处理,这在 90% 的情况下都会有用。而在不能使用的地方,你可以自定义处理方式来令其正常工作。我们只需要把sql语句中 where 替换成 MyBatis 中的 where元素即可,如下:
<select id="findStudentById" resultType="Student" parameterType="Student">
select * from student
<where>
<if test="sid != null">
sid = #{sid}
</if>
<if test="sname != null and sex !=null ">
and sname = #{sname}
</if>
</where>
</select>
where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句,如果没有 SQL 子句的返回则不会插入“WHERE”子句。而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除。
如果 where 元素没有按正常套路出牌,我们可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
<select id="findStudentById" resultType="Student" parameterType="Student">
select * from student
<trim prefix="where" prefixOverrides="AND | OR ">
<if test="sid != null">
sid = #{sid}
</if>
<if test="sname != null and sex !=null ">
and sname = #{sname}
</if>
</trim>
</select>
prefixOverrides 属性可以忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它的作用是移除所有指定在 prefixOverrides 属性中的内容,并且插入 prefix 属性中指定的内容。但是 prefixOverrides 属性移除的是文本中前面的内容,例如有一段文本内容如下:
AND sname = #{sname} AND
然后指定 prefixOverrides 属性的值为 “AND” ,由于 prefixOverrides 属性只会移除前面的AND,所以移除后的文件内容如下:
sname = #{sname} AND
与之相对应的属性是 suffixOverrides ,它的作用是移除所有指定在 suffixOverrides 属性中的内容,而它移除的是文本后面的内容。例如,在执行update更新语句的时候,我们也希望至少有一个子元素的条件返回 SQL 子句的情况下才去插入 “SET” 子句,而且,若语句的结尾为 “ , ” 时需要将它们去除。使用trim元素实现如下:
<update id="updateStudent">
update student
<trim prefix="set" suffixOverrides=",">
<if test="sname != null ">sname = #{sname},</if>
<if test="age != 0 ">age = #{age},</if>
<if test="sex != null ">sex = #{sex},</if>
<if test="address != null ">address = #{address}</if>
</trim>
where sid = #{sid}
</update>
suffixOverrides属性会把语句末尾中的逗号删除,同样的,prefix属性会将指定的内容插入到语句的开头。与prefix属性相对应的是suffix属性,该属性会将指定的内容插入到语句的末尾。
以上我们使用trim元素实现了动态的更新语句,这种方式还有些麻烦,其实还可以更简单,使用set元素即可,如下:
<update id="updateStudent">
update student
<set>
<if test="sname != null ">sname = #{sname},</if>
<if test="age != 0 ">age = #{age},</if>
<if test="sex != null ">sex = #{sex},</if>
<if test="address != null ">address = #{address}</if>
<set>
where sid = #{sid}
</update>
set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号,因为用了条件语句之后很可能就会在生成的 SQL 语句的后面留下这些逗号。(因为用的是“if”元素,若最后一个“if”没有匹配上而前面的匹配上,SQL 语句的最后就会有一个逗号遗留)
(4)foreach元素:
动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:
<select id="selectStudentIn" resultMap="stuMap">
select * from student where sid in
<foreach collection="list" index="index" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</select>
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及在迭代结果之间放置分隔符。这个元素是很智能的,因此它不会偶然地附加多余的分隔符:
- collection属性指定接收的是什么集合
- open属性指定开头的符号
- close属性指定结尾的符号
- separator属性指定迭代结果之间的分隔符
- item属性存储每次迭代的集合元素(map集合时为值)
- index属性存储每次迭代的索引(map集合时为键)
测试代码如下:
@Test
public void testSelectPostIn() {
List<Integer> idList = new ArrayList<Integer>();
for (int i = 1; i <= 4; i++) {
idList.add(i);
}
List<Student> studentList = studentMapper.selectStudentIn(idList);
for (Student student : studentList) {
System.out.println(new JSONObject(student));
System.out.println(student.getClass().getName() + "n");
}
}
运行该方法后,最终生成出来的sql语句为:
SELECT * FROM student WHERE sid IN (1,2,3,4)
注意:
你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
(5)bind元素:
bind 元素可以从 OGNL 表达式中创建一个变量并将其绑定到上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
(6)sql、include元素:
sql元素用来定义可重用的 SQL 代码段,这些代码段可以被包含在其他语句中,它可以被静态地(在加载参数) 参数化。而include元素就是用来引入sql元素所定义的可重用 SQL 代码段的,如下示例:
<sql id="base_column_list">
sid,sname,age,sex,address
</sql>
<select id="selectAll" resultMap="stuMap">
select
<include refid="base_column_list"/>
from student
</select>
最终生成出来的sql语句为:
select sid,sname,age,sex,address from student
sql元素中也可以使用include元素,例如:
<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>
关于返回null值:
有时候我们的表格中的某个列可能会存储了一些null值,如下表格:
当某个列存在null值的话,我们使用数据库的内置函数进行求和、统计之类的操作时,可能会刚好操作的记录的同一个字段都是null,那么返回的结果集就会是null。例如,我们对上面那张表格进行sum求和操作:
<select id="selectBySumAge" resultType="int">
SELECT SUM(age) FROM student
</select>
如果我们在dao层接口方法中声明的返回值是基本数据类型的话,就会报错,如下:
...
public interface StudentMapper {
...
int selectBySumAge();
}
测试用例代码:
@Test
public void testSelectBySumAge() {
Integer sumAge = studentMapper.selectBySumAge();
System.out.println(sumAge);
}
这时运行测试用例的话,就会报如下错误:
org.apache.ibatis.binding.BindingException: Mapper method 'org.zero01.dao.StudentMapper.selectBySumAge attempted to return null from a method with a primitive return type (int).
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:93)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy5.selectBySumAge(Unknown Source)
at org.zero01.test.TestStudent.testSelectBySumAge(TestStudent.java:87)
... 略
会报这个错误是因为int这种基本数据类型是无法接收null的,只能使用包装类型进行接收。
为了解决这个问题,我们需要把dao层接口方法的返回值修改为Integer类型,如下:
...
public interface StudentMapper {
...
Integer selectBySumAge();
}
xml中的resultType可以使用int,但是为了防止意外问题,建议还是使用Integer包装类型:
<select id="selectBySumAge" resultType="java.lang.Integer">
SELECT SUM(age) FROM student
</select>
测试代码不变,运行后,控制台输出内容如下:
null
除了在代码层解决这个问题外,还可以在sql中解决这个问题,以sum求和示例,使用以下几种sql语句,可以避免返回null值:
/* 第一种: 采用 IFNULL(expr1,expr2)函数,当expr1为NULL时,则数据返回默认值expre2 */
SELECT IFNULL(SUM(age),0) FROM student /* 若 SUM() 函数结果返回为 NULL 则返回 0 */
/* 第二种: 采用从 COALESCE(value,...) 函数, COALESCE 函数作用是返回传入参数中第一个非空的值 */
SELECT COALESCE(SUM(age),0) FROM student
/* 第三种: 采用 case WHEN THEN WHEN THEN .. ELSE END 函数,注意 CASE WHEN 函数最后是以 END 结尾 */
SELECT CASE WHEN ISNULL(SUM(age)) THEN 0 ELSE SUM(age) END AS ageSum FROM student
上一篇: 探究Java OGNL注入漏洞的运作机制
推荐阅读
-
❤️ 创意网页:制作简单美观的网页旋转图(HTML 简单实现旋转图)操作简单可直接使用!
-
Mybatis 高级版(动态 SQL)
-
利用 Google & Tampermonkey & Modify Headers 实现问卷星调查问卷自动填写提交--操作与使用
-
MyBatis 动态拼接 Sql 字符串问题
-
使用 springboot 整合 mybatis-plus 实现视点功能
-
第 9 天]Mybatis CURD + XML 映射 + 动态 SQL
-
一种结构设计模式,允许在对象中动态添加新行为。它通过创建一个封装器来实现这一目的,即把对象放入一个装饰器类中,然后把这个装饰器类放入另一个装饰器类中,以此类推,形成一个封装器链。这样,我们就可以在不改变原始对象的情况下动态添加新行为或修改原始行为。 在 Java 中,实现装饰器设计模式的步骤如下: 定义一个接口或抽象类作为被装饰对象的基类。 公共接口 Component { void operation; } } 在本例中,我们定义了一个名为 Component 的接口,该接口包含一个名为 operation 的抽象方法,该方法定义了被装饰对象的基本行为。 定义一个实现基类方法的具体装饰对象。 公共类 ConcreteComponent 实现 Component { public class ConcreteComponent implements Component { @Override public void operation { System.out.println("ConcreteComponent is doing something...") ; } } 定义一个抽象装饰器类,该类继承于基类,并将装饰对象作为一个属性。 公共抽象类装饰器实现组件 { protected Component 组件 public Decorator(Component component) { this.component = component; } } @Override public void operation { component.operation; } } } 在这个示例中,我们定义了一个名为 Decorator 的抽象类,它继承了 Component 接口,并将被装饰对象作为一个属性。在操作方法中,我们调用了被装饰对象上的同名方法。 定义一个具体的装饰器类,继承自抽象装饰器类并实现增强逻辑。 公共类 ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component 组件) { super(component); } } public void operation { super.operation System.out.println("ConcreteDecoratorA 正在添加新行为......") ; } } 在本例中,我们定义了一个名为 ConcreteDecoratorA 的具体装饰器类,它继承自装饰器抽象类,并实现了操作方法的增强逻辑。在操作方法中,我们首先调用被装饰对象上的同名方法,然后添加新行为。 使用装饰器增强被装饰对象。 公共类 Main { public static void main(String args) { Component 组件 = new ConcreteComponent; component = new ConcreteDecoratorA(component); 组件操作 } } 在这个示例中,我们首先创建了一个被装饰对象 ConcreteComponent,然后通过 ConcreteDecoratorA 类创建了一个装饰器,并将被装饰对象作为参数传递。最后,调用装饰器的操作方法,实现对被装饰对象的增强。 使用场景 在 Java 中,装饰器模式被广泛使用,尤其是在 I/O 中。Java 中的 I/O 库使用装饰器模式实现了不同数据流之间的转换和增强。 让我们打开文件 a.txt,从中读取数据。InputStream 是一个抽象类,FileInputStream 是专门用于读取文件流的子类。BufferedInputStream 是一个支持缓存的数据读取类,可以提高数据读取的效率,具体代码如下: @Test public void testIO throws Exception { InputStream inputStream = new FileInputStream("C:/bbb/a.txt"); // 实现包装 inputStream = new BufferedInputStream(inputStream); byte bytes = new byte[1024]; int len; while((len = inputStream.read(bytes)) != -1){ System.out.println(new String(bytes, 0, len)); } } } } 其中 BufferedInputStream 对读取数据进行了增强。 这样看来,装饰器设计模式和代理模式似乎有点相似,接下来让我们讨论一下它们之间的区别。 第三,与代理模式的区别: 代理模式的目的是控制对对象的访问,它在对象外部提供一个代理对象来控制对原对象的访问。代理对象和原始对象通常实现相同的接口或继承相同的类,以确保两者可以相互替换。 装饰器模式的目的是动态增强对象的功能,而这是通过对象内部的包装器来实现的。在装饰器模式中,装饰器类和被装饰对象通常实现相同的接口或继承自相同的类,以确保两者可以相互替代。装饰器模式也被称为封装器模式。 在代理模式中,代理类附加了与原类无关的功能。
-
关于使用后端实现动态表单功能的见解
-
什么是可用性测试?有效性(Effectiveness)-- 用户完成特定任务和实现特定目标的正确性和完整性程度;效率(Efficiency)-- 用户完成任务的正确性和完整性程度与所用资源(如时间)之比;满意度(Satisfaction)-- 用户在使用产品时的主观满意度和接受程度。 2.如何获得可用性? 可以参考以下原则:Gould、Boies 和 Lewis(1991 年)为以用户为中心的设计定义了 4 个重要原则: 早期以用户为中心:设计者应在设计过程的早期就努力了解用户的需求。 综合设计:设计的所有方面都应同步发展,而不是按顺序进行。使产品的内部设计始终与用户界面的需求保持一致。 早期和持续测试:当今唯一可行的软件测试方法是经验主义方法,即如果实际用户认为设计可行,该设计就是可行的。通过在整个开发过程中引入可用性测试,用户就有机会在产品推出之前对设计提出反馈意见。 迭代设计:大问题往往掩盖了小问题的存在。设计人员和开发人员应在整个测试过程中对设计进行迭代。 3...什么是可用性测试? 可用性测试是根据可用性标准对图形用户界面进行的系统评估。 可用性测试是衡量用户与系统(网站、软件应用程序、移动技术或任何用户操作设备)交互时的体验质量。4.如何进行可用性测试? l 实验室实验
-
纯干货分享 | 研发效能提升——敏捷需求篇-而敏捷需求是提升效能的方式中不可或缺的模块之一。 云智慧的敏捷教练——Iris Xu近期在公司做了一场分享,主题为「敏捷需求挖掘和组织方法,交付更高业务价值的产品」。Iris具有丰富的团队敏捷转型实施经验,完成了企业多个团队从传统模式到敏捷转型的落地和实施,积淀了很多的经验。 这次分享主要包含以下2个部分: 第一部分是用户影响地图 第二部分是事件驱动的业务分析Event driven business analysis(以下简称EDBA) 用户影响地图,是一种从业务目标到产品需求映射的需求挖掘和组织的方法。 在软件开发过程中可能会遇到一些问题,比如大家使用不同的业务语言、技术语言,造成角色间的沟通阻碍,还会导致一些问题,比如需求误解、需求传递错误等;这会直接导致产品的功能需求和要实现的业务目标不是映射关系。 但在交付期间,研发人员必须要将这些需求实现交付,他们实则并不清楚这些功能需求产生的原因是什么、要解决客户的哪些痛点。研发人员往往只是拿到了解决方案,需要把它实现,但没有和业务侧一起去思考解决方案是否正确,能否真正的帮助客户解决问题。而用户影响地图通常是能够连接业务目标和产品功能的一种手段。 我们在每次迭代里加入的假设,也就是功能需求。首先把它先实现,再逐步去验证我们每一个小目标是否已经实现,再看下一个目标要是什么。那影响地图就是在这个过程中帮我们不断地去梳理目标和功能之间的关系。 我们在软件开发中可能存在的一些问题 针对这些问题,我们如何避免?先简单介绍做敏捷转型的常规思路: 先做团队级的敏捷,首先把产品、开发、测试人员,还有一些更后端的人员比如交互运维的同学放在一起,组成一个特训团队做交付。这个团队要包含交付过程中所涉及的所有角色。 接着业务敏捷要打通整个业务环节和研发侧的一个交付。上图中可以看到在敏捷中需求是分层管理的,第一层是业务需求,在这个层级是以用户目标和业务目标作为输入进行规划,同时需要去考虑客户的诉求。业务人员通过获取到的业务需求,进一步的和团队一起将其分解为产品需求。所以业务需求其实是我们真正去发布和运营的单元,它可以被独立发布到我们的生产环境上。我们的产品需求其实就是产品的具体功能,它是我们集成和测试的对象,也就是我们最终去部署到系统上的一个基本单元。产品需求再到了我们的开发团队,映射到迭代计划会上要把它分解为相应的技术任务,包括我们平时所说的比如一些前端的开发、后端的开发、测试都是相应的技术任务。所以业务敏捷要达到的目标是需要去持续顺畅高质量的交付业务价值。 将这几个点串起来,形成金字塔结构。最上层我们会把业务目标放在整个金字塔的塔尖。这个业务目标是通过用户的目标以及北极星指标确立的。确认业务目标后再去梳理相应的业务流程,最后生产。另外产品需求包含了操作流程和业务规则,具需求交付时间、工程时间以及我们的一些质量标准的要求。 谈到用户影响的地图,在敏捷江湖上其实有一个传说,大家都有一个说法叫做敏捷需求的“任督二脉”。用户影响地图其实就是任脉,在黑客马拉松上用过的用户故事地图其实叫督脉。所以说用户影响地图是在用户故事地图之前,先帮我们去梳理出我们要做哪些东西。当我们真正识别出我们要实现的业务活动之后,用户故事地图才去梳理我们整个的业务工作流,以及每个工作流节点下所要包含的具体功能和用户故事。所以说用户影响地图需要解决的问题,我们包括以下这些: 首先是范围蔓延,我们在整张地图上,功能和对应的业务目标是要去有一个映射的。这就避免了一些在我们比如有很多干系人参与的会议上,那大家都有不同想法些立场,会提出很多需求(正确以及错误的需求)。这个时候我们会依据目标去看这些需求是否真的是会影响我们的目标。 这里提到的错误需求,比如是利益相关的人提出的、客户认为产品应该有的、某个产品经理需求分析师认为可以有的....但是这些功能在用户影响地图中匹配不到对应目标的话,就需要降低优先级或弃掉。另外,通常我们去制定解决方案的时候,会考虑较完美的实现,导致解决方案括很多的功能。这个时候关键目标至关重要,会帮助我们梳理筛选、确定优先级。 看一下用户影响到地图概貌 总共分为一个三层的结构: 第一层why,你的业务目标哪个是最重要的,为什么?涉及到的角色有哪些? 第二层how ,怎样产生影响?影响用户角色什么样的行为? (不需要去列出所有的影响,基于业务目标) 第三层what,最关键的是在梳理需求时不需一次把所有细节想全,这通常团队中经常遇到的问题。 我们用这个例子来看一下 这是一个客服中心的影响地图,业务目标是 3个月内不增加客服人数的前提下能支持1.5倍的用户数。此业务目标设定是符合 smart 原则的,specific非常的具体,miserable 是可以衡量的,action reoriented是面向活动的, real list 也是很实际的。 量化的目标会指引我们接下来的行动,梳理一个业务目标,尽量去量化,比如 :我们通过打造一条什么样的流水线,能够提高整个部署的效率,时间是原来的 1/2 。这样才是一个能量化的有意义的目标。 回到这幅图, how 层级识别出来的内容,客服角色:想要对它施加的影响,把客户引导到论坛上,帮助客户更容易的跟踪问题,更快速的去定位问题。初级用户:方论坛上找到问题。高级用户:在论坛上回答问题。通过我们这些用户角色,进行活动,完成在不增加客户客服人数的前提下支持更多的用户数量。 最后一个层级,才是我们日常接触比较多的真正的功能的特性和需求,比如引导到客户到论坛上,其实这个产品就需要有一个常见问题的论坛的链接。这个层次需要我们团队进一步地在交付,在每个迭代之前做进一步的梳理,细化成相应的用户故事。 这个是云智慧团队中,自己做的影响地图的范例,可以看下整个的层级结构。序号表示优先级。 那我们用户影响地图可以总结为: