没有Provider的服务提供者接口

时间:2012-07-07 16:00:18

标签: java design-patterns service-provider

我正在阅读Bloch的有效java书籍 [1] ,并且遇到了以下SPI示例:

//Service interface
public interface Service {
  //Service specific methods here
}

//Service provider interface
public interface Provider {
  Service newService();
}

//Class for service registration and access
public class Services {
  private Services(){}

  private static final Map<String, Provider> providers =
    new ConcurrentHashMap<String, Provider>();
  public static final String DEFAULT_PROVIDER_NAME = "<def>";

  //Registration
  public static void registerDefaultProvider(Provider p) {
    registerProvider(DEFAULT_PROVIDER_NAME, p);
  }
  public static void registerProvider(String name, Provider p) {
    providers.put(name, p);
  }

  //Access
  public static Service newInstance() {
    return newInstance(DEFAULT_PROVIDER_NAME);
  }
  public static Service newInstance(String name) {
     // you get the point..lookup in the map the provider by name
     // and return provider.newService();
  }

这是我的问题:为什么必须使用Provider接口?难道我们不能轻易地自己注册服务 - 例如维护服务实现的映射,然后在查找时返回实例?为什么要额外的抽象层?

也许这个例子过于通用 - 任何“更好”的例子来说明这一点也会很棒。


[1] Second edition,第2章。第一版示例未引用服务提供者接口。

6 个答案:

答案 0 :(得分:5)

为什么需要Provider接口?我们不能轻易地自己注册服务 - 例如维护服务实现的映射,然后在查找时返回实例?

正如其他人所说,提供者的目的是拥有一个可以生成Service个实例的AbstractFactory。您并不总是希望保留对所有服务实现的引用,因为它们可能是短暂的和/或在执行后可能无法重用。

但是提供商的目的是什么,以及如何使用&#34;提供商注册API&#34;如果您没有提供者

拥有Provider接口的最有力的原因之一就是你不需要在编译时拥有一个实现。您的API用户可以稍后添加自己的实现。

让我们在另一个答案中使用JDBC作为Ajay的例子,但让我们更进一步:

有许多不同类型的数据库和数据库供应商都有略微不同的管理和实现数据库的方式(以及可能如何查询它们)。由于许多原因,Java的创建者可能无法创建所有这些不同可能方式的实现:

  • 当Java首次编写时,许多数据库公司或系统尚未存在。
  • 并非所有这些数据库供应商都是开源的,因此Java的创建者即使他们想要也不知道如何与他们进行通信。
  • 用户可能想要编写自己的自定义数据库

那你怎么解决这个问题?使用Service Provider

  • Driver界面是Provider。它提供了与特定供应商的数据库交互的方法。 Driver中的一个方法是一个工厂方法,用于在给定url和其他属性(如用户名和密码等)的情况下为数据库创建Connection实例(Service) 。

每个数据库供应商都会编写自己的Driver实现,以了解如何与自己的数据库系统进行通信。这些都不包含在JDK中;你必须去公司网站或其他一些代码库,并将它们作为一个单独的jar下载。

要使用这些驱动程序,必须将jar添加到类路径中,然后使用JDK DriverManager类注册驱动程序。

DriverManager类有一个方法registerDriver(Driver),用于在服务注册中注册Driver实例,以便可以使用它。按照惯例,大多数Driver实现在类加载时注册,因此您在代码中所要做的就是写

Class.forname("foo.bar.Driver"); 

为供应商注册驱动程序&#34; foo.bar&#34; (假设你的类路径中有该类的jar。)

注册数据库驱动程序后,您可以获得连接到数据库的Service实现实例。

例如,如果您的本地计算机上有一个名为&#34的mysql数据库;测试&#34;你有一个用户名帐户&#34; monty&#34;和密码&#34; greatsqldb&#34;然后你可以像这样创建一个Service实现:

Connection conn =
   DriverManager.getConnection("jdbc:mysql://localhost/test?" +
                               "user=monty&password=greatsqldb");

DriverManager类会看到您传入的String,并找到可以理解其含义的已注册驱动程序。 (这实际上是使用Chain of Responsibility模式完成的,方法是遍历所有已注册的驱动程序并调用其Driver.acceptsUrl(Stirng)方法,直到网址被接受为止。

请注意,JDK中没有特定于mysql的代码。您所要做的就是注册某个供应商的驱动程序,然后将格式正确的String传递给服务提供商。如果我们后来决定使用不同的数据库供应商(如oracle或sybase),那么我们只需交换jar并修改我们的连接字符串。 DriverManager中的代码不会更改。

为什么我们不进行一次连接并保留它?我们为什么需要这项服务?

我们可能希望在每次操作后连接/断开连接。或者我们可能希望保持更长时间的连接。拥有该服务允许我们随时创建新的连接,并且不会妨碍我们在以后重新使用它。

这是一个非常强大的概念,框架使用它来允许许多可能的排列和扩展,而不会使核心代码库混乱。

修改

与提供多个Services的多个提供商和提供商合作:

没有什么能阻止您拥有多个提供商。您可以同时连接到使用不同数据库供应商软件创建的多个数据库。您还可以同时连接到同一供应商生成的多个数据库。

多个服务 - 某些提供商甚至可能提供不同的Service实现,具体取决于连接URL。例如,H2可以创建基于文件系统或基于内存的数据库。告诉H2你要使用哪一个是一种不同的url格式。我没有查看H2代码,但我认为基于文件和基于内存的是不同的服务实现。

为什么DriverManager不管理Connections而Oracle可以实现OracleConnectionWrapper?没有供应商!

这也需要您知道您有Oracle连接。这是非常紧密的耦合,如果我改变了供应商,我将不得不改变很多代码。

Service Registration只接受一个字符串。请记住,它使用chain of Responsiblity来查找知道如何处理URL的第一个注册的Provider。应用程序可以是供应商中立的,它可以从属性文件中获取连接URL和驱动程序类名称。这样,如果我更换供应商,我就不必重新编译代码。但是,如果我硬编码引用&#34; OracleConnectionWrapper&#34;然后我更换了供应商,我将不得不重写部分代码,然后重新编译。

如果需要,没有什么可以阻止某人支持多种数据库供应商网址格式。所以如果我愿意,我可以制作一个可以处理mysql和oracle的GenericDriver。

答案 1 :(得分:1)

如果您可能需要多种类型的服务,则不能只重复使用旧服务。 (此外,测试等可能希望为每个测试创建新的服务,而不是重用可能已被先前测试修改或更新的服务。)

答案 2 :(得分:1)

我认为Effective Java中提到了答案以及一个例子。

  

服务提供者框架的可选第四个组件是a   服务提供者接口,提供者实现创建   他们的服务实施的实例。在没有服务的情况下   提供者接口,实现是通过类名和注册的   反思性地实例化(第53项)。

JDBC的情况下,
 Connection扮演服务界面的一部分,
DriverManager.registerDriver提供商注册API DriverManager.getConnection服务访问API ,以及
 Driver服务提供商界面

因此,正确地指出,提供者界面不是必须的,而只是一种更清洁的方法。

答案 3 :(得分:0)

因此,对于相同的Provider,您似乎可以拥有多个Service,并且根据特定的提供商名称,您可能会获得同一服务的不同实例。所以我想说每个提供商都像工厂那样适当地创建服务。

例如假设class PaymentService implements Service,它需要Gateway。您有PayPal和Chase网关处理这些支付处理器。现在,您创建一个PayPalProvider和ChaseProvider,每个都知道如何使用正确的网关创建正确的PaymentService实例。

但我同意,似乎做作了。

答案 4 :(得分:0)

作为其他答案的综合(第四个组成部分是文本原因)我认为这是为了限制编译依赖性。使用SPI,您可以使用所有工具来排除对实现的明确引用:

  • META-INF / services / 目录包含提及可用服务提供商实施的文件
  • ServiceLoader标准类允许解析可用的实现名称,并采用动态构造 [1]

第一版中未提及SPI。将它包含在关于静态工厂的项目中可能不是正确的地方。文中提到的DriverManager是一个暗示,但Bloch并没有深入。在某种程度上,平台实现了一种ServiceLocator pattern以减少编译依赖性,具体取决于环境。在抽象工厂中使用SPI,它在ServiceLoader的帮助下成为ServiceLocator的ServiceFactory,用于模块化。

ServiceLoader iterator可用于动态填充示例的服务地图。


[1] 在OSGi环境中,这是一个subtle operation

答案 5 :(得分:-1)

没有提供商的服务提供商界面

让我们看看没有提供者会是什么样子。

//Service interface
public interface Service {
  //Service specific methods here
}

//Class for service registration and access
public class Services {
  private Services(){}

  private static final Map<String, Service> services =
    new ConcurrentHashMap<String, Service>();
  public static final String DEFAULT_SERVICE_NAME = "<def>";

  //Registration
  public static void registerDefaultService(Provider p) {
    registerService(DEFAULT_SERVICE_NAME, p);
  }
  public static void registerService(String name, Provider p) {
    services.put(name, p);
  }

  //Access
  public static Service getInstance() {
    return newInstance(DEFAULT_SERVICE_NAME);
  }
  public static Service getInstance(String name) {
     // you get the point..lookup in the map the service by name
     // and return it;
  }

如您所见,可以在没有 Provider接口的情况下创建服务提供者接口。 #getInstance(..)的来电者最终不会注意到差异。

那为什么我们需要提供商?

Provider界面为Abstract FactoryServices#newInstance(String)Factory Method。这两种设计模式都具有将服务实现与服务注册分离的优势。

Single responsibility principle

不是在注册所有服务的启动事件处理程序中实现服务实例化,而是为每个服务创建一个提供程序。这使得它松散耦合并且更容易重构,因为服务和服务提供者可以彼此靠近,例如放到另一个JAR文件中。

“工厂方法在工具包和框架中很常见,其中库代码需要创建可以由使用框架的应用程序子类化的类型的对象。” [1]

终身管理

您可能已经在没有提供程序的高级代码中意识到我们正在注册服务实例而不是提供程序,这可能决定实例化新的服务实例。

这种方法有一些缺点:

1。必须在第一次服务调用之前创建服务实例。懒惰初始化是不可能的。这将延迟启动并将资源绑定到很少使用甚至从不使用的服务。

1b中。您在使用后“无法”关闭服务,因为无法重新实例化它们。 (使用提供程序,您可以以调用者必须调用#close()的方式设计服务接口,这会通知提供者,提供者决定保留或最终确定服务实例。)< / p>

2。所有调用者都将使用相同的服务实例,因此您必须确保它是线程安全的。但是使它成为线程安全会使它变慢。相反,提供商可能会选择创建几个服务实例以减少保留时间。

<强>结论

不需要提供程序接口,但它封装了特定于服务的实例化逻辑并优化了资源分配。