引子
最近看《Java Application Architecture-Modularity Patterns with Examples Using OSGi-中文譯名Java應(yīng)用架構(gòu)設(shè)計》時,在物理模塊的封裝設(shè)計方面深受啟發(fā)。市面上很多書都是介紹類的封裝、訪問性控制、邏輯設(shè)計,但是關(guān)于物理模塊的設(shè)計的書并不多見,這本書是這方面的好書,有Bob大叔推薦作序。同時此書推薦的一本老書《Large-Scale C++ Software Design》也是這方面的經(jīng)典好書。就像這本書所說的缺乏物理設(shè)計的邏輯設(shè)計并不會帶來預(yù)期的影響,深有同感。
問題描述
討論一個和參考資料[1]中3.4節(jié)、[2]、[3]中描述的問題相同的簡化問題。根據(jù)面向接口編程的理念,提供服務(wù)的模塊只暴露服務(wù)接口,隱藏實現(xiàn)??蛻舳四K以接口訪問服務(wù)端模塊的服務(wù)??蛻舳四K中不能出現(xiàn)任何具體實現(xiàn)類的引用耦合。這樣便于以后改變服務(wù)端模塊實現(xiàn)的同時不影響客戶端模塊。我們希望具體實現(xiàn)類對客戶端模塊不可見。這樣在提供服務(wù)端模塊時強制以接口公開服務(wù)。
解決方法
[1]中采用Spring框架注入實現(xiàn)類,[3]中描述了采用ServiceLoader和META-INF注入實現(xiàn)類。以下以[2]中類似的代碼為例介紹Java的方法,此處沒有采用框架,僅僅是用了一個簡單的工廠控制實現(xiàn)類的注入。并和.NET的解決方法對比。這里采用參考資料[1]中的模塊定義,Java定義jar文件為物理模塊單元,.NET定義程序集dll文件為物理模塊單元,這也是我們平時常用引用第三方類庫的方法。
Java的解決方法
服務(wù)接口
package org.p2.helloworld;
public interface HelloService {
void sayHello(String name);
}
實現(xiàn)類
package org.p2.helloworld.impl;
import org.p2.helloworld.HelloService;
public class HelloServiceImpl implements HelloService {
public void sayHello(String name){
System.out.println("Hello," + name);
}
}
簡單的工廠控制實現(xiàn)
package org.p2.helloworld;
import org.p2.helloworld.impl.HelloServiceImpl;
public class HelloFactory {
public static HelloService getHelloService() {
return new HelloServiceImpl();
}
}
服務(wù)模塊單元provider.jar包含以上幾個包,客戶端client.jar內(nèi)容如下。
package org.p1.helloworld.main
import org.p2.helloworld.*;
public class Main {
public static void main(String[] args) {
HelloService helloService = HelloFactory.getHelloService();
helloService.sayHello("World");
}
}
以上實現(xiàn)由于將接口和實現(xiàn)類放在了不同的包中,所以實現(xiàn)類可見性必須為public。如果將實現(xiàn)類和接口放在同一個包中,則實現(xiàn)類可見性可設(shè)置為僅包可見。實現(xiàn)類代碼如下,其他類同上,可實現(xiàn)provider.jar僅向外暴露接口。
class HelloServiceImpl implements HelloService {
public void sayHello(String name){
System.out.println("Hello," + name);
}
}
.NET解決方案討論
服務(wù)接口
namespace Provider
{
public interface HelloService
{
void SayHello(string name);
}
}
實現(xiàn)
namespace Provider.Impl
{
internal class HelloServiceImpl : HelloService
{
public void SayHello(string name)
{
Console.WriteLine("Hello," + name);
}
}
}
工廠類
using Provider.Impl;
namespace Provider
{
public class HelloServiceFactory
{
public static HelloService GetHelloService()
{
return new HelloServiceImpl();
}
}
}
以上為服務(wù)模塊單元provider.dll包含內(nèi)容,.NET下可實現(xiàn)將實現(xiàn)類和接口放在不同命名空間下,同時向外僅暴露接口。
分析討論
以上將HelloService接口和實現(xiàn)類HelloServiceImpl放在了同一個物理provider.jar下的兩個包下,以provider.jar類庫形式提供給客戶端。無論是采用[1]中的Spring框架注入實現(xiàn)類,還是[3]中的方法注入實現(xiàn)類,如果將實現(xiàn)類和接口放在不同包中,都必須將HelloServiceImpl的包可見性設(shè)置為public。這樣導致了一旦客戶端模塊引用了服務(wù)端模塊并導入包,則可直接實例化實現(xiàn)類。這就是參考資料里所說的Java沒有提供將包或類定義為模塊作用域的方法,導致一個模塊中的類總是能夠訪問另一個模塊的實現(xiàn)細節(jié),這也OSGi這樣的框架致力解決的問題。如果將實現(xiàn)類和接口放在一個包中則可以向外僅暴露接口,但同一物理模塊jar下的其他包無法使用實現(xiàn)類。
對比.NET下的解決方法,.NET下可以將實現(xiàn)類設(shè)置為internal,物理模塊內(nèi)可見,對引用此模塊的客戶端程序不可見。而Java的包可見性有邏輯方面和物理設(shè)計方面的限制,并不是很純粹。因此java的包和.NET的命名空間有很大不同。個人猜測這可能和跨平臺有關(guān),畢竟物理模塊和具體平臺有關(guān)。結(jié)合實際的應(yīng)用情況,確實需要物理方面的可見性控制,這樣才能提供更好的封裝性。物理模塊的組織設(shè)計及良好的封裝性確實是這本書給我最大的啟示。