gossip 服務(一)重構


像〈@RepositoryRestResource〉中的範例專案,將功能實作為獨立的服務,需要服務的其他服務或應用程式,就可以基於它來建立,而提供服務的專案本身還可以獨立演化,感覺好像真的不錯,下個案子就這樣做嗎?

真的要這樣做嗎?從無到有建立逐個服務,並且將之組合起來,似乎是比較簡單?問題是,有辦法真的服務彼此間不互相影響嗎?如果傳統單一應用程式中,應用程式的元件之間,都沒辦法做到 program to interface,沒辦法做到一定程度的解耦合,那麼拆成許多服務的話,服務之間的耦合只會讓你疲於奔命吧!

另一方面,事先規劃的服務真的是有用嗎?真的值得獨立出來嗎?會不會實際上根本還是只有一個應用程式與之溝通呢?就像單一應用程式中的過度設計?定義的介面根本就只有一個實作?服務之間的管理、部署呢?搞不好這根本只是公司內部幾個小部門會用到的東西,值得嗎?

當然,也許真的值得做,也許公司上層就是要做,只要運用的技術有足夠的文件,從無到有來做或許真的比較簡單一些,然而,現實上可能比較多的情況是,既有的單一應用程式,因為某些需求,必須將其中的元件抽取出來成為服務,以便其他服務或應用程式也可以取用,這時該怎麼做?

從另一個角度來看,如果真的能做到,從既有應用程式抽取出服務,因為是有實際的需求,目標比較明確,抽出的服務也比較有價值,服務之間的架構也不致於天馬行空,可以階段性地增加彈性,而不是一開始就來個巨大的架構。

或許有個簡單的原型程式,會比較好評估要不要與應該怎麼做吧!嗯?這系列文件一直在惡搞的 gossip 應用程式可以嗎?有趣!就來試試看吧!首先,來看看要從哪開始!

就從〈套用 Spring Data JDBC〉 的 gossip 成果來談好了,至少這是個遵守 MVC 架構,Model、View、Controller 都做了適當劃分,彼此間有適當的隔離變化,作為呈現層的 Controller 與 View,與作為服務及儲存的 Model 之間界線清楚,會有助於前後端分離,從而促進服務的劃分,如果你的單一應用程式,連這個要求都做不到的話,要做的是重構,不然請打消抽取服務這個念頭!

接下來要做的事情,稍微用圖表示一下會比較清楚,首先,必須知道 gossip 的架構:

gossip 服務(一)重構

蠻單純的架構(單純真好!)應用程式運行在一個容器中,使用 Spring MVC 基於 Servlet 實作,與外部的郵件及資料庫伺服器溝通,非常傳統的一個應用程式。

接著,在〈分離 gossip 組態〉為了示範如何管理共用的組態資訊,gossip 成了這個架構:

gossip 服務(一)重構

其實也不複雜,就是抽出共用的組態罷了;假設因為業務需求,現在需要將 gossip 中的帳號功能、訊息功能以及郵件功能抽取出來成為三個獨立的服務,那麼就要先檢視一下目前 gossip 應用程式本身的這些元件,功能上是否獨立。

郵件功能比較沒有問題,本身已經是個獨立的元件,帳號功能、訊息功能就有點問題,因為目前是由 UserService 實現,那麼第一步就還是重構,必須從 UserService 中拆出帳號功能、訊息功能,既然如此,那就直接查看 UserService 中有關帳號的公開行為,定義出 AccountService 介面:

package cc.openhome.model;

import java.util.Optional;

public interface AccountService {
    Optional<Account> tryCreateUser(String email, String username, String password);
    boolean userExisted(String username);
    Optional<Account> verify(String email, String token);
    Optional<Account> accountByNameEmail(String name, String email);
    void resetPassword(String name, String password);
}

然後,將 UserService 中有關帳號的實作,全部由 AccountServiceLocal 來實現,當然 AccountServiceLocal 實作 AccountService 介面;至於訊息功能,也是根據 UserService 定義出 MessageService,並由 MessageServiceLocal 來實現:

package cc.openhome.model;

import java.util.List;

public interface MessageService {
    List<Message> messages(String username);
    void addMessage(String username, String blabla);
    void deleteMessage(String username, String millis);
    List<Message> newestMessages(int n);
}

UserService 就可以刪掉了,然後,本來相依於 UserService 的元件,現在必須分別改用 AccountServiceMessageService,以 DisplayController 為例:

package cc.openhome.controller;

...略

@Controller
public class DisplayController {
    @Value("${path.view.index}")
    private String INDEX_PATH;

    @Value("${path.view.user}")
    private String USER_PATH;

    @Autowired
    private AccountService accountService;

    @Autowired
    private MessageService messageService;

    @GetMapping("/")
    public String index(Model model) {
        List<Message> newest = messageService.newestMessages(10);
        model.addAttribute("newest", newest);
        return INDEX_PATH;
    }

    @GetMapping("user/{username}")
    public String user(
            @PathVariable("username") String username,
            Model model) {

        model.addAttribute("username", username);
        if(accountService.userExisted(username)) {
            List<Message> messages = messageService.messages(username);
            model.addAttribute("messages", messages);
        } else {
            model.addAttribute("errors", Arrays.asList(String.format("%s 還沒有發表訊息", username)));
        }
        return USER_PATH;
    }
}

想要將單一應用程式中的元件抽取出來成為服務之前,必須先得能做到這點,你也看得出來,這不過就是 program to interface、解耦合、隔離變化的概念,更具體地說,在重構之後,有了帳戶、訊息、郵件三個界線清楚、可抽換的模組,模組可能就暗示著,這是一個可抽取的服務,做好這種事前準備,在進一步抽取為服務時,遇到的阻力會少一點!

你可以在 GossipSvi/1st 中找到以上的重構結果,為了方便,也將組態伺服器專案放了進去。