Em Java, quais são algumas boas maneiras de separar APIs da implementação de * projetos inteiros *?

8

Imagine que você possui um módulo de software, que é um plug-in para algum programa (semelhante ao Eclipse) e deseja que ele tenha uma API que outros plug-ins podem chamar. Seu plugin não está disponível gratuitamente, assim que você quer ter um módulo API separado, que é livremente disponível e é a única coisa que outros plugins precisam diretamente link para - clientes da API pode compilar com apenas o módulo API, e não o módulo de aplicação, no caminho da construção. Se a API for forçada a evoluir de maneiras compatíveis, os plug-ins de clientes poderão até incluir o módulo da API em seus próprios jars (para evitar qualquer possibilidade de Errors resultantes do acesso a classes inexistentes).

O licenciamento não é o único motivo para colocar a API e a implementação em módulos separados. Pode ser que o módulo de implementação seja complexo, com inúmeras dependências próprias. Os plug-ins do Eclipse geralmente têm pacotes internos e não internos, em que os pacotes não internos são semelhantes a um módulo de API (ambos estão incluídos no mesmo módulo, mas podem ser separados).

Eu já vi algumas alternativas diferentes para isso:

  1. A API está em um pacote separado (ou grupo de pacotes) da implementação. As classes da API chamam diretamente nas classes de implementação. A API não pode ser compilada a partir da fonte (o que é desejável em alguns casos incomuns) sem a implementação. Não é fácil prever os efeitos exatos de chamar métodos de API quando a implementação não está instalada - portanto, os clientes geralmente evitam fazer isso.

    package com.pluginx.api;
    import com.pluginx.internal.FooFactory;
    public class PluginXAPI {
        public static Foo getFoo() {
            return FooFactory.getFoo();
        }
    }
    
  2. A API está em um pacote separado e usa reflexão para acessar as classes de implementação. A API pode ser compilada sem a implementação. O uso da reflexão pode causar um impacto no desempenho (mas os objetos de reflexão podem ser armazenados em cache se houver um problema. É fácil controlar o que acontece se a implementação não estiver disponível.

    package com.pluginx.api;
    public class PluginXAPI {
        public static Foo getFoo() {
            try {
                return (Foo)Class.forName("com.pluginx.internal.FooFactory").getMethod("getFoo").invoke(null);
            } catch(ReflectiveOperationException e) {
                return null;
                // or throw a RuntimeException, or add logging, or raise a fatal error in some global error handling system, etc
            }
        }
    }
    
  3. A API consiste apenas em interfaces e classes abstratas, além de uma maneira de obter uma instância de uma classe.

    package com.pluginx.api;
    public abstract class PluginXAPI {
        public abstract Foo getFoo();
    
        private static PluginXAPI instance;
        public static PluginXAPI getInstance() {return instance;}
        public static void setInstance(PluginXAPI newInstance) {
            if(instance != null)
                throw new IllegalStateException("instance already set");
            else
                instance = newInstance;
        }
    }
    
  4. O mesmo que acima, mas o código do cliente precisa obter a referência inicial de outro lugar:

    // API
    package com.pluginx.api;
    public interface PluginXAPI {
        Foo getFoo();
    }
    
    // Implementation
    package com.pluginx.internal;
    public class PluginX extends Plugin implements PluginXAPI {
        @Override
        public Foo getFoo() { ... }
    }
    
    // Client code uses it like this
    PluginXAPI xapi = (PluginXAPI)PluginManager.getPlugin("com.pluginx");
    Foo foo = xapi.getFoo();
    
  5. Não. Faça com que os clientes se vinculem diretamente ao plug-in (mas ainda os impeçam de chamar métodos que não são da API). Isso tornaria difícil para muitos outros plugins (e a maioria dos plugins de código aberto) usar a API desse plug-in sem gravar seu próprio wrapper.

user253751
fonte

Respostas:

3

A resposta para sua pergunta depende da definição de "bom" na pergunta "quais são algumas boas maneiras de separar APIs da implementação de ..."

Se "bom" significa "pragmático fácil de implementar para você como fabricante da API", isso pode ser útil:

como você está usando java, onde as bibliotecas jar são carregadas em tempo de execução, sugiro uma alternativa ligeiramente diferente:

  • A API consiste apenas em interfaces, além de uma implementação fictícia dessas interfaces que não possui função real.

o cliente que usa sua API pode compilar contra esse dummy-jar e, em tempo de execução, você pode substituir o dummy-jar pelo licenciado.

algo semelhante é feito com slf4j, em que a implementação de log real a ser usada com seu aplicativo será escolhida substituindo o jar de log

k3b
fonte
Para expandir o exemplo do slf4j : slf4j contém implementações fictícias nas fontes que são removidas após a compilação. O efeito disso é uma referência forte (o que resulta em uma ClassDefNotFound se não há nenhuma implementação fornecida na sua candidatura)
dot_Sp0T
@ dot_Sp0T Concordo que o problema "ClassDefNotFound" existe para o Android, porque no Android todos os frascos são mesclados em um arquivo apk. No java comum (sem usar um ofuscador), isso não deve ser um problema. Se eu estiver errado, me avise sob quais circunstâncias acontecem.
K3b
4

Você já viu os Mecanismos do ServiceLoader em Java? Basicamente, você pode especificar a implementação de uma interface através do arquivo de manifesto em um jar. O Oracle também fornece algumas informações adicionais sobre plug-ins em programas Java.

Benni
fonte
1

Pelo que entendi, as pessoas costumam usar o padrão de fábrica para isso.

Eles colocam as interfaces da API em um módulo separado (por exemplo, um arquivo jar) e, quando os clientes desejam usar a API e têm acesso a uma implementação da API, a implementação da API terá um ponto de entrada de fábrica para os clientes. comece a criar objetos concretos implementando essa API.

Isso significa que sua primeira ideia acima é a semelhança mais próxima.

InformedA
fonte