重学 Java 设计模式:实战外观模式

简介: 设计模式是解决程序中不合理、不易于扩展、不易于维护的问题,也是干掉大部分ifelse的利器,在我们常用的框架中基本都会用到大量的设计模式来构建组件,这样也能方便框架的升级和功能的扩展。但!如果不能合理的设计以及乱用设计模式,会导致整个编程变得更加复杂难维护,也就是我们常说的;反设计、过渡设计。而这部分设计能力也是从实践的项目中获取的经验,不断的改造优化摸索出的最合理的方式,应对当前的服务体量。

目录


  • 一、前言
  • 二、开发环境
  • 三、外观模式介绍
  • 四、案例场景模拟
  • 1. 场景模拟工程
  • 2. 场景简述
  • 五、用一坨坨代码实现
  • 1. 工程结构
  • 2. 代码实现
  • 六、外观模式重构代码
  • 1. 工程结构
  • 2. 代码实现
  • 3. 测试验证
  • 七、总结


一、前言

你感受到的容易,一定有人为你承担不容易

这句话更像是描述生活的,许许多多的磕磕绊绊总有人为你提供躲雨的屋檐和避风的港湾。其实编程开发的团队中也一样有人只负责CRUD中的简单调用,去使用团队中高级程序员开发出来的核心服务和接口。这样的编程开发对于初期刚进入程序员行业的小伙伴来说锻炼锻炼还是不错的,但随着开发的日子越来越久一直做这样的事情就很难得到成长,也想努力的去做一些更有难度的承担,以此来增强个人的技术能力。

没有最好的编程语言,语言只是工具

刀枪棍棒、斧钺钩叉、包子油条、盒子麻花,是「语言」。五郎八卦棍、十二路弹腿、洪家铁线拳,是「设计」。记得叶问里有一句台词是:金山找:今天我北方拳术,输给你南方拳术了。叶问:你错了,不是南北拳的问题,是你的问题。所以当你编程开发写的久了,就不会再特别在意用的语言,而是为目标服务,用最好的设计能力也就是编程的智慧做出做最完美的服务。这也就是编程人员的价值所在!

设计与反设计以及过渡设计

设计模式是解决程序中不合理、不易于扩展、不易于维护的问题,也是干掉大部分ifelse的利器,在我们常用的框架中基本都会用到大量的设计模式来构建组件,这样也能方便框架的升级和功能的扩展。但!如果不能合理的设计以及乱用设计模式,会导致整个编程变得更加复杂难维护,也就是我们常说的;反设计过渡设计。而这部分设计能力也是从实践的项目中获取的经验,不断的改造优化摸索出的最合理的方式,应对当前的服务体量。

二、开发环境

  1. JDK 1.8
  2. Idea + Maven
  3. SpringBoot 2.1.2.RELEASE
  4. 涉及工程三个,可以通过关注「公众号」bugstack虫洞栈,回复源码下载获取(打开获取的链接,找到序号18)
工程 描述
itstack-demo-design-10-00 场景模拟工程;模拟一个提供接口服务的SpringBoot工程
itstack-demo-design-10-01 使用一坨代码实现业务需求
itstack-demo-design-10-02 通过设计模式开发为中间件,包装通用型核心逻辑

三、外观模式介绍

15.jpg

外观模式,图片来自 refactoringguru.cn

外观模式也叫门面模式,主要解决的是降低调用方的使用接口的复杂逻辑组合。这样调用方与实际的接口提供方提供方提供了一个中间层,用于包装逻辑提供API接口。有些时候外观模式也被用在中间件层,对服务中的通用性复杂逻辑进行中间件层包装,让使用方可以只关心业务开发。

「那么这样的模式在我们的所见产品功能中也经常遇到」,就像几年前我们注册一个网站时候往往要添加很多信息,包括;姓名、昵称、手机号、QQ、邮箱、住址、单身等等,但现在注册成为一个网站的用户只需要一步即可,无论是手机号还是微信也都提供了这样的登录服务。而对于服务端应用开发来说以前是提供了一个整套的接口,现在注册的时候并没有这些信息,那么服务端就需要进行接口包装,在前端调用注册的时候服务端获取相应的用户信息(从各个渠道),如果获取不到会让用户后续进行补全(营销补全信息给奖励),以此来拉动用户的注册量和活跃度。

四、案例场景模拟

16.jpg

场景模拟;所有服务添加白名单校验

「在本案例中我们模拟一个将所有服务接口添加白名单的场景」

在项目不断壮大发展的路上,每一次发版上线都需要进行测试,而这部分测试验证一般会进行白名单开量或者切量的方式进行验证。那么如果在每一个接口中都添加这样的逻辑,就会非常麻烦且不易于维护。另外这是一类具备通用逻辑的共性需求,非常适合开发成组件,以此来治理服务,让研发人员更多的关心业务功能开发。

一般情况下对于外观模式的使用通常是用在复杂或多个接口进行包装统一对外提供服务上,此种使用方式也相对简单在我们平常的业务开发中也是最常用的。你可能经常听到把这两个接口包装一下,但在本例子中我们把这种设计思路放到中间件层,让服务变得可以统一控制。

1. 场景模拟工程

itstack-demo-design-10-00
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design
    │   │       ├── domain
    │   │       │ └── UserInfo.java
    │   │       ├── web 
    │   │       │ └── HelloWorldController.java
    │   │       └── HelloWorldApplication.java
    │   └── resources 
    │       └── application.yml 
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java
  • 这是一个SpringBootHelloWorld工程,在工程中提供了查询用户信息的接口HelloWorldController.queryUserInfo,为后续扩展此接口的白名单过滤做准备。

2. 场景简述

2.1 定义基础查询接口

@RestController
public class HelloWorldController {
    @Value("${server.port}")
    private int port;
    /**
     * key:需要从入参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使用
     * returnJson:预设拦截时返回值,是返回对象的Json
     *
     * http://localhost:8080/api/queryUserInfo?userId=1001
     * http://localhost:8080/api/queryUserInfo?userId=小团团
     */
    @RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
    public UserInfo queryUserInfo(@RequestParam String userId) {
        return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号");
    }
}
  • 这里提供了一个基本的查询服务,通过入参userId,查询用户信息。后续就需要在这里扩展白名单,只有指定用户才可以查询,其他用户不能查询。

2.2 设置Application启动类

@SpringBootApplication
@Configuration
public class HelloWorldApplication {
    public static void main(String[] args) {
        SpringApplication.run(HelloWorldApplication.class, args);
    }
}
  • 这里是通用的SpringBoot启动类。需要添加的是一个配置注解@Configuration,为了后续可以读取白名单配置。

五、用一坨坨代码实现

一般对于此种场景最简单的做法就是直接修改代码

累加if块几乎是实现需求最快也是最慢的方式,「快」是修改当前内容很快,「慢」是如果同类的内容几百个也都需要如此修改扩展和维护会越来越慢。

1. 工程结构

itstack-demo-design-10-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── HelloWorldController.java
  • 以上的实现是模拟一个Api接口类,在里面添加白名单功能,但类似此类的接口会有很多都需要修改,所以这也是不推荐使用此种方式的重要原因。

2. 代码实现

public class HelloWorldController {
    public UserInfo queryUserInfo(@RequestParam String userId) {
        // 做白名单拦截
        List<String> userList = new ArrayList<String>();
        userList.add("1001");
        userList.add("aaaa");
        userList.add("ccc");
        if (!userList.contains(userId)) {
            return new UserInfo("1111", "非白名单可访问用户拦截!");
        }
        return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号");
    }
}
  • 在这里白名单的代码占据了一大块,但它又不是业务中的逻辑,而是因为我们上线过程中需要做的开量前测试验证。
  • 如果你日常对待此类需求经常是这样开发,那么可以按照此设计模式进行优化你的处理方式,让后续的扩展和摘除更加容易。

六、外观模式重构代码

接下来使用外观器模式来进行代码优化,也算是一次很小的重构。

这次重构的核心是使用外观模式也可以说门面模式,结合SpringBoot中的自定义starter中间件开发的方式,统一处理所有需要白名单的地方。

后续接下来的实现中,会涉及的知识;

  1. SpringBoot的starter中间件开发方式。
  2. 面向切面编程和自定义注解的使用。
  3. 外部自定义配置信息的透传,SpringBoot与Spring不同,对于此类方式获取白名单配置存在差异。

1. 工程结构

itstack-demo-design-10-02
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.demo.design.door
    │   │       ├── annotation
    │   │       │ └── DoDoor.java 
    │   │       ├── config
    │   │       │ ├── StarterAutoConfigure.java
    │   │       │ ├── StarterService.java
    │   │       │ └── StarterServiceProperties.java
    │   │       └── DoJoinPoint.java
    │   └── resources 
    │       └── META_INF
    │           └── spring.factories
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

「门面模式模型结构」

17.jpg

门面模式模型结构

  • 以上是外观模式的中间件实现思路,右侧是为了获取配置文件,左侧是对于切面的处理。
  • 门面模式可以是对接口的包装提供出接口服务,也可以是对逻辑的包装通过自定义注解对接口提供服务能力。

2. 代码实现

2.1 配置服务类

public class StarterService {
    private String userStr;
    public StarterService(String userStr) {
        this.userStr = userStr;
    }
    public String[] split(String separatorChar) {
        return StringUtils.split(this.userStr, separatorChar);
    }
}
  • 以上类的内容较简单只是为了获取配置信息。

2.2 配置类注解定义

@ConfigurationProperties("itstack.door")
public class StarterServiceProperties {
    private String userStr;
    public String getUserStr() {
        return userStr;
    }
    public void setUserStr(String userStr) {
        this.userStr = userStr;
    }
}
  • 用于定义好后续在 application.yml 中添加 itstack.door 的配置信息。

2.3 自定义配置类信息获取

@Configuration
@ConditionalOnClass(StarterService.class)
@EnableConfigurationProperties(StarterServiceProperties.class)
public class StarterAutoConfigure {
    @Autowired
    private StarterServiceProperties properties;
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(prefix = "itstack.door", value = "enabled", havingValue = "true")
    StarterService starterService() {
        return new StarterService(properties.getUserStr());
    }
}
  • 以上代码是对配置的获取操作,主要是对注解的定义;@Configuration@ConditionalOnClass@EnableConfigurationProperties,这一部分主要是与SpringBoot的结合使用。

2.4 切面注解定义

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoDoor {
    String key() default "";
    String returnJson() default "";
}
  • 定义了外观模式门面注解,后续就是此注解添加到需要扩展白名单的方法上。
  • 这里提供了两个入参,「key」:获取某个字段例如用户ID、「returnJson」:确定白名单拦截后返回的具体内容。

2.5 白名单切面逻辑

@Aspect
@Component
public class DoJoinPoint {
    private Logger logger = LoggerFactory.getLogger(DoJoinPoint.class);
    @Autowired
    private StarterService starterService;
    @Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")
    public void aopPoint() {
    }
    @Around("aopPoint()")
    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
        //获取内容
        Method method = getMethod(jp);
        DoDoor door = method.getAnnotation(DoDoor.class);
        //获取字段值
        String keyValue = getFiledValue(door.key(), jp.getArgs());
        logger.info("itstack door handler method:{} value:{}", method.getName(), keyValue);
        if (null == keyValue || "".equals(keyValue)) return jp.proceed();
        //配置内容
        String[] split = starterService.split(",");
        //白名单过滤
        for (String str : split) {
            if (keyValue.equals(str)) {
                return jp.proceed();
            }
        }
        //拦截
        return returnObject(door, method);
    }
    private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        return getClass(jp).getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
    }
    private Class<? extends Object> getClass(JoinPoint jp) throws NoSuchMethodException {
        return jp.getTarget().getClass();
    }
    //返回对象
    private Object returnObject(DoDoor doGate, Method method) throws IllegalAccessException, InstantiationException {
        Class<?> returnType = method.getReturnType();
        String returnJson = doGate.returnJson();
        if ("".equals(returnJson)) {
            return returnType.newInstance();
        }
        return JSON.parseObject(returnJson, returnType);
    }
    //获取属性值
    private String getFiledValue(String filed, Object[] args) {
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (null == filedValue || "".equals(filedValue)) {
                    filedValue = BeanUtils.getProperty(arg, filed);
                } else {
                    break;
                }
            } catch (Exception e) {
                if (args.length == 1) {
                    return args[0].toString();
                }
            }
        }
        return filedValue;
    }
}
  • 这里包括的内容较多,核心逻辑主要是;Object doRouter(ProceedingJoinPoint jp),接下来我们分别介绍。

「@Pointcut("@annotation(org.itstack.demo.design.door.annotation.DoDoor)")」

定义切面,这里采用的是注解路径,也就是所有的加入这个注解的方法都会被切面进行管理。

「getFiledValue」

获取指定key也就是获取入参中的某个属性,这里主要是获取用户ID,通过ID进行拦截校验。

「returnObject」

返回拦截后的转换对象,也就是说当非白名单用户访问时则返回一些提示信息。

「doRouter」

切面核心逻辑,这一部分主要是判断当前访问的用户ID是否白名单用户,如果是则放行jp.proceed();,否则返回自定义的拦截提示信息。

3. 测试验证

这里的测试我们会在工程:itstack-demo-design-10-00中进行操作,通过引入jar包,配置注解的方式进行验证。

3.1 引入中间件POM配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>itstack-demo-design-10-02</artifactId>
</dependency>
  • 打包中间件工程,给外部提供jar包服务

3.2 配置application.yml

# 自定义中间件配置
itstack:
  door:
    enabled: true
    userStr: 1001,aaaa,ccc #白名单用户ID,多个逗号隔开
  • 这里主要是加入了白名单的开关和白名单的用户ID,逗号隔开。

3.3 在Controller中添加自定义注解

/**
 * http://localhost:8080/api/queryUserInfo?userId=1001
 * http://localhost:8080/api/queryUserInfo?userId=小团团
 */
@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")
@RequestMapping(path = "/api/queryUserInfo", method = RequestMethod.GET)
public UserInfo queryUserInfo(@RequestParam String userId) {
    return new UserInfo("虫虫:" + userId, 19, "天津市南开区旮旯胡同100号");
}
  • 这里核心的内容主要是自定义的注解的添加@DoDoor,也就是我们的外观模式中间件化实现。
  • key:需要从入参取值的属性字段,如果是对象则从对象中取值,如果是单个值则直接使用。
  • returnJson:预设拦截时返回值,是返回对象的Json。

3.4 启动SpringBoot

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.2.RELEASE)
2020-06-11 23:56:55.451  WARN 65228 --- [           main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2020-06-11 23:56:55.531  INFO 65228 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-11 23:56:55.533  INFO 65228 --- [           main] o.i.demo.design.HelloWorldApplication    : Started HelloWorldApplication in 1.688 seconds (JVM running for 2.934)
  • 启动正常,SpringBoot已经启动可以对外提供服务。

3.5 访问接口接口测试

「白名单用户访问」

http://localhost:8080/api/queryUserInfo?userId=1001

{"code":"0000","info":"success","name":"虫虫:1001","age":19,"address":"天津市南开区旮旯胡同100号"}
  • 此时的测试结果正常,可以拿到接口数据。

「非白名单用户访问」

http://localhost:8080/api/queryUserInfo?userId=小团团

{"code":"1111","info":"非白名单可访问用户拦截!","name":null,"age":null,"address":null}
  • 这次我们把userId换成小团团,此时返回的信息已经是被拦截的信息。而这个拦截信息正式我们自定义注解中的信息:@DoDoor(key = "userId", returnJson = "{\"code\":\"1111\",\"info\":\"非白名单可访问用户拦截!\"}")

七、总结

  • 以上我们通过中间件的方式实现外观模式,这样的设计可以很好的增强代码的隔离性,以及复用性,不仅使用上非常灵活也降低了每一个系统都开发这样的服务带来的风险。
  • 可能目前你看这只是非常简单的白名单控制,是否需要这样的处理。但往往一个小小的开始会影响着后续无限的扩展,实际的业务开发往往也要复杂的很多,不可能如此简单。因而使用设计模式来让代码结构更加干净整洁。
  • 很多时候不是设计模式没有用,而是自己编程开发经验不足导致即使学了设计模式也很难驾驭。毕竟这些知识都是经过一些实际操作提炼出来的精华,但如果你可以按照本系列文章中的案例方式进行学习实操,还是可以增强这部分设计能力的。
目录
相关文章
|
14天前
|
设计模式 存储 缓存
「全网最细 + 实战源码案例」设计模式——享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在减少大量相似对象的内存消耗。通过分离对象的内部状态(可共享、不变)和外部状态(依赖环境、变化),它有效减少了内存使用。适用于存在大量相似对象且需节省内存的场景。模式优点包括节省内存和提高性能,但会增加系统复杂性。实现时需将对象成员变量拆分为内在和外在状态,并通过工厂类管理享元对象。
147 83
|
10天前
|
设计模式 存储 算法
「全网最细 + 实战源码案例」设计模式——命令模式
命令模式(Command Pattern)是一种行为型设计模式,将请求封装成独立对象,从而解耦请求方与接收方。其核心结构包括:Command(命令接口)、ConcreteCommand(具体命令)、Receiver(接收者)和Invoker(调用者)。通过这种方式,命令的执行、撤销、排队等操作更易扩展和灵活。 适用场景: 1. 参数化对象以操作。 2. 操作放入队列或远程执行。 3. 实现回滚功能。 4. 解耦调用者与接收者。 优点: - 遵循单一职责和开闭原则。 - 支持命令组合和延迟执行。 - 可实现撤销、恢复功能。 缺点: - 增加复杂性和类数量。
48 14
「全网最细 + 实战源码案例」设计模式——命令模式
|
10天前
|
设计模式 存储 Java
「全网最细 + 实战源码案例」设计模式——责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,允许将请求沿着处理者链进行发送。每个处理者可以处理请求或将其传递给下一个处理者,从而实现解耦和灵活性。其结构包括抽象处理者(Handler)、具体处理者(ConcreteHandler)和客户端(Client)。适用于不同方式处理不同种类请求、按顺序执行多个处理者、以及运行时改变处理者及其顺序的场景。典型应用包括日志处理、Java Web过滤器、权限认证等。
47 13
「全网最细 + 实战源码案例」设计模式——责任链模式
|
12天前
|
设计模式 算法 开发者
「全网最细 + 实战源码案例」设计模式——策略模式
策略模式(Strategy Pattern)是一种行为型设计模式,用于定义一系列可替换的算法或行为,并将它们封装成独立的类。通过上下文持有策略对象,在运行时动态切换算法,提高代码的可维护性和扩展性。适用于需要动态切换算法、避免条件语句、经常扩展算法或保持算法独立性的场景。优点包括符合开闭原则、运行时切换算法、解耦上下文与策略实现、减少条件判断;缺点是增加类数量和策略切换成本。示例中通过定义抽象策略接口和具体策略类,结合上下文类实现动态算法选择。
47 8
「全网最细 + 实战源码案例」设计模式——策略模式
|
12天前
|
设计模式 SQL 算法
「全网最细 + 实战源码案例」设计模式——模板方法模式
模板方法模式是一种行为型设计模式,定义了算法的骨架并在父类中实现不变部分,将可变部分延迟到子类实现。通过这种方式,它避免了代码重复,提高了复用性和扩展性。具体步骤由抽象类定义,子类实现特定逻辑。适用于框架设计、工作流和相似算法结构的场景。优点包括代码复用和符合开闭原则,缺点是可能违反里氏替换原则且灵活性较低。
60 7
「全网最细 + 实战源码案例」设计模式——模板方法模式
|
24天前
|
设计模式
「全网最细 + 实战源码案例」设计模式——模式扩展(配置工厂)
该设计通过配置文件和反射机制动态选择具体工厂,减少硬编码依赖,提升系统灵活性和扩展性。配置文件解耦、反射创建对象,新增产品族无需修改客户端代码。示例中,`CoffeeFactory`类加载配置文件并使用反射生成咖啡对象,客户端调用时只需指定名称即可获取对应产品实例。
86 40
|
18天前
|
设计模式 Java 开发者
「全网最细 + 实战源码案例」设计模式——适配器模式
适配器模式(Adapter Pattern)是一种结构型设计模式,通过引入适配器类将一个类的接口转换为客户端期望的另一个接口,使原本因接口不兼容而无法协作的类能够协同工作。适配器模式分为类适配器和对象适配器两种,前者通过多重继承实现,后者通过组合方式实现,更常用。该模式适用于遗留系统改造、接口转换和第三方库集成等场景,能提高代码复用性和灵活性,但也可能增加代码复杂性和性能开销。
67 28
|
14天前
|
设计模式 存储 安全
「全网最细 + 实战源码案例」设计模式——组合模式
组合模式(Composite Pattern)是一种结构型设计模式,用于将对象组合成树形结构以表示“部分-整体”的层次结构。它允许客户端以一致的方式对待单个对象和对象集合,简化了复杂结构的处理。组合模式包含三个主要组件:抽象组件(Component)、叶子节点(Leaf)和组合节点(Composite)。通过这种模式,客户端可以统一处理简单元素和复杂元素,而无需关心其内部结构。适用于需要实现树状对象结构或希望以相同方式处理简单和复杂元素的场景。优点包括支持树形结构、透明性和遵循开闭原则;缺点是可能引入不必要的复杂性和过度抽象。
72 22
|
18天前
|
设计模式 缓存 Java
「全网最细 + 实战源码案例」设计模式——代理模式
代理模式(Proxy Pattern)是一种结构型设计模式,通过代理对象控制对目标对象的访问并添加额外功能。它分为静态代理和动态代理,后者包括JDK动态代理和CGLIB动态代理。JDK动态代理基于接口反射生成代理类,而CGLIB通过继承目标类生成子类。代理模式适用于延迟初始化、访问控制、远程服务、日志记录和缓存等场景,优点是职责分离、符合开闭原则和提高安全性,缺点是增加系统复杂性。
69 25
|
16天前
|
设计模式 前端开发 数据库
「全网最细 + 实战源码案例」设计模式——桥接模式
桥接模式(Bridge Pattern)是一种结构型设计模式,通过将抽象部分与实现部分分离,使它们可以独立变化,从而降低代码耦合度,避免类爆炸,提高可扩展性。其结构包括实现类接口、具体实现类、抽象类和精确抽象类。适用于多维度扩展类、隐藏实现细节、简化庞杂类以及运行时切换实现方法的场景。优点包括高扩展性、隐藏实现细节、遵循开闭原则和单一职责原则;缺点是可能增加代码复杂度。示例中展示了不同操作系统播放不同格式视频文件的实现。
46 19

热门文章

最新文章