• 《Effective Java 第三版》——《骨架实现类》


    Java Skeletal Implementation/Abstract Interfaces(骨架实现 / 抽象接口) 

    (7 条消息)Java Skeletal Implementation/Abstract Interfaces(骨架实现 / 抽象接口)_Android 技术之家 - CSDN 博客_skeletal implementation java

    前言:这个模式的主要作用是,集接口多继承的优势与抽象类可以减少重复代码的优势于一体。(skeletal implementation,简称 SI)

    一、接口与抽象类的优劣。

    • 接口,可以实现多继承,但抽象类不行。

    • 抽象类,可以有实现,但接口不行。

    我们先一步一步来,先举一个,用接口的例子,再通过分析例子,一步一步进行讲解。

    二、使用接口,规范相同行为。 
    假设,我们要做一个苹果自动贩卖机 (自动贩卖机简称贩卖机) 和葡萄贩卖机。那么,良好的代码设计,肯定不是一上来,就开始创建一个类,然后,开始写方法,做功能。我们应该,首先,定义一个贩卖机的协议 。规范两种不同水果贩卖机的行为。比如,都有启动 start()、贩卖 process()、停止 stop()的功能。接口定义如下:

    /**
     * @author Lyf
     * @describe 贩卖机协议
     */public interface IVending {
     
        // 启动机器
        void start();    // 贩卖水果
        void process();    // 暂停机器
        void stop();
     
    }

    下一步,我们先来创建,苹果贩卖机,代码如下:

    /**
     * @author Lyf
     * @describe 苹果贩卖机
     */public class AppleVending implements IVending {
     
        @Override
        public void start() {
            System.out.println("启动,贩卖机。");
        }    @Override
        public void process() {
            System.out.println("正在卖苹果....");
        }    @Override
        public void stop() {
            System.out.println("关闭,贩卖机。");
        }
     
    }

    下一步,我们再来创建,葡萄贩卖机,代码如下:

    /**
     * @author Lyf
     * @describe 葡萄贩卖机
     */public class GrapeVending implements IVending {
     
        @Override
        public void start() {
            System.out.println("启动,贩卖机。");
        }    @Override
        public void process() {
            System.out.println("正在卖葡萄....");
        }    @Override
        public void stop() {
            System.out.println("关闭,贩卖机。");
        }
     
    }

    乍眼一看,好像没啥毛病。但只要你仔细一看,就会发现。两种贩卖机的,start() 和 stop() 方法的代码,重复了。这就是使用接口,带来的问题。共同的协议行为有了,但是,相同的动作,却要重复的写。还好,现在只有两种。如果种类,变多,接口协议也变多,那重复的代码量,是相当可怕的。所以,为了解决这个问题,我们引出一种设计模式:模板类模式 (Template)。也即是,采用抽象类的方法,来解决这一问题,因为抽象类可以包含实现,而接口不行。这个例子,只是具有模板类的一些相似思维,不是特别相似,请忽略。

    三、使用抽象类,解决代码重复的问题。 
    首先,我们需要,创建一个抽象类。然后,把共同的动作,在抽象类里面实现。抽象类的代码如下:

    /**
     * @author Lyf
     * @describe 抽象的贩卖机类
     */public abstract class AbstractVending {
     
        public void start() {
            System.out.println("启动,贩卖机。");
        }    // 卖什么,由子类自己去决定
    
        public abstract void process();    
    
        public void stop() {
            System.out.println("关闭,贩卖机。");
        }
     
    }

    下一步,我们让两个子类去继承它,两个子类的代码如下:

    /**
     * @author Lyf
     * @describe 葡萄贩卖机,继承自贩卖机抽象类
     */public class GrapeVendingImpl extends AbstractVending {
     
        @Override
        public void start() {
            System.out.println("启动,贩卖机。");
        }    @Override
        public void process() {
            System.out.println("正在卖葡萄....");
        }    @Override
        public void stop() {
            System.out.println("关闭,贩卖机。");
        }
     
    }
    /**
     * @author Lyf
     * @describe 苹果贩卖机,继承自贩卖机抽象类
     */public class AppleVendingImpl extends AbstractVending {
     
        @Override
        public void start() {
            System.out.println("启动,贩卖机。");
        }    @Override
        public void process() {
            System.out.println("正在卖苹果....");
        }    @Override
        public void stop() {
            System.out.println("关闭,贩卖机。");
        }
     
    }

    很明显,通过抽象类的方式,可以解决第二条,使用接口,所带来的代码重复问题。但这种做法,虽然解决了代码重复的问题,却引来另一个问题。即两个 AbstractVending 类的子类,无法再被其它类继承。这就无法达到,我们一开始所说的,要集接口和抽象类优势的说法。这时候,就该轮到 Skeletal Implementation 模式,粉墨登场,啪啪啪(掌声响起来)。

    四、使用 Skeletal Implementation 模式,集二、三之所长。 
    实现 Skeletal Implementation 模式,主要分三步走:

    1. 创建一个接口,即共同协议 (有些时候,只为了解决类无法多继承的问题,这个协议也可以不用)。

    2. 创建一个抽象类,并实现 1 所创建的接口,然后,把通用行为写在里面,并抽出需要子类自己定制的抽象方法。(听起来,跟模板类差不多)

    3. 在本该继承这个抽象类的子类里面,创建一个私有内部类,在这个私有内部类里面,重写,填充抽象方法。然后,由这个子类,扩展 1 所说的接口,然后,1 所说的接口所带的方法实现,代理给这个私有内部类去做。

    第 3 步,听起来好复杂。还是看看代码,比较好理解。下面,将会重写上面所写的 GrapeVending 和 AppleVending 的实现。你可以通过对比,来理解两者的区别

    /**
     * @author Lyf
     * @describe 苹果贩卖机
     */public class AppleVending implements IVending {
     
        @Override
        public void start() {
            _abstractVending.start();
        }    @Override
        public void process() {
            _abstractVending.process();
        }    @Override
        public void stop() {
            _abstractVending.stop();
        }    

    private AbstractVending _abstractVending = new AbstractVending() { @Override public void process() { System.out.println("正在卖苹果...."); } }; }


    /**
     * @author Lyf
     * @describe 葡萄贩卖机
     */public class GrapeVending implements IVending {
     
        @Override
        public void start() {
            _abstractVending.start();
        }    @Override
        public void process() {
            _abstractVending.process();
        }    @Override
        public void stop() {
            _abstractVending.stop();
        }    

    private AbstractVending _abstractVending = new AbstractVending() { @Override public void process() { System.out.println("正在卖葡萄...."); } }; }

    在 Java 中应用骨架实现

    编译:唐尤华

    链接:dzone.com/articles/favour-skeletal-interface-in-java

    程序中有重复代码?骨架实现(Skeletal Implementation)通过接口与抽象类配合,让你摆脱重复,留下程序中有用的代码。

    骨架实现是一种设计,我们可以同时享受接口和抽象类的好处。

    Java Collection API 已经采用了这种设计:AbstractSet、 AbstractMap 等都是骨架实现案例。Joshua Bloch 的 "Effective Java" 书中也提到了骨架接口。

    本文我们将探讨如何高效设计系统,使其能够同时利用接口和抽象类的特性。

    让我们试着通过一个实际问题来理解。

    假设我们想创建不同类型的自动售货机。从自动售货机购买产品,需要启动售货机、选择产品、付款、然后取货。

    取货完成之后,自动售货机应该停止操作。

    1. 方法一

    我们可以为不同的产品类型创建一个自动售货机接口。为了让接口工作,我们还要为自动售货机提供具体实现。

    1.1 代码

    Ivending.java
    
    ```java
    package com.example.skeletal;
    public interface Ivending {
        void start();
        void chooseProduct();
        void stop();
        void process();
    }
    ```
    
    CandyVending.java
    
    ```java
    package com.example.skeletal;
    public class CandyVending implements Ivending {
        @Override
        public void start() {
            System.out.println("Start Vending machine");
        }
    
        @Override
        public void chooseProduct() {
            System.out.println("Produce different candies");
            System.out.println("Choose a type of candy");
            System.out.println("Pay for candy");
            System.out.println("Collect candy");
        }
    
            @Override
        public void stop() {
            System.out.println("Stop Vending machine");
        }
    
        @Override
        public void process() {
            start();
            chooseProduct();
            stop();
        }
    }
    ```
    
    DrinkVending.java
    
    ```java
    package com.example.skeletal;
    public class DrinkVending implements Ivending {
        @Override
        public void start() {
            System.out.println("Start Vending machine");
        }
    
        @Override
        public void chooseProduct() {
            System.out.println("Produce diiferent soft drinks");
            System.out.println("Choose a type of soft drinks");
            System.out.println("pay for drinks");
            System.out.println("collect drinks");
        }
    
        @Override
        public void stop() {
            System.out.println("stop Vending machine");
        }
    
        @Override
        public void process() {
            start();
            chooseProduct();
            stop();
        }
    }
    ```
    
    VendingManager.java
    
    ```java
    package com.example.skeletal;
    public class VendingManager {
        public static void main(String[] args) {
            Ivending candy = new CandyVending();
            Ivending drink = new DrinkVending();
            candy.process();
            drink.process();
        }
    }
    ```
    

    输出结果:

    ```shell
    Start Vending machine
    Produce different candies
    Choose a type of candy
    Pay for candy
    Collect candy
    Stop Vending machine
    *********************
    Start Vending machine
    Produce diiferent soft drinks
    Choose a type of soft drinks
    Pay for drinks
    Collect drinks
    Stop Vending machine
    ```
    

    简单起见,我没有将每个步骤定义一个单独方法,在 `chooseProduct()` 中合并了这些步骤。

    虽然看起来很好,但是上面的代码” 有一些问题 “。如果我们仔细检查一下,就会发现其中有很多重复代码。 `start()`、 `stop()` 和 `process()` 方法在每个实现类中做了相同的事情

    当新增具体实现时,系统的代码会复制三次。

    这时我们可以新建工具类,将公共代码放到工具类里。然而这么做会破坏” 单一责任原则 “,产生 Shotgun surgery 问题代码。

    译注:[Shotgun surgery][1] 是软件开发中的一种反模式,它发生在开发人员向应用程序代码库添加特性的地方,这些代码库会在一次更改中跨越多个实现。

    [1]:https://en.wikipedia.org/wiki/Shotgun_surgery

    1.2 接口的缺点

    由于接口是一种约定且不包含方法体,因此每个实现都必须按照约定实现接口中的所有方法。在具体的实现中一些方法可能会重复。

    2. 方法二

    通过抽象类弥补接口的不足。

    2.1 代码

    AbstractVending.java
    
    ```java
    package com.example.skeletal;
    public abstract class AbstractVending {
        public void start()
        {
            System.out.println("Start Vending machine");
        }
    
        public abstract void chooseProduct();
    
            public void stop()
        {
            System.out.println("Stop Vending machine");
        }
    
            public void process()
        {
            start();
            chooseProduct();
            stop();
        }
    }
    ```
    
    CandyVending.java
    
    ```java
    package com.example.skeletal;
    public class CandyVending extends AbstractVending { 
        @Override
        public void chooseProduct() {
            System.out.println("Produce diiferent candies");
            System.out.println("Choose a type of candy");
            System.out.println("Pay for candy");
            System.out.println("Collect candy");
        }
    }
    ```
    
    DrinkVending.java
    
    ```java
    package com.example.skeletal;
    public class DrinkVending extends AbstractVending { 
        @Override
        public void chooseProduct() {
            System.out.println("Produce diiferent soft drinks");
            System.out.println("Choose a type of soft drinks");
            System.out.println("Pay for drinks");
            System.out.println("Collect drinks");
        }
    }
    ```
    
    VendingManager.java
    
    ```java
    package com.example.skeletal;
    public class VendingManager {
        public static void main(String[] args) {
            AbstractVending candy =  new CandyVending();
            AbstractVending drink =  new DrinkVending();
            candy.process();
            System.out.println("*********************");
            drink.process();
        }
    }
    ```
    

    这里我为抽象类提供了通用的代码,`CandyVending` 和 `DrinkVending` 都继承了 `AbstractVending`。这么做虽然消除了重复代码,但引入了一个新问题。

    `CandyVending` 和 `DrinkVending` 继承了 `AbstractVending`,由于 Java 不支持多重集成因此不能继承其他类。

    假如要添加一个 `VendingServicing` 类,负责清洁和检查自动售货机。在这种情况下,由于已经继承了 `AbstractVending`,因此不能继承 `VendingServicing`。这里可以新建组合(composition),但是必须把 `VendingMachine` 传入该组合,这会让 `VendingServicing` 和 `VendingMachine` 产生强耦合。

    2.2 抽象类的缺点

    由于菱形继承问题,Java 不支持多重继承。假如我们能够同时利用接口和抽象类的优点就太好了。

    还是有办法的。

    译注:菱形继承问题。两个子类继承同一个父类,而又有子类又分别继承这两个子类,产生二义性问题。

    3. 抽象接口或骨架实现

    要完成骨架实现:

    1. 创建接口。

    2. 创建抽象类来实现该接口,并实现公共方法。

    3. 在子类中创建一个私有内部类,继承抽象类。现在把外部调用委托给抽象类,该类可以在使用通用方法同时继承和实现任何接口。

    3.1 代码

    Ivending.java
    
    ```java
    package com.example.skeletal;
    public interface Ivending {
        void start();
        void chooseProduct();
        void stop();
        void process();
    }
    ```
    
    VendingService.java
    
    ```java
    package com.example.skeletal;
    public class VendingService {
        public void service()
        {
            System.out.println("Clean the vending machine");
        }
    }
    ```
    
    AbstractVending.java
    
    ```java
    package com.example.skeletal;
    public abstract class AbstractVending implements Ivending {
        public void start()
        {
            System.out.println("Start Vending machine");
        }
        public void stop()
        {
            System.out.println("Stop Vending machine");
        }
        public void process()
        {
            start();
            chooseProduct();
            stop();
        }
    }
    ```
    
    CandyVending.java
    
    ```java
    package com.example.skeletal;
    public class CandyVending  implements Ivending { 
        private class AbstractVendingDelegator extends AbstractVending
        {
            @Override
            public void chooseProduct() {
                System.out.println("Produce diiferent candies");
                System.out.println("Choose a type of candy");
                System.out.println("Pay for candy");
                System.out.println("Collect candy");
            }
        }
    
        AbstractVendingDelegator delegator = new AbstractVendingDelegator();
    
        @Override
        public void start() {
            delegator.start();
        }
        @Override
        public void chooseProduct() {
            delegator.chooseProduct();
        }
        @Override
        public void stop() {
            delegator.stop();
        }
        @Override
        public void process() {
            delegator.process();
        }
    }
    ```
    
    DrinkVending.java
    
    ```java
    package com.example.skeletal;
    public class DrinkVending extends VendingService  implements Ivending { 
        private class AbstractVendingDelegator extends AbstractVending
        {
            @Override
            public void chooseProduct() {
                System.out.println("Produce diiferent soft drinks");
                System.out.println("Choose a type of soft drinks");
                System.out.println("pay for drinks");
                System.out.println("collect drinks");
            }
        }
        AbstractVendingDelegator delegator = new AbstractVendingDelegator();
        @Override
        public void start() {
            delegator.start();
        }
        @Override
        public void chooseProduct() {
            delegator.chooseProduct();
        }
        @Override
        public void stop() {
            delegator.stop();
        }
        @Override
        public void process() {
            delegator.process();
        }
    }
    ```
    
    VendingManager.java
    
    ```java
    package com.example.skeletal;
    public class VendingManager {
        public static void main(String[] args) {
            Ivending candy = new CandyVending();
            Ivending drink = new DrinkVending();
            candy.process();
            System.out.println("*********************");
            drink.process();
            if(drink instanceof VendingService)
            {
                VendingService vs = (VendingService)drink;
                vs.service();
            }
        }
    }
    ```
    
    ```shell
    Start Vending machine
    Produce diiferent candies
    Choose a type of candy
    Pay for candy
    Collect candy
    Stop Vending machine
    *********************
    Start Vending machine
    Produce diiferent soft drinks
    Choose a type of soft drinks
    Pay for drinks
    Collect drinks
    Stop Vending machine
    Clean the vending machine
    ```
    

    上面的设计中,首先创建了一个接口,然后创建了一个抽象类,在这个类中定义了所有通用的实现。然后,为每个子类实现一个 delegator 类。通过 delegator 将调用转给 `AbstractVending`。

    3.2 骨架实现的好处

    1. 子类可继承其他类,比如 `DrinkVending`。

    2. 通过将调用委托给抽象类消除重复代码。

    3. 子类可根据需要实现其他的接口。

    4. 总结

    当接口有公用方法时可以创建抽象类,使用子类作为委派器,建议使用骨架实现。

  • 相关阅读:
    第八章 路由器交换机及其操作系统的介绍
    k-Tree DP计数
    Drop Voicing 最长升序
    高精度
    1196D2
    C
    POJ 3974 马拉车
    2020.8.1第二十六天
    2020.7.31第二十五天
    每日日报
  • 原文地址:https://www.cnblogs.com/cx2016/p/13231092.html
Copyright © 2020-2023  润新知