• 设计模式之道-模板方法模式


    模板方法模式

    简介

    模板方法模式是一种行为型设计模式,该设计模式的核心在于通过抽象出一套相对标准的处理步骤,并可灵活的将任意步骤交给子类去进行扩展,使得可以在不改变整体业务处理流程的前提下,通过定义不同的子类实现即可完成业务处理的扩展。

    我们可以举个简单的例子,比如对于下面定义的method方法中调用的a、b、c三个子方法,可以通过不同的子类实现来完成不同业务逻辑的处理。

    public abstract class Temp {
    
        public final void method() {
            a();
            b();
            c();
        }
    
        protected abstract void c();
    
        protected abstract void b();
    
        protected abstract void a();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    还可以这样定义,此时相当于b方法在父类中有一套默认的处理,子类可以根据需要选择重写或者不重写。

    public abstract class Temp {
    
        public final void method() {
            a();
            b();
            c();
        }
    
        protected abstract void c();
    
        protected void b() {
            // 默认处理逻辑。。。
        }
    
        protected abstract void a();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    当然,还可以将b方法声明为private或者加上final关键字从而禁止子类重写,此时b方法的逻辑就完全由父类统一管理。

    public abstract class Temp {
    
        public final void method() {
            a();
            b();
            c();
        }
    
        protected abstract void c();
    
        private void b() {
            // 固定处理逻辑。。。
        }
    
        protected abstract void a();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    作用

    模板方法模式主要有两大作用:复用和扩展。

    复用:复用指的是像method这样的方法,所有子类都可以拿来使用,复用该方法中定义的这套处理逻辑。

    扩展:扩展的能力就更加强大了,狭义上可以针对代码进行扩展,子类可以独立增加功能逻辑,而不影响其他的子类,符合开闭原则,广义上可以针对整个框架进行扩展,比如像下面这段代码逻辑:

    public class Temp {
    
        public final void method() {
            a();
            b();
            c();
            d();
        }
    
        protected void c() {
            // 默认处理逻辑。。。
        };
    
        private void b() {
            // 固定处理逻辑。。。
        }
    
        protected void a() {
             // 默认处理逻辑。。。
        }
        
        protected void d() {
            // 强制子类必须重写
            throw new UnsupportedOperationException();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    框架默认可以直接使用,但同时也预留了acd三个方法的扩展能力,且d方法还通过抛出异常的方式,强制要求子类必须重写,所以现在完全可以通过方法重写的方式实现框架的功能扩展。
    这种框架扩展的方式的典型案例就是Servlet中定义的service方法,该方法分别预留了doGetdoPost等扩展方法。

    模板方法模式的缺点

    从另一个角度来说,设计模式本身实际上并不存在什么缺点,真正导致出现这些问题的原因还是使用设计模式的方式,尤其是新手在刚了解到设计模式的时候,往往会试图到处找场景去套用各种设计模式,甚至一个方法能用上好几种,这就是典型的手里拿个锤子,看什么都是钉子。所以,如果按照这样的使用方式,通常就会导致子类或者实现类非常多,但逻辑却很少,或相似;方法为了兼容各种场景而过于抽象,导致代码复杂度增加,可阅读性也变差。

    针对模板方式模式来说,因为通常情况下是通过继承机制来实现业务流程的不变部分和可变部分的分离,因此,如果可变部分的业务逻辑并不复杂,或者不变部分和可变部分的关系不清晰时,就不适合用模板方法模式了。

    模板方法模式的应用场景

    业务的整体处理流程是固定的,但其中的个别部分是易变的,或者可扩展的,此时就可以使用模板方法模式,下面我们分别举一些常见的业务场景和开源框架的应用来说明。

    业务场景

    订单结算场景

    订单结算在电商平台是非常常见的功能,整个结算过程一定会包含:订单生成、库存校验、费用计算、结果通知,但比如其中费用计算则可能在优惠券、折扣、运费等地方又有所不同,因此可以将整个结算过程抽象为一个模板类,具体的结算类只需要继承该模板类,并实现具体的计算规则即可。

    任务活动场景

    常见的任务活动,主要包含三步骤:任务事件接收、任务规则匹配、任务奖励触发,而往往事件接收和奖励触发都是比较统一的,规则匹配则跟具体的任务相关,所以可以用模板方法模式来实现。

    开源框架中的应用

    Spring MVC

    handleRequestInternal由子类实现

    public abstract class AbstractController extends WebContentGenerator implements Controller {
    
    	@Override
    	@Nullable
    	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    		if (HttpMethod.OPTIONS.matches(request.getMethod())) {
    			response.setHeader("Allow", getAllowHeader());
    			return null;
    		}
    
    		// Delegate to WebContentGenerator for checking and preparing.
    		checkRequest(request);
    		prepareResponse(response);
    
    		// Execute handleRequestInternal in synchronized block if required.
    		if (this.synchronizeOnSession) {
    			HttpSession session = request.getSession(false);
    			if (session != null) {
    				Object mutex = WebUtils.getSessionMutex(session);
    				synchronized (mutex) {
    					return handleRequestInternal(request, response);
    				}
    			}
    		}
    
    		return handleRequestInternal(request, response);
    	}
    
    	/**
    	 * Template method. Subclasses must implement this.
    	 * The contract is the same as for {@code handleRequest}.
    	 * @see #handleRequest
    	 */
    	@Nullable
    	protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
    			throws Exception;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    MyBatis

    BaseExecutorMyBatis中经典的模板方法模式应用,其主要是用来执行SQLquery方法是模板方法的主流程,doQuery方法是其留给子类实现的。

    image.png

    public abstract class BaseExecutor implements Executor {
        
      // 几个do开头的方法都是留给子类实现的
      protected abstract int doUpdate(MappedStatement ms, Object parameter)
          throws SQLException;
    
      protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
          throws SQLException;
    
      protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
          throws SQLException;
    
      protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
          throws SQLException;  
        
        
      @Override
      public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
     }
    
      @SuppressWarnings("unchecked")
      @Override
      public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
          clearLocalCache();
        }
        List<E> list;
        try {
          queryStack++;
          list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
          if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
          } else {
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
          }
        } finally {
          queryStack--;
        }
        if (queryStack == 0) {
          for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
          }
          // issue #601
          deferredLoads.clear();
          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
          }
        }
        return list;
      }
    }
    
    
      private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
          // 具体query方式,交由子类实现
          list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
          localCache.removeObject(key);
        }
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
          localOutputParameterCache.putObject(key, parameter);
        }
        return list;
      }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    JDK AbstractCollection抽象类

    AbstractCollection中实现了Set接口中定义的addAll方法,该方法又是基于add方法来实现的,具体代码如下所示:

    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    AbstractCollection本身并不处理add方法,而是希望子类自己去实现,如果调用者不小心直接调用了AbstractCollectionadd方法,则会直接抛出异常。

    public boolean add(E e) {
        throw new UnsupportedOperationException();
    }
    
    • 1
    • 2
    • 3

    对比回调和Hook模式

    回调和Hook这两种模式,在一定程度上也能起到模板方法模式的效果,他们都可以在一套流程中预留某个扩展点,然后将这个扩展点交由请求方自己来实现,最常见的就是支付场景,在请求支付的时候,往往是不会同步等待支付结果的,而是在请求的同时注册一个回调接口,这样三方支付系统完成支付之后,就会回调这个接口来完成支付结果的通知。

    虽然从应用场景上来回调或者Hook模式和模板方法模式差不多,但从代码实现方式来看,却有很大差异,模板方法模式是基于继承的方式来实现的,这实际上是有很大的局限性,而回调或者Hook模式则是基于组合方式来实现的,我们都知道组合优于继承,其次,回调或者Hook模式还可以基于匿名类的方式来实现,不用事先定义类,显然更加灵活,当然,回调也有其问题,使用不当,容易出现调用关系混乱,系统层次混乱等现象。

    关于组合优先于继承

    继承是实现代码重用的重要手段之一,但并非是实现代码重用的最佳方式,继承打破了封装性,因此很容易在使用时产生问题,为了更好的说明这一点,我们来举个例子,假设我们现在需要为HashSet``添加一个计数功能,即看看HashSet自创建以来,一共被添加过多少个元素,我们可以用下面这种方式来实现:

    public class CountHashSet<E> extends HashSet<E> {
    
        private int addCount = 0;
    
        public CountHashSet() {
    
        }
    
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
    
        @Override
        public boolean addAll(Collection<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
    
        public int getAddCount() {
            return addCount;
        }
    }
    
    class Main {
        public static void main(String[] args) {
            CountHashSet<Integer> countHashSet = new CountHashSet<>();
            countHashSet.addAll(Arrays.asList(1, 2, 3));
            System.out.println(countHashSet.getAddCount());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    很遗憾最终输出结果并不是3,而是6,问题就在于前面介绍的AbstractCollection关于addAll的实现方式,很明显在addAll方法中调用add方法时被重复统计了,你不能因此说是addAll的实现方法有问题。

    也许你只要像下面这段代码一样,就能修复这个问题,但这又依赖一个事实:addAll方法是在add方法中实现的,这实际上并不是什么标准,你也不能保证在之后的版本中不会发生变化。

    public class CountHashSet<E> extends HashSet<E> {
    
        private int addCount = 0;
    
        public CountHashSet() {
    
        }
    
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
    
    //    @Override
    //    public boolean addAll(Collection c) {
    //        addCount += c.size();
    //        return super.addAll(c);
    //    }
    
        public int getAddCount() {
            return addCount;
        }
    }
    
    class Main {
        public static void main(String[] args) {
            CountHashSet<Integer> countHashSet = new CountHashSet<>();
            countHashSet.addAll(Arrays.asList(1, 2, 3));
            System.out.println(countHashSet.getAddCount());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    使用组合的方式

    public class ForwardingSet<E> implements Set<E> {
    
        private final Set<E> s;
    
        public ForwardingSet(Set<E> s) {
            this.s = s;
        }
    
    
        @Override
        public int size() {
            return s.size();
        }
    
        @Override
        public boolean isEmpty() {
            return s.isEmpty();
        }
    
        @Override
        public boolean contains(Object o) {
            return s.contains(o);
        }
    
        @Override
        public Iterator<E> iterator() {
            return s.iterator();
        }
    
        @Override
        public Object[] toArray() {
            return s.toArray();
        }
    
        @Override
        public <T> T[] toArray(T[] a) {
            return s.toArray(a);
        }
    
        @Override
        public boolean add(E e) {
            return s.add(e);
        }
    
        @Override
        public boolean remove(Object o) {
            return s.remove(o);
        }
    
        @Override
        public boolean containsAll(Collection<?> c) {
            return s.containsAll(c);
        }
    
        @Override
        public boolean addAll(Collection<? extends E> c) {
            return s.addAll(c);
        }
    
        @Override
        public boolean retainAll(Collection<?> c) {
            return s.retainAll(c);
        }
    
        @Override
        public boolean removeAll(Collection<?> c) {
            return s.removeAll(c);
        }
    
        @Override
        public void clear() {
            s.clear();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    class CountSet<E> extends ForwardingSet<E> {
    
        private int addCount = 0;
    
        public CountSet(Set<E> s) {
            super(s);
        }
    
        @Override
        public boolean add(E e) {
            addCount++;
            return super.add(e);
        }
    
        @Override
        public boolean addAll(Collection<? extends E> c) {
            addCount += c.size();
            return super.addAll(c);
        }
    
        public int getAddCount() {
            return addCount;
        }
    }
    
    class Main {
        public static void main(String[] args) {
            CountSet<Integer> countHashSet = new CountSet<>(new HashSet<>());
            countHashSet.addAll(Arrays.asList(1, 2, 3));
            System.out.println(countHashSet.getAddCount());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    看吧,这就是使用组合的威力,组合更像是装饰者模式,他可以在不改变原有类的功能的前提下,轻松实现功能的扩展,最重要的是,他比继承要可靠的多。

    关于设计模式乱用的现象

    最后,再来聊聊关于设计模式乱用的问题,主要突出为以下两个阶段:

    1. 新手:这经常发生在刚接触设计模式不久的阶段,急于找地方使用的情况,开发人员不考虑实际的业务场景,完全是为了用设计模式而用设计模式,甚至是先想好要用什么样的设计模式,然后让业务逻辑尽量往这个模式上去套。
    2. 胜任者:过了新手阶段之后,此时你对设计模式也有一定使用经验了,开始意识到胡乱使用设计模式造成的问题了,懂得了理解业务场景才是关键,那还有什么问题呢?此时的阶段就好比术和道的区别,术是多变的,就像我们常说的23种设计模式一样,而道是不变的,无论哪种设计模式始终都是以几种设计原则为依据,正所谓万变不离其宗,设计模式的使用不应当局限于形式上,要能灵活变换。
    3. 精通者:如果跨过新手阶段的关键在于多写多练的话,那么要跨过胜任者阶段则要多思考了,得道的关键在于领悟。
  • 相关阅读:
    2022-11-16 Acwing每日一题
    Codeforces Round #723 (Div. 2) C2. Potions (Hard Version)
    CDN引入Vue3
    如何将字体添加到 ONLYOFFICE 桌面编辑器8.0
    基于SE-YOLOv5s的绝缘子检测
    Java基础------真实大厂面试题汇总(含答案)
    第十章 Python 自定义模块及导入方法
    【栈应用】简化路径
    response.setHeader的各种用法
    Kotlin高仿微信-第29篇-朋友圈-发布作品(图片)
  • 原文地址:https://blog.csdn.net/CSDN_WYL2016/article/details/133269027