【问题标题】:Using Java Generics for Code Reuse in Interface Inheritance在接口继承中使用 Java 泛型进行代码重用
【发布时间】:2016-12-29 22:17:43
【问题描述】:

我正在尝试使用 Java 中的面向对象技术为状态机创建一个框架。每个System 一次只能有一个State 处于活动状态。

public interface State {}
public interface System { 
    State currentState();
}

假设我想添加一种应定期更新以检查和更改其状态的新型系统,称为UpdateableSystem。它只能具有 UpdateableState 类型的状态。

public interface UpdateableState extends State {
    /**
     * Check to see if the system should switch to a different state.
     * @returns new state of the system
     */
    UpdateableState update();
}

public interface UpdateableSystem extends System {
    @Override
    UpdateableState currentState();
    /** 
     * Runs update method of the current state, or otherwise updates the system.
     * @returns the current state of the system after the update
     */
    UpdateableState update();
}

我重写了currentState 方法,只返回UpdateableState,而不是State,因为UpdateableSystem 只能具有UpdateableState 类型的状态,而使用UpdateableSystem 的客户端将期待UpdateableStates。

为了避免在创建更多子类时多次覆盖许多方法,泛型似乎是解决方案。 这是更新的界面。

public interface System<SystemState extends State> {
    SystemState currentState();
}

public interface UpdateableState<SystemState extends UpdateableState<SystemState>> extends State {
    SystemState update();
}

public interface UpdateableSystem<SystemState extends UpdateableState<SystemState>> extends System<SystemState> {
    SystemState update();
}

现在,假设我想添加另一种类型,接口Context,系统和状态需要注意。 Context 应该能够被子类型化,并且客户端和ContextState 子类型应该能够使用该Context 子类型的完整接口。

public interface Context {}
public interface ContextState<SystemContext extends Context, SystemState extends ContextState<SystemContext, SystemState>> extends UpdateableState<SystemState> {
    SystemState updateWithContext(SystemContext context);
}
public interface ContextSystem<SystemContext extends Context, SystemState extends ContextState<SystemContext, SystemState>> extends UpdateableSystem<SystemState> {
    // Some methods here that require SystemContext type
}

突然之间,这些参数化类型变成了一大堆样板文件,令人困惑且难以维护。

这将是一个具有具体实现的示例:

class CarContext implements Context {...}
interface CarState extends ContextState<CarContext, CarState> {...}
    class CarDrivingState implements CarState {}
    class CarIdleState implements CarState {}
class Car implements ContextSystem<CarContext, CarState> {...}

每个具体的CarState 将能够访问CarContext 接口提供的其他方法,因为从ContextState 继承的方法签名将由CarContext 类型参数化。因此,updateWithContext 的上下文参数不需要显式转换。

最终,问题在于添加额外类型以进行参数化时的样板过多,而且我不知道有一种设计替代方案可以通过继承实现代码重用,同时保持强大的类型系统,尽可能避免显式转换。有什么改进这个设计的建议吗?

【问题讨论】:

  • Java 泛型不是模板类型。这个interface CarState extends ContextState&lt;CarContext, CarState&gt;(例如)也可以写成interface CarState extends ContextState&lt;A, B&gt;——该类型的本地名称只是一个标签
  • 我不确定您的意思,它们在ContextState 接口的定义中充当标签,但在CarState 接口的定义中,它们必须是实际类型。据我所知,实际的接口或类。
  • 我会再试一次; Java 泛型是一个编译时类型检查系统。编译后它们是erased(因此没有运行时开销)成java.lang.Object;您似乎期望的是它们的行为类似于 C++ 模板类型。他们没有。
  • @Elliott 抱歉,如果我误解了,但我可以将其扩展为原始类型:interface CarState extends ContextState,并且它可以功能相同,因为擦除。但是,我会收到编译器警告,并且在编译时它会缺乏相同的类型检查保证。我不确定在这种情况下擦除对我有什么帮助。
  • 擦除对您没有帮助。擦除是 Java 中如何实现泛型的直接(也是不可避免的)结果。

标签: java generics inheritance design-patterns


【解决方案1】:

在花了一天的时间思考这个问题并稍微尝试一下你的示例代码之后,我认为你已经有了解决这个问题的最佳解决方案,尽管你可能会从正确遵守 Java Generics code style standards 中受益。

通过在接口声明中使用泛型类型的全名,实际上会使它们更难阅读,尤其是在涉及方法时。泛型类型很容易与实际的接口或类名混淆。更不用说它们占用了更多空间。让我们看看您的代码遵循标准会是什么样子:

public interface State{}
public interface System<S extends State> {
    S currentState();
}

public interface UpdateableState<S extends UpdateableState<S>> extends State {
    S update();
}
public interface UpdateableSystem<S extends UpdateableState<S>> extends System<S> {
    S update();
}

public interface Context{}
public interface ContextState<C extends Context, S extends ContextState<C, S>> extends UpdateableState<S> {
    S updateWithContext(C context);
}
public interface ContextSystem<C extends Context, S extends ContextState<C, S>> extends UpdateableSystem<S> {
    // Some methods here that require SystemContext type
}

class CarContext implements Context {...}
interface CarState extends ContextState<CarContext, CarState> {...}
class CarDrivingState implements CarState {}
class CarIdleState implements CarState {}
class Car implements ContextSystem<CarContext, CarState> {...}

声明仍然很冗长,但没有那么冗长。此外,我们现在对完全引用类型参数和泛型类型参数有了明确的区分。

最重要的是这个解决方案有效。


但是,如果您想进一步简化此操作,您可以冒这些接口可能被滥用的风险。例如:

public interface State{}
public interface System<S> {
    S currentState();
}

public interface UpdateableState<S> extends State {
    S update();
}
public interface UpdateableSystem<S> extends System<S> {
    S update();
}

public interface Context{}
public interface ContextState<C, S> extends UpdateableState<S> {
    S updateWithContext(C context);
}
public interface ContextSystem<C, S> extends UpdateableSystem<S> {
    S updateWithContext(C context);
}

public class ExampleContext implements Context{}
public class ExampleContextState implements ContextState<ExampleContext, ExampleContextState>{
    @Override
    public ExampleContextState updateWithContext(ExampleContext context)
    {
        return this;
    }

    @Override
    public ExampleContextState update()
    {
        return this;
    }
}
public class ExampleContextSystem implements ContextSystem<ExampleContext, ExampleContextState>{
    private ExampleContextState currentState;
    public ExampleContextSystem(){
        currentState = new ExampleContextState();
    }
    @Override
    public ExampleContextState updateWithContext(ExampleContext context)
    {
        this.currentState = currentState.updateWithContext(context);
        return currentState;
    }

    @Override
    public ExampleContextState update()
    {
        this.currentState = currentState.update();
        return currentState;
    }

    @Override
    public ExampleContextState currentState()
    {
        return currentState;
    }

}

在这里,我认为接口声明大大减少了。这里的一切都和以前一样顺利。问题是有人可以去做一些愚蠢的事情,比如......

public class BadExampleContextState implements ContextState<Integer, String>{
    @Override
    public String updateWithContext(Integer context)
    {
        return "Garbage";
    }

    @Override
    public String update()
    {
        return "Garbage";
    }
}

幸运的是,这种滥用并不能真正进入我们的ExampleContextSystem,所以我们仍然不能最终尝试将String 分配给ExampleContextState 引用,但它仍然表明我们已经输了我们对如何使用这些接口的一些控制。尽管如此,即使我们制作了一个通用的具体系统类......

public class GenericSystem<S extends UpdateableState<S>> implements UpdateableSystem<S>{
    private S currentState;
    public GenericSystem(S startingState){
        this.currentState = startingState;
    }
    @Override
    public S update()
    {
        this.currentState = currentState.update();
        return currentState;
    }

    @Override
    public S currentState()
    {
        return currentState;
    }

}

类型系统仍然足够强大,可以防止我们炸毁...

GenericSystem<BadExampleContextState> s = new GenericSystem<>(new BadExampleContextState()); 
// error: type argument Generics.BadExampleContextState is not within bounds of type-variable S

TL;DR:我认为你可以放宽你的通用约束,给出一个不太冗长的接口声明,而不会承担太多(如果有的话)运行时失败的风险。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多