Skeletal implementation(SI)可以综合接口和抽象类的优点.java的集合API中已经使用了SI,如:AbstractSet, AbstractMap等.Joshua Bloch也在他的<Effective Java.>中提到了该种设计方法.
本文将介绍如何使用SI.先从一个问题开头,假如我们想实现一个贩卖机系统,贩卖机可以有多种.贩卖机处理流程包括启动贩卖机,选择商品,付钱,然后取货,最后停止贩卖机.
第一种方法
我们可以针对不同的商品创建一个抽象的贩卖机接口.然后具体实现每个商品类型,代码如下:
package com.example.skeletal;
public interface Ivending {
void start();
void chooseProduct();
void stop();
void process();
}
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 diiferent 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();
}
}
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();
}
}
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();
}
}
/**
Output :
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
**/
为了简略,并未为每一个步骤写单独的方法,所以chooseProduct()里合并了一些步骤.尽管上述代码功能完善,但是还是存在一些问题,如果我们仔细观察,会发现代码里存在许多重复代码.start(),stop(),和process()在每个子类中都做同样的事情.以后每当添加一个子类时,都会有三倍的代码冗余.当然我们可以创建一个utility类,然后把所有操作代码都放在这个类里,但是这破坏了单一职责的原则,并且以后修改代码可能牵一发而动全身(shotgun surgery code smell)
接口缺点
因为接口是完全抽象的,所以其中不能有任何的具体方法实现(java8后可以添加默认方法,但此不在讨论范围之内),实现一个接口就必须重写接口里的所有方法,所以一些通用方法在实现类中反复重写造成同一个接口的实现类之间存在冗余代码.
第二种方法
我们可以通过抽象类来解决该问题.
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();
}
}
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");
}
}
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");
}
}
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已经继承了一个类,因此无法再继承其他类,因为Java不支持多继承.比如,我们想添加一个新类VendingServicing用于清理和检测贩卖机.这种场景下,子类无法再继承VendingServicing因为它们已经继承了AbstractVending.一种替代方法是使用混合模式,但同样存在问题:我们必须将VendingMachine传给类VendingServicing,这样造成VendingService和VendingMachine之间存在很强的相互依赖性,加重了耦合.
抽象类的缺点
因为钻石问题(https://en.wikipedia.org/wiki/Multiple_inheritance#The_diamond_problem),Java没有像C++那样支持多继承.
如果我们能同时使用到接口和抽象类的有点该有多好,幸好,我们有办法.
抽象接口或者叫Skeletal Implementation
是想skeletal implementation步骤:
Step 1: 创建一个接口.
Step 2: 创建一个抽象类实现这个接口,并且在抽象类中实现一些接口中的通用方法.
Step 3: 子类在实现接口的同时,在内部定义一个私有内部类继承抽象类.这样在就可以通过内部类调用抽象类中的已写好的方法来去除冗余代码,同时子类还可以继承其他类如VendServicing.
package com.example.skeletal;
public interface Ivending {
void start();
void chooseProduct();
void stop();
void process();
}
package com.example.skeletal;
public class VendingService {
public void service()
{
System.out.println("Clean the vending machine");
}
}
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();
}
}
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();
}
}
package com.example.skeletal;
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();
}
}
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();
}
}
}
/**
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
**/
观察以上的代码,我们先定义了一个接口,然后定义了一个抽象类来实现这个接口,并在抽象类中实现接口中的通用方法.在每个子类中定义了一个代理内部类来调用抽象类AbstractVending中的方法.
Skeletal Implementation优点
- 子类可以挣脱单继承规则,继续继承其他类,如上面的DrinkVending.
- 去除了冗余代码.
- 如果子类更换一种接口实现,修改起来也很容易.
总结
如果你的接口中存在一些通用的方法,那么首先思考一下能不能创建一个抽象类继承该接口,实现通用方法.然后用子类继承该接口,同时在子类内部定义一个代理类继承抽象类.该方法叫skeletal implementation.(就像使用一个骨架支起一副血肉一样,形象)