问题 随着时间的推移,使用新功能规划Java API的适当方法是什么?


我正在与一个团队合作开发一个新的Java API,用于我们的一个内部项目。我们可能无法花时间停止并散布Java接口的所有细节,并在开始时使它们100%完美。

我们有一些核心功能必须预先设置,其他的可能会在以后添加,但现在不重要,+现在花时间设计这些功能是我们没有的奢侈品。特别是因为我们还没有足够的信息来获得所有的设计细节。

API的Java方法是,一旦你发布了一个接口,它实际上是不可变的,你永远不应该改变它。

有没有办法计划API随时间的演变? 我读了 这个问题 我想我们可以做到这一点:

// first release
interface IDoSomething
{
    public void hop();
    public void skip();
    public void jump();
}

// later
interface IDoSomething2 extends IDoSomething
{
    public void waxFloor(Floor floor);
    public void topDessert(Dessert dessert);
}

// later still
interface IDoSomething3 extends IDoSomething2
{
    public void slice(Sliceable object);
    public void dice(Diceable object);
}

然后升级我们的课程从支持 IDoSomething 至 IDoSomething2 接着 IDoSomething3,但这似乎有一个代码气味问题。

然后我猜是有的 番石榴标记界面的方式 @Beta 因此,应用程序可以在冻结之前使用这些风险,但我不知道这是否正确。


11747
2017-10-08 16:06


起源

如果你的项目是内部的,我不确定这个陈述是否适用“API的Java方法是,一旦你发布了一个接口,它就是不可改变的......”。作为一个内部项目,您可以将更改传达给您的团队。如果这种类型的沟通不可行。您可以使用工厂提供“正确的”API,具体取决于客户端版本(为了向后兼容),因此客户端需要知道他们正在使用版本化API并包含他们在请求中使用的版本(或提供不同的入口点)对于新版本) - ochi
注意,使用Java 8减轻了接口的主要问题之一:您现在可以添加 default 接口的方法,简化了很多事情,并且更容易遵守规则 “如有疑问,请将其删除” Josh Bloch在他关于“如何设计一个好的API及其重要性”的精彩演讲中提及, infoq.com/presentations/effective-api-design 。除此之外,另一个指针:我认为Wiki(和书)来自 wiki.apidesign.org/wiki/TheAPIBook 值得一看。 - Marco13
谢谢Marco ...是的,我在发布这个问题前约10分钟就订购了Tulach的书。 - Jason S


答案:


如果您想要灵活的代码泛型可以提供帮助。

例如,而不是:

interface FloorWaxer
{
    public void waxFloor(Floor floor);
}

你可以有:

interface Waxer<T> 
{
    void wax(T t);
}

class FloorWaxer implements Waxer<Floor> 
{
    void wax(Floor floor);
}

另外,Java 8带来了 default methods 在接口中,允许您在现有接口中添加方法;考虑到这一点,您可以使界面通用。这意味着您应该使您的接口尽可能通用;代替:

interface Washer<T>
{
    void wash(T what);   
}

然后再添加

interface Washer<T>
{
    void wash(T what);   
    void wash(T what, WashSubstance washSubstance); 
}

然后添加

interface Washer<T>
{
    void wash(T what);   
    void wash(T what, WashSubstance washSubstance); 
    void wash(T what, WashSubstance washSubstance, Detergent detergent); 
}

你可以从头开始添加

@FunctionalInterface
interface Washer<T>
{
    void wash(T what, WashSubstance washSubstance, Detergent detergent); 

    default wash(T what, WashSubstance washSubstance) 
    {
        wash(what, washSubstance, Detergent.DEFAULT_DETERGENT);
    }

    default wash(T what, Detergent detergent) 
    {
        wash(what, WashSubstance.DEFAULT_WASH_SUBSTANCE, detergent);
    }

    default wash(T what) 
    {
        wash(what, WashSubstance.DEFAULT_WASH_SUBSTANCE, Detergent.DEFAULT_DETERGENT);
    }
}

此外,尝试使您的接口功能(只有一个抽象方法),这样您就可以从lambdas中获益。


3
2017-10-08 16:27





你可以采取这种方法 挂毯5 已经采取了它称为“自适应API”(更多信息 这里)。

tapestry不使用锁定接口,而是使用注释和pojo。我不完全确定你的情况,但这可能适合也可能不适合。请注意,tapestry使用ASM(通过 塑料)在引擎盖下,以便没有运行时反射来实现这一点。

例如:

public class SomePojo {
   @Slice
   public void slice(Sliceable object) {
      ...
   }

   @Dice
   public void dice(Diceable object) {
      ...
   }
}

public class SomeOtherPojo {
   @Slice
   public void slice(Sliceable object) {
      ...
   }

   @Hop
   public void hop(Hoppable object) {
      ...
   }
}

2
2017-10-08 16:12



有趣....这对我来说是一个新概念所以我不确定我们是否可以这样使用注释......但也许它对我们有用。 - Jason S


你可以用一个 新版API的新软件包名称  - 这将允许新旧API并排,API用户可以一次将其组件转换为新的API。你可以提供一些 适配器 帮助他们在使用新旧API的对象通过类之间的边界传递的边界上进行繁重的工作。

另一种选择非常苛刻,但可以用于内部项目 - 只是 改变你的需求并使用户适应

如果您只是添加,提供 默认实现 (在抽象类中)新方法可以使过程更加平滑。当然,这并不总是适用。

通过更改主要版本号来发送更改信号,在两种情况下提供有关如何将代码库升级到新版API的详细文档。


2
2017-10-08 16:39





我建议看看这些 结构模式。我觉得 装饰图案 (也称为自适应模式)可以满足您的需求。请参阅链接的Wikipedia文章中的示例。


1
2017-10-08 16:34



我认为这就是上面提到的Tapestry 5所使用的。 - Rostislav Matl
@RostislavMatl也许,我不知道Tapestry。但是从文档中我可以看出它是一个“自适应API”,但也许它不是“自适应模式”。我不知道这些注释是如何工作的。 - alain.janinm


这是我处理这种情况的方式。

首先,我使用抽象类,以便您以后可以插入默认实现。随着JDK 1.1中内部和嵌套类的出现,接口几乎没有增加;几乎所有的用例都可以很容易地转换为使用纯抽象类(通常作为嵌套类)。

首发

abstract class DoSomething {
    public abstract void hop();
    public abstract void skip();
    public abstract void jump();
}

第二次发布

abstract class DoSomething {
    public abstract void hop();
    public abstract void skip();
    public abstract void jump();

    abstract static class VersionTwo {
        public abstract void waxFloor(Floor floor);
        public abstract void topDessert(Dessert dessert);
    }

    public VersionTwo getVersionTwo() {
        // make it easy for callers to determine whether new methods are supported
        // they can do if (doSomething.getVersionTwo() == null)
        return null;
        // OR throw new UnsupportedOperationException(), depending on specifics
        // OR return a default implementation, depending on specifics
    }

    // if you like the interface you proposed in the question, you can do this:

    public final void waxFloor(Floor floor) {
        getVersionTwo().waxFloor();
    }

    public final void topDessert(Dessert dessert) {
        getVersionTwo().topDessert();
    }
}

第三次发布将类似于第二次发布,所以为了简洁,我将省略它。


1
2017-10-08 16:39



多年来,我一直在钻脑,抽象课程很糟糕,界面也很好......我需要一段时间来处理你的观点+与我过去的学习相协调。 - Jason S
有很多这种Java疯狂的例子,比如“通配符导入很糟糕,因为我需要通过查看我班级的顶层确切知道哪些类依赖”以及各种其他不能很好地审查的栗子。但是,换句话说,“IT世界的弧线很长,但却向理智倾斜。” :) - David P. Caldwell


如果您还没有设计最终的API,请不要使用您想要的名称!

称之为V1RC1,V1RC2,...,当它完成后,你有V1。

人们会在他们的代码中看到他们仍在使用RC版本并且可以删除它以在准备好时获得真实的东西。

Rostistlav 基本上是说同样的,但他称之为所有真正的API版本,所以它将是V1,V2,V3,....认为这取决于你的口味。


0
2017-10-08 21:04





您还可以尝试事件驱动的方法,并在API更改时添加新的事件类型,而不会影响向后兼容性。

例如:

public enum EventType<T> {
    SLICE<Sliceable>(Sliceable.class),
    DICE<Diceable>(Diceable.class),
    HOP<Hoppable>(Hoppable.class);

    private final Class<T> contextType;

    private EventType<T>(Class<T> contextType) {
       this.contextType = contextType;
    }

    public Class<T> getContextType() {
       return this.contextType;
    }
}

public interface EventHandler<T> {
    void handleEvent(T context);
}

public interface EventHub {
    <T> void subscribe(EventType<T> eventType, EventHandler<T> handler);
    <T> void publish(EventType<T> eventType, T context);
}

public static void main(String[] args) {
    EventHub eventHub = new EventHubImpl(); // TODO: Implement
    eventHub.subscribe(EventType.SLICE, new EventHandler<Sliceable.class> { ... });
    eventHub.subscribe(EventType.DICE, new EventHandler<Diceable.class> { ... });
    eventHub.subscribe(EventType.HOP, new EventHandler<Hoppable.class> { ... });

    Hoppable hoppable = new HoppableImpl("foo", "bar", "baz");
    eventHub.publish(EventType.HOP, hoppable); // fires EventHandler<Hoppable.class>
}

0
2017-10-09 07:49



嗯。更改枚举类型以添加​​新成员似乎值得怀疑。 - Jason S
它可以很容易地适应使用字符串作为主题。枚举非常适合编译时检查。 - lance-java