Каким образом можно планировать Java API с новыми функциями с течением времени?

Я работаю с командой над новым API Java для одного из наших внутренних проектов. Вероятно, мы не сможем потратить время на то, чтобы остановить и отобразить все детали интерфейсов Java и получить их на 100% в начале.

У нас есть некоторые основные функции, которые должны быть впереди, и другие, которые, вероятно, будут добавлены позже с течением времени, но теперь не важны, + время, затраченное на разработку этих функций, теперь роскошь, которую мы не делаем иметь. Тем более, что у нас недостаточно информации, чтобы получить все детали дизайна.

Java-подход к API-интерфейсам заключается в том, что как только вы публикуете интерфейс, он эффективно неизменен, и вы не должны его изменять.

Есть ли способ планировать эволюцию 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, но, похоже, проблема с запахом кода.

Тогда я предполагаю, что способ Guava для маркировки интерфейсов с @Beta, чтобы приложения могли использовать их под угрозой, прежде чем они будут заморожены, но я не знаю, правильно ли это.

Ответ 1

Если вам нужна гибкая генерация кода, вы можете помочь.

Например, вместо:

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);
    }
}

Кроме того, попробуйте сделать ваши интерфейсы функциональными (только один абстрактный метод), чтобы вы могли воспользоваться сахаром лямбда.

Ответ 2

Вы можете принять подход, который tapestry-5 взял, что он называет "Adaptive API" (подробнее здесь).

Вместо заблокированных интерфейсов, гобелен использует аннотации и pojo. Я не совсем уверен в ваших обстоятельствах, но это может быть или не быть хорошим. Обратите внимание, что в гобелене используется ASM (через plastic) под капотом, чтобы не было отражения во время выполнения, чтобы достичь этого.

Например:

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) {
      ...
   }
}

Ответ 3

Вы можете использовать новое имя пакета для новой версии API. Это позволит старым и новым API жить бок о бок, а пользователи API могут преобразовывать свои компоненты в новый API один на время. Вы можете предоставить несколько адаптеров , чтобы помочь им с тяжелым подъемом на границах, где объекты проходят через границы между классами с использованием нового и старого API.

Другой вариант довольно суровый, но может работать для внутреннего проекта - просто измените то, что вам нужно, и сделайте для пользователей адаптацию.

Если вы просто добавляете, предоставляя стандартную реализацию (в абстрактном классе) новых методов, можно сделать процесс более плавным. Конечно, это не всегда применимо.

Сигнализация изменения путем изменения номера главной версии a дает подробную документацию о том, как обновить базу кода до новой версии API, является хорошей идеей в обоих случаях.

Ответ 4

Я бы предложил посмотреть на Структурный шаблон. Я думаю, что Decorator pattern (также известный как Adaptive pattern) может удовлетворить ваши потребности. См. Пример в связанной статье Википедии.

Ответ 5

Вот как я подхожу к этой ситуации.

Во-первых, я бы использовал абстрактные классы, чтобы позже вы могли подключаться к реализациям по умолчанию. С появлением внутренних и вложенных классов в 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();
    }
}

Третий релиз будет похож на второй, поэтому я опускаю его для краткости.

Ответ 6

Если вы не разработали окончательный API, не используйте имя, которое вы хотите для него!

Назовите это что-то вроде V1RC1, V1RC2,.. и когда это будет сделано, у вас есть V1.

Люди будут видеть в своем коде, что они все еще используют RC-версию и могут удалить это, чтобы получить реальную информацию, когда она будет готова.

Ростислав в основном говорит то же самое, но он называет их всеми реальными версиями API, поэтому это будут V1, V2, V3,.... Думайте, что на ваш вкус.

Ответ 7

Вы также можете попробовать подход, основанный на событиях, и добавить новые типы событий в зависимости от изменений 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>
}