如何解決模板式的冗餘代碼問題?

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當項目中在使用到諸如 Elasticsearch 的中間件時,客戶端對不同數據模型的 CRUD 操作存在着大量模版式的冗餘代碼,每次有新的業務數據需要 Elasticsearch 的管理時都會重寫類似的 CRUD 邏輯,這些 CRUD 代碼除了數據模型不同,通用功能的代碼邏輯幾乎一樣。顯然,在這種情況下,我們完全可以抽取出通用功能的代碼,將其定義成一個模版。當接入具體的業務數據時,只需要進行模版實例化的代碼書寫,把因業務不同的數據模型嵌入到模版中,從而避免重複書寫功能相同的代碼,最終達到提高開發效率,降低開發成本的目的。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實際的項目開發中,如何解決模板式的冗餘代碼?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,考慮到 Java 語言以及 ElasticSerarch 在項目開發中的普及性,我選擇基於 Java 語言使用 Elasticsearch 的技術場景來澄清以及解決模板式的代碼冗餘問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次,爲了使得問題的澄清和解決更具象化,我以簡單的用戶信息和訂單信息爲例,來闡述澄清和解決問題的具體過程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,我們先定義一下簡單用戶信息和訂單信息的數據模型:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * 簡單用戶信息數據模型。\n *\/\npublic class UserInfo {\n private String id; \/\/ ID\n private String nickName; \/\/ 暱稱\n private Integer age; \/\/ 年齡\n private String introduction; \/\/ 簡介\n private String signature; \/\/ 簽名\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * 簡單訂單信息數據模型。\n *\/\n@Data\npublic class OrderInfo {\n private String id; \n private String productId; \/\/ 產品 ID\n private Integer productNum; \/\/ 產品數據量\n private Integer status; \/\/ 訂單狀態\n private String remark; \/\/ 備註\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"什麼是模板式的冗餘代碼?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,我們澄清一下模版式的冗餘代碼問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當用戶信息和訂單信息接受 Elasticsearch 的管理時,直接使用 Elasticsearch 客戶端 API 創建索引的代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * ES 創建用戶信息文檔。\n *\/\npublic ServiceResponse create(UserInfo userInfo) {\n \/\/ 參數檢查邏輯。\n String userInfoId = userInfo.getId();\n if (StringUtils.isEmpty(userInfoId)) {\n return ServiceResponse.FAIL_RESPONSE;\n }\n \/\/ 創建索引請求。\n IndexRequest indexRequest = new IndexRequest(\"user_info_index\", \"user_info_type\", userInfoId);\n String documentData = JSON.toJSONString(userInfo);\n indexRequest.source(documentData, XContentType.JSON);\n indexRequest.opType(DocWriteRequest.OpType.INDEX);\n \/\/ 調用客戶端 API。\n try {\n InitESRestClient.getClient().index(indexRequest, RequestOptions.DEFAULT);\n } catch (Exception e) {\n log.error(\"error when save document:{}.\", JSON.toJSONString(userInfo), e);\n return ServiceResponse.FAIL_RESPONSE;\n }\n return ServiceResponse.SUCCESS_RESPONSE;\n}\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * ES 創建訂單信息文檔。\n *\/\npublic ServiceResponse create(OrderInfo orderInfo) {\n \/\/ 參數檢查邏輯。\n String orderInfoId = orderInfo.getId();\n if (StringUtils.isEmpty(orderInfoId)) {\n return ServiceResponse.FAIL_RESPONSE;\n }\n \/\/ 創建索引請求。\n IndexRequest indexRequest = new IndexRequest(\"order_info_index\", \"order_info_type\", orderInfoId);\n String documentData = JSON.toJSONString(orderInfo);\n indexRequest.source(documentData, XContentType.JSON);\n indexRequest.opType(DocWriteRequest.OpType.INDEX);\n \/\/ 調用客戶端 API。\n try {\n InitESRestClient.getClient().index(indexRequest, RequestOptions.DEFAULT);\n } catch (Exception e) {\n log.error(\"error when save document:{}.\", JSON.toJSONString(orderInfo), e);\n return ServiceResponse.FAIL_RESPONSE;\n }\n return ServiceResponse.SUCCESS_RESPONSE;\n}\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了讓大家更清晰地關注到代碼中的重點,這裏說明一下代碼上的約定:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"ServiceResponse 是一個通用的業務對象,用來表達業務邏輯執行成功或失敗的信息;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"InitESRestClient.getClient() 用來獲取默認的 Elasticsearch 集羣客戶端;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"其他未特別說明的都是標準的 Java 語言和 Elasticsearch 客戶端 API。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面的代碼我們可以看出,無論對於用戶信息的索引創建還是訂單信息的索引創建,我們無可避免地要寫參數檢查、創建索引請求、調用客戶端 API 的相同邏輯。這些邏輯的代碼看着似乎都是不一樣的,但整體的邏輯都是相同的,就像是一個執行流程的模板,只是模板實例化時傳遞的參數不一樣而已。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從代碼冗餘的角度講,我們完全沒有必要,對用戶信息和訂單信息索引創建都寫一份邏輯相似的代碼,即使他們看上去並不是一摸一樣。像上面這樣,看上去並不是一摸一樣的代碼,但是存在着一樣的處理邏輯,使用着相同形式的代碼,我們就叫做模板式的冗餘代碼。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"定義模板"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從用戶信息與訂單信息的索引創建過程中,我們發現模板式的冗餘代碼主要由兩部分構成。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個是不變的部分,即對任意的數據模型,這些部分的處理都是一樣的,主要有以下幾點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"對數據中的唯一標識做合法性檢查,即通用參數的檢查邏輯;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"構建 Elasticsearch API 的 IndexRequest,即構建索引創建請求;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"獲取 Elasticsearch 的集羣客戶端發送索引創建請求並進行異常處理,即通用的發送請求並進行異常處理的邏輯。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一個是變化的部分,這些部分都是根據不同的數據模型而變化的,主要有以下幾點:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"傳入參數不同,用戶信息傳入的是 UserInfo,而訂單信息是 OrderInfo;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"數據配置不同,用戶信息進入的 user_info_index 索引的 user_info_type,而訂單信息進入的是 order_info_index 索引的 order_info_type;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"集羣選擇可能不同,這裏用戶信息和訂單信息都採用的默認的集羣,但實際項目開發中可能用戶信息在集羣 1,訂單信息在集羣 2。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,我們想要避免模板式的冗餘代碼,那麼必然要將上述變化的部分和不變的部分定義在一個模板裏,這樣相似的代碼只會存在於模板內。然而,上述不變的部分我們可以直接定義在模板內,而變化的部分我們怎麼在模板內抽象表達呢?下面我就基於 Java 語言以 Elasticsearch 索引創建的過程爲例具體定義一個包含上述不變和變化部分的模板。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"方法定義"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們在模板中定義創建索引的方法,在 Java 語言中想要實現上述模板的功能,最好的語言組件自然是 Java 中的抽象類了,同時這裏爲了在模板中描述方法中變化的參數,即 UserInfo、OrderInfo 等數據模型,我們必然要使用到 Java 中的泛型參數在抽象類對數據模型進行抽象統一。所以我們初步定義的包含創建索引功能的抽象類就是下面這個樣子了:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"\/**\n * 模板的定義。\n * DOC 泛型參數的定義是對模板中變化部分的抽象,\n * 對接受 Elasticsearch 管理的數據進行統一表示。\n *\/\npublic class AbstractBaseESServiceImpl {\n \/**\n * 在模板中提供創建索引的功能,該功能可對任意數據模型生效。\n *\/\n public ServiceResponse create(DOC doc) {}\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述代碼基於 Java 中的抽象類和泛型參數對模板做了一個初步的定義,該模板暫時只包含創建索引的功能,但是具體的創建索引的方法並沒有實現,爲什麼呢?因爲我們要想實現上述創建索引的參數檢查,創建索引請求,發送請求的邏輯,我們必須先知道數據模型中那些通用字段需要做參數檢查,該數據要去那個索引和那個類型,該數據要進入那個集羣這些信息,顯然我們僅僅根據 Doc 類型的 doc 參數是無法獲取這些信息的。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"元數據初始化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼如何獲取上述所述的信息呢?這時我們就可以利用 Java 語言強大的元語言編程能力,在對象初始化的時候,獲取到泛型參數實例化後實際數據模型的元數據。當有了這些準備好的元數據後,就可以在方法運行時,根據實際參數和元數據獲取到相關的信息從而完成創建索引的邏輯。所以,接下來我們要做的就是如何準備元數據可以獲取到上述那些通用字段需要做參數檢查,數據要進入那個索引和那個類型的信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Java 語言中,元數據信息都是存放在 Class 類型的對象中,所以我們可以在對象初始化的時候先獲取到上述 DOC 泛型參數所表示的數據模型的 Class 對象,代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"cs"},"content":[{"type":"text","text":"\/**\n * DOC 數據模型的 Class 對象。\n *\/\nprivate Class docClass;\n\/**\n * 初始化元數據。\n *\/\npublic AbstractBaseESServiceImpl() {\n this.docClass = this.getDocClass();\n}\n\/**\n * 獲取 DOC 數據模型的元數據。\n * 這裏主要是獲取子類泛型參數實例化後實際數據類型的 Class 對象。\n *\/\nprivate Class getDocClass() {\n Type genericSuperclass = this.getClass().getGenericSuperclass();\n Type docType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];\n if (!(docType instanceof ParameterizedType)) {\n return (Class) docType; \n } \n return (Class)((ParameterizedType)docType).getRawType();\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述代碼都是基於 Java 語言反射機制的實現,具體 API 這裏就不細說了。大概思路就是在對象初始化時,使獲取到元數據,即在模板中獲取到實際數據類型的元數據,爲後面獲取具體變化的部分信息做準備。變化部分的信息主要包括兩方面,一是哪些通用字段需要做參數校驗,二是數據進入哪個 Elasticsearch 索引和類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"通用字段元數據的準備"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們先解決獲取通用字段元數據信息的問題,從而爲通用字段的參數校驗做準備。Java 語言中,我們可以通過註解,在各個組件中添加一些標記信息。自然而然地,我們可以自定義註解在實際數據模型定義中標記那些通用字段,根據該標記信息我們可以獲取到需要做參數校驗地通用字段的元數據信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如上述我們要求任何接受 Elastic 管理的數據都要定義唯一標識,且要求唯一標識是不爲空的字符串類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何實現這個需求呢?這時候我們就可發揮 Java 中註解的強大功能了,比如我們自定義一個 DocID 的註解,該註解只能加在 Java 類的字段上,而且必須是不爲空的字符串類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,我們先自定義一個 DocID 的註解,代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"css"},"content":[{"type":"text","text":"\/**\n * 定義 DOC 數據模型中的唯一標識字段。\n * 該註解用於在實際的數據模型中標記 Elasticsearch 文檔的唯一標識。\n *\/\n@Target({ElementType.FIELD})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface DocID{}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然,該註解的處理邏輯必然是要在模板對象即上述抽象類對象初始化時存在,也就是說,我們要在上述抽象類初始化時,根據數據模型的元數據獲取到標記了該註解的字段的元數據,從而爲後面通用參數的校驗做準備,這段邏輯具體的代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"kotlin"},"content":[{"type":"text","text":"\/**\n * 標識字段元數據,用於處理唯一標識相關邏輯,比如參數檢查。\n *\/\nprotected final Field idField;\n\/**\n * 初始化元數據。\n *\/\npublic AbstractBaseESServiceImpl() {\n this.docClass = this.getDocClass();\n this.idField = this.getIDField();\n this.idField.setAccessible(true);\n}\n\/**\n * 獲取文檔的 ID 字段元數據。\n * 要求用戶在使用時,必須通過自定義註解 DocID,告訴我們實際數據模型\n * 哪個字段是 ID 字段,從而獲取到 ID 字段的具體值,用於通用參數的邏輯檢查以及和唯一標識\n * 相關的邏輯。\n *\/\nprivate Field getIDField() {\n Field[] declaredFields = this.docClass.getDeclaredFields();\n Field idField = null;\n for (Field declaredField : declaredFields) {\n DocID docID =declaredField.getAnnotation(DocID.class);\n if (null == docID) {\n continue;\n }\n idField = declaredField;\n break;\n }\n \/\/ 要求數據模型中必須定義 ID 字段。\n if (null == idField) {\n throw new DocIDUndefineException();\n }\n \/\/ 要求 ID 字段的類型爲字符串。\n if (idField.getType() != String.class) {\n throw new DocIDNotStringException();\n }\n return idField;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述代碼,首先我們通過之前已經準備好的數據模型元數據獲取到所有字段的元數據,然後找到標記了 DocID 註解的字段的元數據。這裏因爲我們要求數據模型必須通過 DocID 定義 ID 字段,所以如果沒有定義該字段,那麼就拋出 DocIDUndefineException 異常,提示用戶通過 DocID 註解在數據模型中定義該字段。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的道理,我們要求 ID 字段必須是字符串類型的,如果用戶定義的該字段不是字符串類型的,就拋出 DocIDNotStringException 的異常提示用戶修正該字段的類型。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後如果滿足 DocID 的語義,我們就正常獲取到該字段的元數據信息了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Elasticsearch 索引和類型信息的準備"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,我們解決數據進入哪個 Elasticsearch 索引和類型的數據配置信息獲取的問題。仔細思考這個問題,我們會發現,這兩個信息的作用和具體的數據模型是有關係的。正如上文所述,用戶信息數據要進入的 user_info_index 索引的 user_info_type,而訂單信息數據要進入的是 order_info_index 索引的 order_info_type。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,很顯然,這樣的信息,我們只需要提供一種方式可以讓用戶在模版實例化時將索引和類型的信息與具體的數據模型綁定在一起。順其自然地,我們在定義模版時,只需要根據綁定信息所採用的方式獲取到這些信息的抽象表示,然後實現模版本身該有的邏輯即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Java 語言中,上述所說的方式,我們依舊可以採用註解的方式來實現。例如,這裏我們定義如下的註解,用於在具體的數據模型上標記出該數據模型的具體索引數據在進入 Elasticsearch 中的哪個索引的哪個類型的信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"bash"},"content":[{"type":"text","text":"\/**\n * 定義實際數據進入 Elasticsearch 中作爲文檔時的邏輯存儲位置。\n * 要求,接受 Elasticsearch 管理的實際數據的數據模型必須添加該註解。\n *\/\n@Target({ElementType.TYPE})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Document {\n \/**\n * 索引名稱,必須值。\n *\n * @return\n *\/\n String index();\n \/**\n * 索引類型名,默認\"_doc\"。\n * ES6 保留了對 type 的支持,兼容了之前版本的多 type,但是 ES6 本身只支持創建一個 \n * type,默認 type 名稱爲\"_doc\"。\n * 按照 ES 的發展規劃,ES7 會廢棄 type,ES8 會刪除 type。\n * 廢除 type 的原因:1. 導致數據稀疏;2. 干擾 lucene 對於文檔的壓縮編碼。\n * {@see https:\/\/www.elastic.co\/guide\/en\/Elasticsearch\/reference\/6.6\/removal-\nof-types.html}\n *\n * @return\n *\/\n String type() default \"_doc\";\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於自定義註解 Document,我們要求用戶在定義接受 Elasticsearch 管理的數據模型時,必須在類上添加該註解,通過註解中的 index、type 字段爲模版中要使用到的變化的信息,即數據進入哪個 Elasticsearch 索引和類型,進行賦值。那麼,我們如何在模版的抽象類中獲取到這些信息呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的道理,在 Java 語言中,我們在類的實例化階段基於數據模型的反射元數據獲取到註解的相關信息,存放在實例域中,在模版方法的實現中使用即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * 文檔註解的 Class 對象。\n *\/\nprotected Class documentClass;\n\/**\n * 指定文檔的索引。\n *\/\nprivate final String index;\n\/**\n * 指定文檔的類型。\n *\/\nprotected final String type;\n\/**\n * 初始化元數據。\n *\/\npublic AbstractBaseESServiceImpl() {\n this.documentClass = this.getDocumentClass();\n this.index = this.getDocumentAnnotation().index();\n this.type = this.getDocumentAnnotation().type();\n this.idField = this.getIDField();\n this.idField.setAccessible(true);\n}\n\/**\n * 獲取文檔註解的元數據,即註解的 Class 對象。\n * 這裏主要是通過獲取子類泛型參數實例化後實際數據類型的元數據對象來獲取註解的元數據 \n * 對象。\n *\/\nprivate Class getDocumentClass() {\n Type genericSuperclass = this.getClass().getGenericSuperclass();\n Type documentType = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];\n if (!(documentType instanceof ParameterizedType)) {\n return (Class) documentType; \n } \n return (Class)((ParameterizedType)documentType).getRawType();\n}\n\/**\n * 獲取有效的的 Document 註解。\n * 這裏的 Document 註解是我們自己定義的,用於定義不同數據模型的 Elasticsearch 索引和\n * 類型。\n *\/\nprivate Document getDocumentAnnotation() {\n Document document = this.documentClass.getAnnotation(Document.class);\n \/\/ 我們要求要使用我們的組件的用戶在定義實際數據類型時必須使用我們的註解。\n if (null == document) {\n throw new DocumentAnnotationAbsentException();\n }\n \/\/ 註解必須告訴組件該數據模型要進入的索引。\n String index = document.index();\n if (StringUtils.isBlank(index)) {\n throw new BlankIndexNameException();\n }\n return document;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"getDocumentClass 方法主要通過數據模型的元數據對像獲取到 Document 註解的元數據對象。getDocumentAnnotation 方法主要通過註解的元數據獲取到具體數據模型上標記的具體的 Document 註解。這裏因爲我們要求必須添加對 Document 註解的語義定義,且必須指定索引值,所以,當未檢測到註解爲空時,我們拋出 DocumentAnnotationAbsentException 異常,提醒用戶去添加註解。同樣的道理,檢測到索引 index 字段未定義時,拋出 BlankIndexNameException 異常,提醒用戶去定義索引。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的道理,我們在抽象類初始化的時候,基於獲取到的 Document 註解拿到具體數據模型會進入到哪個索引哪個類型的信息,將該信息放在抽象類的實例域中,爲後面模版中具體方法的實現做信息的準備。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了上述對於數據模型、通用參數、配置信息等元數據的準備,我們在模版定義時具體方法的實現就變得簡單多了。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"通用參數檢查"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我們看下抽象類 create 方法中通用參數檢查實現,代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"public ServiceResponse create(DOC doc) {\n \/\/ 通用參數檢查。\n String idValue = this.getIDValue(doc);\n if (StringUtils.isNonEmpty(idValue)) {\n return ServiceResponse.createFailResponse(INVALID_DOCUMENT_ID);\n } \n}\n\/**\n * 獲取數據模型唯一標識的字段值。\n *\/\nprivate String getIDValue(DOC document) {\n try {\n return (String) this.idField.get(document);\n } catch (Exception e) {\n log.error(GET_DOCUMENT_ID_VALUE_ERROR_MSG, JSON.toJSONString(document), e);\n }\n return null;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述代碼,我們只是給出了 create 方法實現中關於通用參數檢查的部分,強調一下,這裏僅僅以 id 字段爲例,一般情況下通用字段根據具體領域模型具體定義,但實現思路上都大同小異。getIdValue 方法的實現中,我們明顯可以發現,前期元數據準備的作用在這裏體現出來了。通過 id 字段的元數據和具體的數據模型參數的實例引用,我們輕易地拿到了數據模型對象中 id 字段的實際參數值。對於反射的異常處理,我們簡單地記錄下日誌,將異常吸收,同時方法返回 null,具體生產環境的實現大傢俱體分析,這裏不是我們講解的重點,就不再贅述了。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"創建索引請求"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們基於已經準備好的索引和類型信息,在上述模版方法實現的基礎上創建索引請求,代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"public ServiceResponse create(DOC doc) {\n \/\/ 通用參數檢查。\n String idValue = this.getIDValue(doc);\n if (StringUtils.isNonEmpty(idValue)) {\n return ServiceResponse.createFailResponse(INVALID_DOCUMENT_ID);\n } \n\n \/\/ 創建索引請求。\n \/\/ 這裏數據進入哪個索引, 我們已經在初始化的時候通過註解讓用戶傳進來。\n IndexRequest indexRequest = new IndexRequest(this.index, this.type, idValue);\n String documentData = JSON.toJSONString(doc);\n indexRequest.source(documentData, XContentType.JSON);\n indexRequest.opType(DocWriteRequest.OpType.INDEX)\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需要強調的一點是,上述創建索引請求時,採用了固定的配置,例如 XContentType.JSON,DocWriteRequest.OpType.INDEX 等,也就是將這些都實現成了不變的部分。其實,這裏我們根據 Elasticsearch 客戶端 API 的定義可以發現,創建索引請求時還是有很多可以配置的參數,所以具體的實現中,我們依舊可以採用註解的思路將需要指定配置的參數項定義到註解中,提供用戶定製請求配置的入口。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"發送請求和異常處理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述通用參數檢查、創建索引請求的過程中,相信大家都發現了一點,我們在模版定義的方法實現中使用到了模版的抽象類在初始化時準備好的元數據。這裏準備的元數據解決了我們前面提到的變化部分前兩個邏輯的抽象定義,但是並沒有解決最後一個邏輯的抽象定義,也就是並沒有爲發送請求和異常處理時獲取 Elasticsearch 集羣客戶端的邏輯準備相關的元數據,爲什麼呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們看一下獲取 Elasticsearch 集羣客戶端的邏輯是什麼樣的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,Elasticsearch 集羣客戶端是一個 Java 實例對象,該對象管理着和一個指定 Elastcisearch 集羣交互的相關信息,例如集羣節點的 IP 地址、端口號、連接池大小以及各種超時時間等。考慮到對象配置的複雜和足夠的靈活性以及不是每個數據模型都會對應着一個不同的集羣的原因,我們再次採用上述數據模型中定義註解和抽象類實例化時準備元數據的方法實現獲取客戶端集羣的邏輯,就顯得不夠用了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,我們怎麼在模版定義的抽象類中留出用戶定製集羣客戶端的入口呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相信大家應該瞭解設計模式中的模版方法模式,這裏我們就採用模版方法的模式實現上述創建索引方法的發送請求和異常處理邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"kotlin"},"content":[{"type":"text","text":"public ServiceResponse save(DOC doc) {\n \/\/ 通用參數檢查。\n String idValue = this.getIDValue(doc);\n if (StringUtils.isNonEmpty(idValue)) {\n return ServiceResponse.createFailResponse(INVALID_DOCUMENT_ID);\n } \n\n \/\/ 創建索引請求。\n \/\/ 這裏數據進入哪個索引, 我們已經在初始化的時候通過註解讓用戶傳進來。\n IndexRequest indexRequest = new IndexRequest(this.index, this.type, idValue);\n String documentData = JSON.toJSONString(doc);\n indexRequest.source(documentData, XContentType.JSON);\n indexRequest.opType(DocWriteRequest.OpType.INDEX)\n\n try {\n \/\/ 這裏我們在真正調用 Elasticsearch 客戶端的 API 時,通過模板模式定義客戶端 \n \/\/ 的獲取方法。這樣,用戶可以在子類中定製自己的實現。\n this.getClient().index(indexRequest, RequestOptions.DEFAULT);\n } catch (Exception e) {\n log.error(SAVE_DOCUMENT_ERROR_MSG, JSON.toJSONString(doc), e);\n return ServiceResponse.FAIL_RESPONSE;\n }\n return ServiceResponse.SUCCESS_RESPONSE;\n}\n\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到這裏,我們終於完成了模版定義中創建索引方法的實現。代碼中的 getClient 方法的實現最終會返回一個 Elasticsearch 集羣客戶端的實例,由該客戶端實例真正執行索引請求的發送,同時做了一個通用的異常處理。具體返回一個怎麼樣的集羣客戶端呢?我們來看下面的代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"kotlin"},"content":[{"type":"text","text":"\/**\n * 獲取 ES 客戶端。\n * 這裏我們提供了一個默認的客戶端實現。\n *\/\nRestHighLevelClient getClient() {\n if (null != this.client) {\n return this.client;\n }\n \/\/ 優先子類實現自己的客戶端。\n \/\/ 模板模式中定義的客戶端獲取方法。\n RestHighLevelClient client = this.doGetClient();\n do {\n if (null == client) {\n break;\n }\n synchronized (this.clientLock) {\n if (null == this.client) {\n this.client = client;\n }\n }\n return this.client;\n } while (false);\n \/\/ 系統默認配置的客戶端。\n client = InitESRestClient.getClient();\n if (null == client) {\n throw new RestClientNotFoundException();\n }\n synchronized (this.clientLock) {\n if (null == this.client) {\n this.client = client;\n }\n }\n return this.client;\n}\n\/**\n * 執行獲取 ES 客戶端,子類可定製實現。\n *\/\nprotected RestHighLevelClient doGetClient() {\n return null;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼中包括兩個方法,我們先看下 getClient 方法的實現邏輯。因爲這裏我們需要一個數據模型只會與一個集羣綁定,所以我們對 Elasticsearch 集羣客戶端的獲取做了一個單例的處理,當然這不是我們的重點。我們看到方法在獲取客戶端時會先調用一下 doGetClient 方法獲取一個集羣客戶端,當該方法返回的客戶端不爲空時,我們按照單例的處理邏輯將當前數據模型的集羣客戶端保存下來;當該方法返回的客戶爲空時,我們依舊按照單例的處理邏輯獲取到一個默認的集羣客戶端並保存下來;如果默認客戶端也沒有獲取到,我們就拋出 RestClientNotFoundException 異常要求用戶去定義客戶端。從 getClient 方法的分析中,我們很輕鬆地便會發現 doGetClient 方法的巧妙之處,所以接下來我們看下 doGetClient 方法的實現。相信大家立刻會發現 doGetClient 方法實現的怪異之處,首先這是一個定義爲 protected 的方法,意味着任何子類都可以覆蓋它,同時該方法直接返回了 null,意味着如果子類沒有覆蓋該方法,或者覆蓋了該方法但依舊返回了 null,那麼上述的 getClient 方法就會返回一個默認的集羣客戶端。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是模版方法模式的巧妙之處,我們把邏輯比較複雜,可能需要用戶定製的邏輯通過模版方法的入口留給用戶自己去定製,從而完成模版定義中變化部分信息的抽象定義。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到此爲止,我們就完成了模版式冗餘代碼的抽取過程,最終我們定義了一個只存有一份冗餘代碼中邏輯的模版。只不過我們的模版目前只是一個簡單的模版,僅僅實現了創建索引的方法邏輯。但是從中我們可以發現,要想將模版冗餘代碼抽取到模版中,我們必須解決冗餘代碼中變化部分信息的抽象表達問題,在不同的語言中,這種抽象表達的方式會不同。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 Java 語言中,我們主要通過兩種方式來解決上述抽象表達的問題。一個是基於泛型參數,自定義註解以及反射機制來準備元數據的方式,二是通過模版方法模式將變化部分的邏輯留給用戶自己去定製。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"模板實例化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義完了模版,我們就完成了模版式冗餘代碼中通用代碼邏輯抽象封裝,那麼我們在實際應用時如何將具體的業務模型嵌入到模版中呢?也就是說我們如何完成上述模版的實例化呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們還是用前面提到的簡單用戶信息和訂單信息的數據模型來說明。請注意這裏說的不是 Java 對象的實例化,而是我們通用概念下的模版實例化。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"用戶信息的模板實例化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當我們定義了用戶信息的數據模型時,按照以上定義模版的實現,我們需要在數據模型的唯一標識字段中添加 DocID 註解來標記該字段是唯一標識,要求該字段的值是非空字符串類型。同時我們需要在數據模型類上添加 Document 註解來表示該數據模型要接受 Elasticsearch 的管理,其數據要進入\"user_info_index\"索引的\"user_info_type\"類型。代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * Elasticsearch 用戶信息數據模型。\n *\/\n@Document(index = \"user_info_index\", type = \"user_info_type\")\npublic class UserInfo {\n @DocumentID\n private String id; \/\/ ID\n private String nickName; \/\/ 暱稱\n private Integer age; \/\/ 年齡\n private String introduction; \/\/ 簡介\n private String signature; \/\/ 簽名\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了用戶信息數據模型的定義,我們如何享受到上述模版定義中創建索引的方法呢?而不用自己手動再實現一遍用戶信息創建索引的方法邏輯。顯而易見,我們定義一個用戶信息的 Elasticsearch 服務,集成上述模版定義的抽象類,同時完成泛型參數的實例化即可,具體的代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class UserInfoESService extends AbstractBaseESServiceImpl {}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然這裏用戶信息在接受 Elasticsearch 管理時,數據是進入到項目中默認的 Elasticsearch 集羣的,因爲我們並沒有覆蓋模版類中的 doGetClient 方法的實現。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"訂單信息的模板實例化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同理,訂單信息數據模型的定義如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * Elasticsearch 訂單信息數據模型。\n *\/\n@Document(index = \"order_info_index\", type = \"order_info_type\")\npublic class OrderInfo {\n private String id;\n private String productId; \/\/ 產品 ID\n private Integer productNum; \/\/ 產品數據量\n private Integer status; \/\/ 訂單狀態\n private String remark; \/\/ 備註\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"訂單信息 Elasticsearch 相關服務的實現我們依舊可以繼承上述模版定義的抽象類。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是這裏我們假設訂單信息的數據會進入項目中的另一集羣,那麼自然而然地,對於訂單信息 Elasticsearch 相關服務的實現就需要我們自己去定製集羣訪問的客戶端了,代碼如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class OrderInfoESService extends AbstractBaseESServiceImpl {\n @Override\n protected RestHighLevelClient doGetClient() {\n return InitESRestClientAnother.getClient();\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼中 InitESRestClientAnother 即完成了另一個客戶端獲取的邏輯,大家只需要明白這裏是另一個集羣客戶端即可。我們關注的重點是有了入口可以定製我們的業務數據要進入哪個集羣,而不用寫過多的冗餘代碼。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"擴展模版"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在定義模版和模版實例化的過程中,我們始終圍繞着創建索引的方法來說明如何解決模版式的冗餘代碼。但其實在真實的項目中,還存在着增刪改查等其他的通用功能,這些通用的功能我們都可以在定義模版的時候實現它們。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而要實現上述更多的通用功能所採用的思路無非和我們講述創建索引的方法大同小異,都是抽取出通用功能不變的部分和變化的部分。對於不變的部分我們直接使用,對於變化的部分我們採用語言提供的相關元數據機制或良好的設計模式進行抽象表達,然後在實現具體的業務邏輯時進行模版實例化即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,爲了讓大家更深刻地體會上述解決模版式冗餘代碼的思路,我們用 Java 語言中的接口來定義一下模版中通用的增刪改查的功能,然後提供一個相對完整的抽象類進行模版功能的實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"考慮到文章內容長度有限,我這裏暫且先給出模版功能的接口定義,如果大家感興趣的話,也可以採用上述的思路自己動手實現一下下面模版功能的接口:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"typescript"},"content":[{"type":"text","text":"\/**\n * Elasticsearch 的基礎服務接口定義。\n *\/\npublic interface IBaseESService {\n \/**\n * 保存文檔,文檔基於 ID 不存在,則創建,已存在,則覆蓋。\n *\/\n ServiceResponse create(DOC doc);\n \/**\n * 獲取指定 ID 的文檔。\n *\/\n ServiceResponse get(String id);\n \/**\n * 分頁查詢 ID 列表。\n *\/\n ServiceResponse> queryIDs(PageRequest pageRequest, BoolQueryBuilder queryBuilder, List sorts);\n \/**\n * 刪除指定 ID 的文檔。\n *\/\n ServiceResponse delete(String id);\n \/**\n * 更新文檔,僅更新文檔中存在的字段,文檔的 ID 字段必須存在。\n *\/\n ServiceResponse update(DOC doc);\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到此爲止,我們採用 Java 語言,藉助 Elasticsearch 在項目使用中的創建索引場景,完整講述瞭如何快速解決模版式的冗餘代碼問題。下面我做一下簡單的總結:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在解決模版式的冗餘代碼問題時,我的思路是抽取出冗餘代碼中不變的部分和變化的部分定義一個模版。對於不變的部分,直接在模版中使用,對於變化的部分,一般都會用到語言的元編程功能以及一些良好的設計模式,提供對數據和操作的統一抽象和擴展。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"在 Java 中提供的元編程功能主要是圍繞着以反射爲核心,註解,泛型,字節碼注入等技術而提供的。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"採用模版方法模式爲定義模版中變化的集羣客戶端留出了定製的擴展點。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,我簡單畫一下解決問題前後代碼結構的對比圖,方便大家清晰地理解文章中的思路。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/wechat\/images\/05\/0596c152144eb37eeae9ee3028ef687e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者介紹:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"李立坤,搜狐高級研發工程師,從 0 到 1 參與了搜狐內部玄武內容監控平臺的架構設計與研發工作。參與過教育產品、電商系統、分銷系統、團購系統、分佈式任務調度系統的架構與研發。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章