像〈@RepositoryRestResource〉中的範例專案,將功能實作為獨立的服務,需要服務的其他服務或應用程式,就可以基於它來建立,而提供服務的專案本身還可以獨立演化,感覺好像真的不錯,下個案子就這樣做嗎?
真的要這樣做嗎?從無到有建立逐個服務,並且將之組合起來,似乎是比較簡單?問題是,有辦法真的服務彼此間不互相影響嗎?如果傳統單一應用程式中,應用程式的元件之間,都沒辦法做到 program to interface,沒辦法做到一定程度的解耦合,那麼拆成許多服務的話,服務之間的耦合只會讓你疲於奔命吧!
另一方面,事先規劃的服務真的是有用嗎?真的值得獨立出來嗎?會不會實際上根本還是只有一個應用程式與之溝通呢?就像單一應用程式中的過度設計?定義的介面根本就只有一個實作?服務之間的管理、部署呢?搞不好這根本只是公司內部幾個小部門會用到的東西,值得嗎?
當然,也許真的值得做,也許公司上層就是要做,只要運用的技術有足夠的文件,從無到有來做或許真的比較簡單一些,然而,現實上可能比較多的情況是,既有的單一應用程式,因為某些需求,必須將其中的元件抽取出來成為服務,以便其他服務或應用程式也可以取用,這時該怎麼做?
從另一個角度來看,如果真的能做到,從既有應用程式抽取出服務,因為是有實際的需求,目標比較明確,抽出的服務也比較有價值,服務之間的架構也不致於天馬行空,可以階段性地增加彈性,而不是一開始就來個巨大的架構。
或許有個簡單的原型程式,會比較好評估要不要與應該怎麼做吧!嗯?這系列文件一直在惡搞的 gossip 應用程式可以嗎?有趣!就來試試看吧!首先,來看看要從哪開始!
就從〈套用 Spring Data JDBC〉 的 gossip 成果來談好了,至少這是個遵守 MVC 架構,Model、View、Controller 都做了適當劃分,彼此間有適當的隔離變化,作為呈現層的 Controller 與 View,與作為服務及儲存的 Model 之間界線清楚,會有助於前後端分離,從而促進服務的劃分,如果你的單一應用程式,連這個要求都做不到的話,要做的是重構,不然請打消抽取服務這個念頭!
接下來要做的事情,稍微用圖表示一下會比較清楚,首先,必須知道 gossip 的架構:
蠻單純的架構(單純真好!)應用程式運行在一個容器中,使用 Spring MVC 基於 Servlet 實作,與外部的郵件及資料庫伺服器溝通,非常傳統的一個應用程式。
接著,在〈分離 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
的元件,現在必須分別改用 AccountService
或 MessageService
,以 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 中找到以上的重構結果,為了方便,也將組態伺服器專案放了進去。