Wednesday, July 17, 2013

聊聊Service吧!

聊聊Service吧!

我們在一個大型系統裡面,常常會用到許多Service,這些Service通常來講會以Singleton的形式存在(或者他的變形,比方說在Spring裡面就是@Autowire)。這些系統用的Service通常會有一個共通的界面,比方說,這些Service都是CRUD服務的話,大概會長這樣…

interface IService {
    String getServiceName();
    String getServiceID();
}

interface ICRUDService<Target> extends IService {
    long create(Target t) throw ServiceException;
    T read(long uuid) throw ServiceException;
    long update(Target t) throw ServiceException;
    bool delete(long uuid) throw ServiceException;
}

淺顯易懂沒有任何問題。假設我們有一個Service叫做NameCard service,我們可能就會這樣實作…

public class NameCardService implements ICRUDService<NameCard> {
    // ICRUDService implementation is here
    …
    …
    …

    //NameCardService only public method
    public void sort();
    public List<NameCard> getDuplicated();
}

當我們需要取用這個服務的時候,有很多種不同的做法。最普通的Singleton風格就是

NameCardService service = NameCardService.getInst();

而Spring風格則會是

@Autowire
NameCardService service;

而Android風格則會是

NameCardService service = (NameCardService)this.getSystemService(NAME_CARD_SERVICE);

前兩者其實都沒什麼型別安全上的問題,第一個做法會有Scaling上的問題,我們必須要在getInst()裡面檢查太多東西(別忘了,getInst()是static,沒辦法被包進interface -- 這點顯然obj-c是好一點),比方說有沒有權限拿取啊,該做哪些logging阿....balabalaba,這些通通都是duplicated code -- 因為他們沒辦法被寫入interface。第二種作法則是綁死在Spring,而且我想應該不用我說,大家也知道Spring新增一個服務並沒有那麼簡單直觀。

所以Android選擇了3,但是很明顯地,這邊存在一個無法在編譯期檢查出來的型別問題,也就是說,你可能無法阻止別人寫成這樣

NameCardService service = (NameCardService)this.getSystemService(TELEPHONY_SERVICE);

因為它getSystemService傳回來的是一個類似IService的東西,這等於要強迫使用者使用「正確的Context」「正確的Service Handle」去轉型成「正確的Service」

以一個寫Framework的人來講,這真是惡夢 - 誰曉得Framework user這些工程師會不會拿一些莫名其妙的錯用來當作bug開給你?

//以下當然會執行期爆掉
((NameCardService)this.getSystemService(TELEPHONY_SERVICE)).sort();

//我必須對某些喜歡這種懶人寫法的人致敬,這些人專注於寫出讓人(包含自己)看不懂的code的努力,實在是令人太欽佩了。

重點在於Java來講,用任何一個Native型別去當作Handle都是一種頗令人困惑而且不智的行為。什麼是Handle?其實就相當於map裡面的key,用這個key去取得value(在這裡就是指Service)。有興趣的人可以看看Android的getSystemService(...)裡面傳入的參數是什麼?結論會很讓你噴飯,他傳入的Handle索引居然是一個字串…不過這有他的道理,他傳入的字串是一組class的qualified name,這可以讓他很輕易地「無中生有」出一組service -- 我們不在這裡討論這種規格外的做法。另外,傳入string還有一個非常糟糕的用途--它可以把要傳給service的參數包在一部分的String傳進去,很方便,很好用,我們也可以想像這是維護上的多大的災難。

有沒有什麼方法至少可以逼使用者不用轉型,甚至在編輯器裡面就可以輕鬆地幫你指出你拿到的是什麼Service而不會出錯呢?比方說我們能不能在編譯時期就檢查出下面這寫法對不對

//拿出來就是NameCardService,不需要轉型
BaseCRUDService.getService(???????????).sort();
//拿出來就是Telephony Service,不需要轉型
BaseCRUDService.getService(???????????).getTeleponyStatus();

甚至Eclipse就可以幫你檢查出來了!這乾五可能!?

廢話,當然可以,不然我寫這篇幹嘛 XD

這秘密在於我們Handle的選擇。String?這顯然不對,long?這當然也不行,特殊規格的class?我沒想到要怎麼用這種方法,也許誰來發明一下

我們Handler選用的是Java Generic可以直接支援的Class<?>

老實講我一直覺得Java Generic雖然靈活度不比C++的Template,不過Generic裡面的"?"實在是一個很無賴的東西,它可以做出很多平常根本想當想不到的方法。

首先,我們為了讓所有的CRUD Service都會「註冊自己」,讓自己可以被CURDService找到,所以我們必須要有一個「所有的CRUDService都會跑得到的地方」--我們當然第一個想到的就是constructor,所以我們要讓所有的CRUD Service從implements ICRUDService改成extend BaseCRUDService

abstract class BaseCRUDService<T> implements ICRUDService<T> {
    BaseCRUDService() {
    //現在我們有一個地方可以讓所有的CRUD Service都跑到了
    }
}

public class NameCardService extends BaseCRUDService<NameCard> {
…
…
…
}

這邊有一份實作,可以先給大家參考一下,出來差不多就是我剛剛講的結果。所有的Service只要繼承BaseCRUDService,就可以用BaseCRUDService.getService(Class<? extends BaseCRUDService> clz);拿出正確的Service,而且可以直接編譯期判斷出該型別,並且可以用編譯器的自動完成去呼叫該Service獨有的method。

import java.util.HashMap;

public abstract class BaseCRUDService<MarshelBean extends ICRUDBean> implements ICRUDService<MarshelBean> {

    protected BaseCRUDService() {
        registerSelf(this);
    }

    @SuppressWarnings({ "unchecked" })
    private <T extends ICRUDService<?>> void registerSelf(T service) {
        serviceMap.put( (Class<? extends ICRUDService<?>>) service.getClass(), service);
    }

    /**
     * For thread safety, it can be ConcurrentHashMap...as long as I don't think it is needed.
     * Since it is single thread write one time only, HashMap can be good enough in any case.
     */
    static HashMap<Class<? extends ICRUDService<?>>, ICRUDService<?>> serviceMap = new HashMap<Class<? extends ICRUDService<?>>, ICRUDService<?>>();

    @SuppressWarnings("unchecked")
    static public <T extends ICRUDService<?> > T getService(Class<T> targetService) {
        return (T) serviceMap.get(targetService);
    }

    public abstract <T extends ICRUDService<?>, F extends ICRUDBean> T handle(F targetBean);
}

這份實作神奇的地方在於,只要任何服務繼承它,那它就可以被「型別安全的」取出來。這點很重要,請跟著我重複念兩次。另外,這份實作是取自於我自己寫的一個PP通訊界面,所有CRUD都會至少有一個Marshel(管理)的對象,這個被管理的對象一定會繼承自IBean,如果你不知道他是幹嘛的,請完全不用理他。

class PeopleRecordService extends BaseCRUDService<PeopleRecord&gt {
…
…
…

    void people_record_only_function();
}

class CarRecordService extends  BaseCRUDService<CarRecord&gt {
…
…
…
    void car_record_only_function();
}

好,我們把這個東西setup了,該試試看他的威力了。

//PASS!
BaseCRUDService.getService(PeopleRecordService.class).people_record_only_function();

//Compile Time error!
BaseCRUDService.getService(PeopleRecordService.class).car_record_only_function();

//或著試試看下面這邊自動完成會幫你秀出什麼東西?
BaseCRUDService.getService(CarRecordService.class).;

That's it~ 沒有強制轉型,沒有奇怪的Context問題,更沒有duplicated static method,世界真是美好...