TDD实践(一)

编程技术  /  houtizong 发布于 3年前   72

一.TDD概述

        TDD:测试驱动开发,它的基本思想就是在开发功能代码之前,先编写测试代码。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用例。然后循环进行添加其他功能,直到完全部功能的开发。

        TDD的基本思路就是通过测试来推动整个开发的进行。

        需求向来就是软件开发过程中感觉最不好明确描述、易变的东西。这里说的需求不只是指用户的需求,还包括对代码的使用需求。很多开发人员最害怕的就是后期还要修改某个类或者函数的接口进行修改或者扩展,为什么会发生这样的事情就是因为这部分代码的使用需求没有很好的描述。测试驱动开发就是通过编写测试用例,先考虑代码的使用需求(包括功能、过程、接口等),而且这个描述是无二义的,可执行验证的。

        通过编写这部分代码的测试用例,对其功能的分解、使用过程、接口都进行了设计。而且这种从使用角度对代码的设计通常更符合后期开发的需求。可测试的要求,对代码的内聚性的提高和复用都非常有益。因此测试驱动开发也是一种代码设计的过程。

        开发人员通常对编写文档非常厌烦,但要使用、理解别人的代码时通常又希望能有文档进行指导。而测试驱动开发过程中产生的测试用例代码就是对代码的最好的解释。

        当然测试驱动开发最重要的功能还在于保障代码的正确性,能够迅速发现、定位bug。而迅速发现、定位bug是很多开发人员的梦想。针对关键代码的测试集,以及不断完善的测试用例,为迅速发现、定位bug提供了条件。

        实践一段功能比较复杂的代码使用TDD开发完成,只发现几个bug,而且很快能定位解决。我相信,如果在实际工作中运用TDD,也一定会有这种自信的开发过程,功能不断增加、完善的感觉,迅速发现、定位bug的能力。

 

二.TDD实战练习

        说明:这是一个停车场练习,需求是软件运行一段时间后,用户再次提出来的。

需求1:要一个停车场,有一定数目的停车位,能停车取车,如果车停满了就不能停了,在停满的情况下取一辆车后就能再停一辆进来。

        TDD具体开发过程:先写一个测试失败的测试案例,然后编写代码让这个测试案例通过,再重构,重构完成后再写一个测试失败的测试案例......直到需求要求的功能开发完成。

PackLogTest.java

package com.bijian.test;import junit.framework.Assert;import org.junit.Before;import org.junit.Test;import com.bijian.study.PackLog;public class PackLogTest {    private PackLog packLog;    @Before    public void setUp() throws Exception {        packLog = new PackLog("X", 10);    }    @Test    public void test_normal_stop_a_car() {    String vehicleCode = "粤B-123456";        packLog.stopCar(vehicleCode);    int remainStopCarNum = packLog.getRemainStopCarNum();            Assert.assertEquals(10-1, remainStopCarNum);    }        @Test    public void test_normal_stop_a_car_then_get() {    String vehicleCode = "粤B-123456";        packLog.stopCar(vehicleCode);    packLog.getCar("粤B-123456");    int remainStopCarNum = packLog.getRemainStopCarNum();        Assert.assertEquals(10+1-1, remainStopCarNum);    }        private void given_init_data_for_packLog(int num) {for(int i=0;i<num;i++) {packLog.stopCar("粤B-00000" + i);}}        @Test    public void test_full_packlog_then_stop_car_fail() {        given_init_data_for_packLog(10);        String vehicleCode = "粤B-123456";    packLog.stopCar(vehicleCode);    int remainStopCarNum = packLog.getRemainStopCarNum();    boolean isStopInThisPackLog = packLog.isStopInThisPackLog(vehicleCode);        Assert.assertEquals(0, remainStopCarNum);    Assert.assertFalse(isStopInThisPackLog);    }        @Test    public void test_full_packlog_then_stop_car_success_after_get_a_car() {        given_init_data_for_packLog(9);    String vehicleCode = "粤B-123456";    packLog.stopCar(vehicleCode);        packLog.getCar(vehicleCode);    String vehicleCode2 = "粤B-333333";    packLog.stopCar(vehicleCode2);    int remainStopCarNum = packLog.getRemainStopCarNum();    boolean isStopInThisPackLog = packLog.isStopInThisPackLog(vehicleCode2);        Assert.assertEquals(0, remainStopCarNum);    Assert.assertTrue(isStopInThisPackLog);    }        @Test    public void test_empty_packlog_then_get_car() {        String vehicleCode = "粤B-123456";    String resVehicleCode = packLog.getCar(vehicleCode);        int remainStopCarNum = packLog.getRemainStopCarNum();        Assert.assertNull(resVehicleCode);    Assert.assertEquals(10, remainStopCarNum);    }}

PackLog.java

package com.bijian.study;import java.util.HashMap;import java.util.Map;public class PackLog {//停车场名称private String name;//停车场车位总数量private int totalStopCarNum;private Map<String, String> stopCarMap;public PackLog(String name, int stopCarNum) {this.name = name;this.totalStopCarNum = stopCarNum;stopCarMap = new HashMap<String, String>(stopCarNum);}public synchronized void stopCar(String vehicleCode) {if(stopCarMap.size() < totalStopCarNum) {stopCarMap.put(vehicleCode, vehicleCode);}}public synchronized String getCar(String vehicleCode) {return stopCarMap.remove(vehicleCode);}public int getRemainStopCarNum() {return (totalStopCarNum - this.stopCarMap.size()) ;}public boolean isStopInThisPackLog(String vehicleCode) {if(stopCarMap == null || stopCarMap.size() == 0) {return false;}return stopCarMap.containsKey(vehicleCode);}public String getName() {return name;}}

 

需求2:有多个需求1那样的停车场,这多个停车场交由一个管理员统一管理,哪个停车场有车位,就将车停到哪个停车场。

Employee.java

package com.bijian.study;import java.util.ArrayList;import java.util.List;public class Employee {private List<PackLog> packLogs = new ArrayList<PackLog>();public void addPackLog(PackLog packLog) {packLogs.add(packLog);}public void stopCar(String vehicleCode) {PackLog packLog = getPackLog(packLogs);if(packLog == null) {return;}packLog.stopCar(vehicleCode);}public String getCar(String vehicleCode) {String vehicleCodeRes = null;for(PackLog packLog: packLogs) {vehicleCodeRes = packLog.getCar(vehicleCode);if(vehicleCodeRes != null) {break;}}return vehicleCodeRes;}public PackLog getPackLog(List<PackLog> packLogs){for(PackLog packLog: packLogs) {if(packLog.getRemainStopCarNum()>0) {return packLog;}}return null;}}

EmployeeTest.java

package com.bijian.test;import junit.framework.Assert;import org.junit.Before;import org.junit.Test;import com.bijian.study.Employee;import com.bijian.study.PackLog;public class EmployeeTest {private Employee employee;private PackLog packLog1;private PackLog packLog2;    @Before    public void setUp() throws Exception {    employee = new Employee();    packLog1 = new PackLog("X", 1);    packLog2 = new PackLog("Y", 2);    employee.addPackLog(packLog1);    employee.addPackLog(packLog2);    }    @Test    public void test_normal_stop_car() {        String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);            Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-1, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-0, packLog2.getRemainStopCarNum());    }        @Test    public void test_normal_get_car() {        String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);    String vehicleCodeRes = employee.getCar("粤B-123456");        Assert.assertEquals("粤B-123456", vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-0, packLog2.getRemainStopCarNum());    }        private void given_init_data_for_packLog1(int num) {    for(int i=0;i<num;i++) {packLog1.stopCar("粤B-00000" + i);}    }        private void given_init_data_for_packLog2(int num) {    for(int i=0;i<num;i++) {packLog2.stopCar("粤B-00001" + i);}    }        @Test    public void test_full_packlog_then_stop_car_fail() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(2);        String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);    String vehicleCodeRes = employee.getCar("粤B-123456");        Assert.assertNull(vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(0, packLog2.getRemainStopCarNum());    }        @Test    public void test_full_packlog_then_stop_car_success_after_get_a_car() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(1);    String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);        employee.getCar(vehicleCode);    String vehicleCode2 = "粤B-333333";    employee.stopCar(vehicleCode2);    int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();    boolean isStopInThisPackLog = packLog2.isStopInThisPackLog(vehicleCode2);        Assert.assertEquals(0, packLog1RemainStopCarNum);    Assert.assertEquals(0, packLog2RemainStopCarNum);    Assert.assertTrue(isStopInThisPackLog);    }        @Test    public void test_empty_packlog_then_get_car() {        String vehicleCode = "粤B-123456";    String resVehicleCode = employee.getCar(vehicleCode);        int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();        Assert.assertNull(resVehicleCode);    Assert.assertEquals(1, packLog1RemainStopCarNum);    Assert.assertEquals(2, packLog2RemainStopCarNum);    }}

 

需求3:又聘请了一位聪明的管理员,当有车停进来时,他会判断当前这些停车场,哪个停车场剩余的车位多,就将车停到哪个停车场。

采用继承机制实现:

SmartEmployee.java

package com.bijian.study;import java.util.List;public class SmartEmployee extends Employee {public PackLog getPackLog(List<PackLog> packLogs){int res = 0;PackLog packLogRes = null;for(PackLog packLog: packLogs) {int currentPackLogNum = packLog.getRemainStopCarNum();res = currentPackLogNum > res ? currentPackLogNum : res;packLogRes = packLog;}return packLogRes;}}

SmartEmployeeTest.java

package com.bijian.test;import junit.framework.Assert;import org.junit.Before;import org.junit.Test;import com.bijian.study.PackLog;import com.bijian.study.SmartEmployee;public class SmartEmployeeTest {private SmartEmployee smartEmployee;private PackLog packLog1;private PackLog packLog2;    @Before    public void setUp() throws Exception {    smartEmployee = new SmartEmployee();    packLog1 = new PackLog("X", 1);    packLog2 = new PackLog("Y", 2);    smartEmployee.addPackLog(packLog1);    smartEmployee.addPackLog(packLog2);    }    @Test    public void test_normal_stop_car() {    String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);        Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-1, packLog2.getRemainStopCarNum());    }        @Test    public void test_normal_get_car() {    String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);    String vehicleCodeRes = smartEmployee.getCar(vehicleCode);                Assert.assertEquals("粤B-123456", vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-0, packLog2.getRemainStopCarNum());    }        private void given_init_data_for_packLog1(int num) {    for(int i=0;i<num;i++) {packLog1.stopCar("粤B-00000" + i);}    }        private void given_init_data_for_packLog2(int num) {    for(int i=0;i<num;i++) {packLog2.stopCar("粤B-00001" + i);}    }        @Test    public void test_full_packlog_then_stop_car_fail() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(2);        String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);    String vehicleCodeRes = smartEmployee.getCar("粤B-123456");        Assert.assertNull(vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(0, packLog2.getRemainStopCarNum());    }        @Test    public void test_full_packlog_then_stop_car_success_after_get_a_car() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(1);    String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);        smartEmployee.getCar(vehicleCode);    String vehicleCode2 = "粤B-333333";    smartEmployee.stopCar(vehicleCode2);    int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();    boolean isStopInThisPackLog = packLog2.isStopInThisPackLog(vehicleCode2);        Assert.assertEquals(0, packLog1RemainStopCarNum);    Assert.assertEquals(0, packLog2RemainStopCarNum);    Assert.assertTrue(isStopInThisPackLog);    }        @Test    public void test_empty_packlog_then_get_car() {        String vehicleCode = "粤B-123456";    String resVehicleCode = smartEmployee.getCar(vehicleCode);        int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();        Assert.assertNull(resVehicleCode);    Assert.assertEquals(1, packLog1RemainStopCarNum);    Assert.assertEquals(2, packLog2RemainStopCarNum);    }}

 

三.小结

1.单元测试

        a.单元测试三步骤:准备数据、测试、检验结果

        b.单元测试方法命名采用:Given_When_Then方式

        c.类中不要有set方法,如果用初始化测试数据,采用私有方法,即givenXXX()方法初始化数据

2.如上第三个需求采用OO中的继承实现,也可以采用高大上点的方式实现,即运用策略模式,如下所示:

IStrategy.java

package com.bijian.study;import java.util.List;public interface IStrategy {public PackLog getSuitablePackLog(List<PackLog> packLogs);}

CommonStrategy.java

package com.bijian.study;import java.util.List;public class CommonStrategy implements IStrategy {@Overridepublic PackLog getSuitablePackLog(List<PackLog> packLogs) {for(PackLog packLog: packLogs) {if(packLog.getRemainStopCarNum()>0) {return packLog;}}return null;}}

SmartStrategy.java

package com.bijian.study;import java.util.List;public class SmartStrategy implements IStrategy {@Overridepublic PackLog getSuitablePackLog(List<PackLog> packLogs) {int res = 0;PackLog packLogRes = null;for(PackLog packLog: packLogs) {int currentPackLogNum = packLog.getRemainStopCarNum();res = currentPackLogNum > res ? currentPackLogNum : res;packLogRes = packLog;}return packLogRes;}}

Employee.java

package com.bijian.study;import java.util.ArrayList;import java.util.List;public class Employee {private List<PackLog> packLogs = new ArrayList<PackLog>();private IStrategy strategy;public Employee(IStrategy strategy) {this.strategy = strategy;}public void addPackLog(PackLog packLog) {packLogs.add(packLog);}public void stopCar(String vehicleCode) {PackLog packLog = strategy.getSuitablePackLog(packLogs);if(packLog == null) {return;}packLog.stopCar(vehicleCode);}public String getCar(String vehicleCode) {String vehicleCodeRes = null;for(PackLog packLog: packLogs) {vehicleCodeRes = packLog.getCar(vehicleCode);if(vehicleCodeRes != null) {break;}}return vehicleCodeRes;}}

单元测试除了@Before中的构建Employee有点变化之外,其它都没变,如下所示:

EmployeeTest.java

package com.bijian.test;import junit.framework.Assert;import org.junit.Before;import org.junit.Test;import com.bijian.study.CommonStrategy;import com.bijian.study.Employee;import com.bijian.study.PackLog;public class EmployeeTest {private Employee employee;private PackLog packLog1;private PackLog packLog2;    @Before    public void setUp() throws Exception {    employee = new Employee(new CommonStrategy());    packLog1 = new PackLog("X", 1);    packLog2 = new PackLog("Y", 2);    employee.addPackLog(packLog1);    employee.addPackLog(packLog2);    }    @Test    public void test_normal_stop_car() {        String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);            Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-1, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-0, packLog2.getRemainStopCarNum());    }        @Test    public void test_normal_get_car() {        String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);    String vehicleCodeRes = employee.getCar("粤B-123456");        Assert.assertEquals("粤B-123456", vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-0, packLog2.getRemainStopCarNum());    }        private void given_init_data_for_packLog1(int num) {    for(int i=0;i<num;i++) {packLog1.stopCar("粤B-00000" + i);}    }        private void given_init_data_for_packLog2(int num) {    for(int i=0;i<num;i++) {packLog2.stopCar("粤B-00001" + i);}    }        @Test    public void test_full_packlog_then_stop_car_fail() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(2);        String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);    String vehicleCodeRes = employee.getCar("粤B-123456");        Assert.assertNull(vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(0, packLog2.getRemainStopCarNum());    }        @Test    public void test_full_packlog_then_stop_car_success_after_get_a_car() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(1);    String vehicleCode = "粤B-123456";    employee.stopCar(vehicleCode);        employee.getCar(vehicleCode);    String vehicleCode2 = "粤B-333333";    employee.stopCar(vehicleCode2);    int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();    boolean isStopInThisPackLog = packLog2.isStopInThisPackLog(vehicleCode2);        Assert.assertEquals(0, packLog1RemainStopCarNum);    Assert.assertEquals(0, packLog2RemainStopCarNum);    Assert.assertTrue(isStopInThisPackLog);    }        @Test    public void test_empty_packlog_then_get_car() {        String vehicleCode = "粤B-123456";    String resVehicleCode = employee.getCar(vehicleCode);        int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();        Assert.assertNull(resVehicleCode);    Assert.assertEquals(1, packLog1RemainStopCarNum);    Assert.assertEquals(2, packLog2RemainStopCarNum);    }}

SmartEmployeeTest.java

package com.bijian.test;import junit.framework.Assert;import org.junit.Before;import org.junit.Test;import com.bijian.study.Employee;import com.bijian.study.PackLog;import com.bijian.study.SmartStrategy;public class SmartEmployeeTest {private Employee smartEmployee;private PackLog packLog1;private PackLog packLog2;    @Before    public void setUp() throws Exception {    smartEmployee = new Employee(new SmartStrategy());    packLog1 = new PackLog("X", 1);    packLog2 = new PackLog("Y", 2);    smartEmployee.addPackLog(packLog1);    smartEmployee.addPackLog(packLog2);    }    @Test    public void test_normal_stop_car() {    String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);        Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-1, packLog2.getRemainStopCarNum());    }        @Test    public void test_normal_get_car() {    String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);    String vehicleCodeRes = smartEmployee.getCar(vehicleCode);                Assert.assertEquals("粤B-123456", vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(1-0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(2-0, packLog2.getRemainStopCarNum());    }        private void given_init_data_for_packLog1(int num) {    for(int i=0;i<num;i++) {packLog1.stopCar("粤B-00000" + i);}    }        private void given_init_data_for_packLog2(int num) {    for(int i=0;i<num;i++) {packLog2.stopCar("粤B-00001" + i);}    }        @Test    public void test_full_packlog_then_stop_car_fail() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(2);        String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);    String vehicleCodeRes = smartEmployee.getCar("粤B-123456");        Assert.assertNull(vehicleCodeRes);    Assert.assertEquals("X", packLog1.getName());        Assert.assertEquals(0, packLog1.getRemainStopCarNum());        Assert.assertEquals("Y", packLog2.getName());        Assert.assertEquals(0, packLog2.getRemainStopCarNum());    }        @Test    public void test_full_packlog_then_stop_car_success_after_get_a_car() {        given_init_data_for_packLog1(1);    given_init_data_for_packLog2(1);    String vehicleCode = "粤B-123456";    smartEmployee.stopCar(vehicleCode);        smartEmployee.getCar(vehicleCode);    String vehicleCode2 = "粤B-333333";    smartEmployee.stopCar(vehicleCode2);    int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();    boolean isStopInThisPackLog = packLog2.isStopInThisPackLog(vehicleCode2);        Assert.assertEquals(0, packLog1RemainStopCarNum);    Assert.assertEquals(0, packLog2RemainStopCarNum);    Assert.assertTrue(isStopInThisPackLog);    }        @Test    public void test_empty_packlog_then_get_car() {        String vehicleCode = "粤B-123456";    String resVehicleCode = smartEmployee.getCar(vehicleCode);        int packLog1RemainStopCarNum = packLog1.getRemainStopCarNum();    int packLog2RemainStopCarNum = packLog2.getRemainStopCarNum();        Assert.assertNull(resVehicleCode);    Assert.assertEquals(1, packLog1RemainStopCarNum);    Assert.assertEquals(2, packLog2RemainStopCarNum);    }}

 

敏捷教练:http://www.jackyshen.com/

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!

留言需要登陆哦

技术博客集 - 网站简介:
前后端技术:
后端基于Hyperf2.1框架开发,前端使用Bootstrap可视化布局系统生成

网站主要作用:
1.编程技术分享及讨论交流,内置聊天系统;
2.测试交流框架问题,比如:Hyperf、Laravel、TP、beego;
3.本站数据是基于大数据采集等爬虫技术为基础助力分享知识,如有侵权请发邮件到站长邮箱,站长会尽快处理;
4.站长邮箱:[email protected];

      订阅博客周刊 去订阅

文章归档

文章标签

友情链接

Auther ·HouTiZong
侯体宗的博客
© 2020 zongscan.com
版权所有ICP证 : 粤ICP备20027696号
PHP交流群 也可以扫右边的二维码
侯体宗的博客