0
0
Lập trình
Admin Team
Admin Teamtechmely

Thiết Kế Sạch, Khách Hàng Mạnh: SDK Java của Elasticsearch

Đăng vào 6 tháng trước

• 11 phút đọc

Chủ đề:

#design#api#java

Giới thiệu

Java có một hệ sinh thái API phong phú, nhưng không phải tất cả đều hiệu quả hoặc dễ học. Việc phát triển một API tốt không phải là điều đơn giản; việc thiết kế sai các yếu tố chính, định nghĩa các trừu tượng đơn giản và mô hình luồng đều là những vấn đề cần được giải quyết. SDK Java Elasticsearch là một dự án với nỗ lực thiết kế nhằm giải quyết những yếu tố này.

Trong bài viết này, tôi sẽ phân tích những ý tưởng thiết kế làm cho SDK này trở nên hấp dẫn và hiệu quả, đồng thời cũng chỉ ra một số nhược điểm không thể tránh khỏi mà nhà phát triển cần lưu ý.

Tạo mã và nguồn thông tin duy nhất

SDK Java của Elasticsearch không hoàn toàn được viết bằng tay; nó được sinh ra từ một đặc tả API chuẩn, phát triển bằng Typescript, nhưng cũng có một số phần được xây dựng thủ công. Quy trình tạo ra client sản xuất các lớp mô hình Java, builder, serializer và các phương thức không gian tên cấp cao từ đặc tả này. Cách tiếp cận này giải thích cho tính nhất quán về tên gọi, hình dạng và phạm vi bao phủ trên hàng trăm endpoint và nhiều client ngôn ngữ khác nhau. Các phần thủ công (do các kỹ sư của Elastic thực hiện) bao gồm:

  • Tích hợp transport với Low Level REST Client (LLRC)
  • Hạ tầng cốt lõi: xác thực, TLS, retry, thiết lập JSON mapper, kiểm thử tự động.
  • Tính thuận tiện của API: các mẫu builder, quy ước về nullability, đặt tên.
  • Quyết định dựa trên ADR: Các phần thủ công được hỗ trợ bởi các Ghi chép Quyết định Kiến trúc, ghi lại lý do tại sao một số lựa chọn thiết kế nhất định đã được thực hiện.

Mẫu Builder Được Thực Hiện (Hầu Như) Đúng

SDK Elasticsearch dựa nhiều vào Mẫu Builder: nó được thể hiện trong một giao diện cơ sở ObjectBuilder<T> với một phương thức duy nhất là build(). Bạn đang xây dựng các truy vấn tìm kiếm, ánh xạ chỉ mục, các thao tác bulk, v.v. Nếu không có builder, mọi thứ sẽ trở nên hỗn loạn.

SDK cung cấp các overload builder-lambda (ví dụ, () -> ObjectBuilder<T>) để các đối tượng lồng nhau có thể được xây dựng ngay trong các closures kiểu an toàn; những overload này có thể thấy trong Javadocs công khai dưới dạng các setter builder kiểu hàm. Ở bên trong, các builder được sinh ra kế thừa từ ObjectBuilderBase, đảm bảo việc sử dụng một lần thông qua _checkSingleUse() khi cần gọi phương thức build().

Khi bạn gọi build(), bạn đã hoàn tất. Việc tái sử dụng builder được coi là không an toàn vì các cấu trúc bên trong, đặc biệt là các collection, có thể được chia sẻ giữa builder và đối tượng đã xây dựng. Thay đổi một trong số đó có thể làm hỏng đối tượng còn lại mà không ai biết. Vì vậy, họ đóng cánh cửa sau khi xây dựng với một hợp đồng rõ ràng: cấu hình một lần, xây dựng và quên đi. Mỗi lớp yêu cầu và phản hồi đều tuân theo mẫu này.

java Copy
SearchRequest request = SearchRequest.of(s -> s
    .index("products")
    .query(q -> q
        .match(m -> m
            .field("name")
            .query("laptop")
        )
    )
);

Nhìn sơ qua, điều này có vẻ giống như một mớ hỗn độn dùng lambda. Nhưng trên thực tế, nó đang nối một tập hợp các cấu trúc DSL lồng nhau có kiểu. of(...) là một shortcut static khởi tạo builder, áp dụng một hàm cấu hình và hoàn tất quá trình xây dựng. Bạn đang viết mã Java khai báo mà phản ánh trực tiếp DSL truy vấn Elasticsearch.

Lambda Theo Kiểu DSL Thực Sự Hoạt Động

Những lambda này cung cấp một triển khai hiệu quả và thân thiện với IDE, đồng thời giúp việc tạo các cấu trúc DSL lồng nhau sâu trở nên dễ dàng hơn trong khi vẫn giữ kiểu an toàn. Java không phải là một ngôn ngữ hàm: các cách diễn đạt khác nhau được hỗ trợ, nhưng bản chất của nó là ngôn ngữ hướng đối tượng. SDK vẫn quản lý để tạo ra một cú pháp idiomatic bằng cách sử dụng lambda, điều này dẫn đến việc khai báo ý định của nhà phát triển một cách dễ dàng.

java Copy
client.search(s -> s
    .index("products")
    .query(q -> q
        .bool(b -> b
            .must(m -> m
                .match(t -> t
                    .field("description")
                    .query("wireless")
                )
            )
        )
    )
);

Mỗi lambda đại diện cho một bước cấu hình lồng nhau, với các closures kiểu an toàn. Bạn không truyền vào các chuỗi hay các bản đồ ma thuật. Bạn đang tạo thành một cây kiểu tĩnh. Đó là lợi thế. Nó tránh được một mẫu phản diện phổ biến: nối các phương thức .withX(), .setY() trên hàng tá đối tượng có thể thay đổi, với các giá trị null rình rập khắp nơi. Ở đây, mỗi cấp độ được giới hạn, tập trung và không thể thay đổi.

Các Liên Kết Được Đánh Dấu và Biến Thể Kiểu An Toàn

Hãy nói về tính đa hình. Các truy vấn Elasticsearch không phải là cấu trúc phẳng mà là các loại biến thể. Một Query có thể là một MatchQuery, BoolQuery, RangeQuery, v.v. SDK mô hình hóa điều này với một mẫu Liên Kết Được Đánh Dấu.

Một liên kết được đánh dấu là một cấu trúc dữ liệu có thể chứa các giá trị của các loại dữ liệu khác nhau, nhưng chỉ một tại một thời điểm. Nó giống như một liên kết thông thường, nhưng bao gồm một thẻ (hoặc bộ phân biệt) cho thấy loại dữ liệu nào đang được lưu trữ. Thẻ này cho phép truy cập an toàn theo kiểu vào giá trị đã lưu trữ, ngăn ngừa việc sử dụng sai dữ liệu một cách ngẫu nhiên.

Client triển khai một mẫu TaggedUnion tổng quát cho nhiều miền biến thể này (truy vấn, tổng hợp, bộ phân tích, v.v.). Một TaggedUnion phơi bày kind hiện tại và giá trị kiểu mạnh; các builder phơi bày các phương thức rõ ràng cho mỗi biến thể, giúp phát hiện và đúng đắn hơn. Mẫu này đánh đổi một lượng nhỏ gián tiếp để có sự đầy đủ được kiểm tra bởi trình biên dịch để cải thiện khả năng phát hiện trong IDE.

Mỗi liên kết như vậy triển khai:

java Copy
public interface TaggedUnion<Tag extends Enum<?>, BaseType> {
    Tag _kind();
    BaseType _get();
}

Bạn kiểm tra _kind() để xác định nó là gì, sau đó gọi _get() và ép kiểu một cách an toàn:

java Copy
Query query = Query.of(q -> q
    .match(m -> m.field("title").query("elasticsearch"))
);

if (query._kind() == Query.Kind.Match) {
    MatchQuery match = (MatchQuery) query._get();
    // Làm việc với match một cách an toàn
}

Thiết kế này cho phép suy luận với cấu trúc liên kết ngay cả khi không có hỗ trợ cú pháp của các phiên bản Java gần đây nhất, đồng thời duy trì khả năng tương thích ngược với những người đã sử dụng các sản phẩm Elastic trong các cài đặt trước đó. Bắt đầu từ Java 16+, chúng ta có một chút trải nghiệm về khớp mẫu cấu trúc có thể được sử dụng trong tương lai cho sự tiến hóa của SDK; ví dụ, một bước đầu tiên có thể là:

java Copy
switch (query._kind()) {
    case Match -> {
        MatchQuery match = (MatchQuery) query._get();
        // sử dụng match
    }
    case Term -> {
        TermQuery term = (TermQuery) query._get();
        // sử dụng term
    }
}

Thiết Kế Modular: Mẫu Client Không Gian Tên

API của Elasticsearch rất rộng lớn. Nó có tìm kiếm, quản lý chỉ mục, ánh xạ, pipeline nhập, bảo mật, tình trạng cụm và nhiều endpoint khác. Nhồi nhét tất cả vào một lớp sẽ là một cơn ác mộng.

Thay vào đó, SDK chia nhỏ mọi thứ theo miền:

java Copy
ElasticsearchClient client = new ElasticsearchClient(transport);

client.indices().create(c -> c.index("catalog"));

client.search(s -> s
    .index("products")
    .query(q -> q.match(m -> m.field("name").query("laptop")))
);

Mỗi nút sub-DSL (indices(), search(), v.v.) chỉ phơi bày các thao tác liên quan đến ngữ cảnh của nó. Điều này hỗ trợ hiệu quả cả cho IDE và trí não của chúng ta. Nó ánh xạ trực tiếp đến cấu trúc API REST (/_search, /_indices, v.v.), giúp dễ dàng suy luận về những gì đi đâu. Nó cũng làm cho SDK dễ bảo trì. Việc thêm các nhóm API mới trở nên đơn giản: không có các interface khổng lồ, phức tạp để phải tái cấu trúc.

Mục tiêu là một SDK mạng Java đầy đủ tính năng phải hỗ trợ:

  • cấu hình theo kết nối (xác thực, tiêu đề, thời gian chờ)
  • nhiều client đồng thời
  • khả năng kiểm thử tốt và DI
  • trải nghiệm nhà phát triển thuận tiện

Mẫu client có không gian tên là một sự đánh đổi hợp lý. Bạn phải đối mặt với một chút nghi thức và nhiều lớp nhỏ, nhưng hỗ trợ tính modular bên dưới trong khi cung cấp một đối tượng gốc có thể khám phá cho người dùng.

Tính Bất Biến và Trừu Tượng Transport

Các đối tượng yêu cầu không thay đổi. Các builder chỉ xây dựng một lần. Bạn truyền dữ liệu, không phải hành vi: SDK về cơ bản an toàn với luồng và có thể dự đoán.

Các vấn đề về transport được tách biệt khỏi các mô hình kiểu: theo mặc định, client Java ủy quyền xử lý giao thức cho một RestClientTransport, tự nó sử dụng một client HTTP mức thấp (ví dụ, client HTTP Apache) để quản lý kết nối, pooling, retry và phát hiện nút. Việc tách biệt này cho phép client Java tập trung vào việc mô hình hóa yêu cầu/đáp ứng theo kiểu và (de)serialization trong khi transport xử lý các vấn đề vận hành.
Transport là có thể cắm, cho phép cấu hình của client thích ứng với các stack HTTP khác nhau khi cần thiết.

java Copy
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);

Việc tách biệt các vấn đề này làm cho SDK dễ kiểm thử, mở rộng và gỡ lỗi hơn.

Liệu Nó Có Thể Tốt Hơn?

Sự hiện diện của nhiều yếu tố bất biến ảnh hưởng đến việc sử dụng bộ nhớ. Do đó, cần hiểu cách tránh xây dựng quá nhiều đối tượng khiến nhà phát triển phải xem xét các cấu trúc nâng cao của sản phẩm, chẳng hạn như gói, chia sẻ dữ liệu, sử dụng các thực thể tạm thời và/hoặc phân trang dữ liệu tự nó.

Một đánh đổi có thể đo lường của tính bất biến và các builder chỉ sử dụng là cấp phát đối tượng ngắn hạn. Trong hầu hết các ứng dụng, overhead này là không đáng kể; trong các vòng lặp rất nóng hoặc pipeline có lưu lượng cao, bạn nên benchmark và, nếu cần thiết, xây dựng các đoạn bất biến có thể tái sử dụng hoặc điều chỉnh chiến lược bulk/batching. Ngoài ra, hãy xem xét overhead của serialization: client cung cấp các hook để sử dụng các triển khai JsonpMapper khác nhau (ví dụ, các mappers dựa trên Jackson) nếu bạn cần phân tích tùy chỉnh hoặc gửi các payload đã được serialize trước.

Ngày nay, Java cung cấp nhiều cấu trúc hơn: records, khớp mẫu cấu trúc với các lớp sealed, và giải cấu trúc instanceof; điều này cũng có nghĩa là phải liên tục tái cấu trúc SDK để theo kịp các tính năng của Java theo thời gian.

SDK này đạt được một sự cân bằng giữa tính lâu dài và thân thiện với nhà phát triển mà không làm thay đổi kiến thức về các sản phẩm Elastic trước đó.

Kết luận

Bạn có thể học gì từ SDK này? Dưới đây là bảng tóm tắt những khái niệm chính có thể hữu ích cho dự án tiếp theo của bạn. Nếu bạn đang xây dựng một thư viện client - hoặc thậm chí chỉ là một API công khai - bạn có thể học hỏi từ một số ý tưởng từ SDK này. Không có phép thuật. Chỉ có thiết kế tốt... nhưng hãy cẩn thận: Bối cảnh là vua!

Mẫu Giải Quyết
Builder (Sử Dụng Một Lần) Xây dựng bất biến an toàn hơn, tránh trạng thái có thể thay đổi chia sẻ
Giao Diện Lưu Loát (Lambdas) Khai báo, lồng nhau, định nghĩa yêu cầu kiểu an toàn
Mẫu Liên Kết Được Đánh Dấu Mô hình hóa các loại biến thể của Elasticsearch một cách an toàn và rõ ràng
Mẫu Client Không Gian Tên Nhóm API logic phù hợp với cấu trúc REST
Trừu Tượng Transport Các lớp HTTP và serialization có thể hoán đổi
Suy Nghĩ Chức Năng Ít tác dụng phụ, ít biến tạm thời hơn, mã khai báo nhiều hơn
Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào