Context-aware Reader

Bạn có thể tìm thấy toàn bộ mã nguồn tại đâyarrow-up-right

Chương này trình bày cách phát triển theo phương pháp TDD một io.Reader nhận biết ngữ cảnh (context aware), dựa trên bài viết của Mat Ryer và David Hernandez trên The Pace Dev Blogarrow-up-right.

Context aware reader là gì?

Trước hết, hãy cùng tóm lược nhanh về io.Reader.

Nếu bạn đã đọc các chương khác trong cuốn sách này, bạn sẽ bắt gặp io.Reader khi chúng ta mở tệp, mã hóa JSON và nhiều tác vụ phổ biến khác. Đó là một sự trừu tượng đơn giản cho việc đọc dữ liệu từ một thứ gì đó.

type Reader interface {
	Read(p []byte) (n int, err error)
}

Bằng cách sử dụng io.Reader, bạn có thể tận dụng khả năng tái sử dụng rất lớn từ thư viện chuẩn, vì đây là một sự trừu tượng được sử dụng cực kỳ phổ biến (cùng với đối tác của nó là io.Writer).

Nhận biết ngữ cảnh (Context aware)?

Trong chương trước, chúng ta đã thảo luận về cách sử dụng context để cung cấp khả năng hủy bỏ (cancellation). Điều này đặc biệt hữu ích nếu bạn đang thực hiện các tác vụ có thể tốn kém tài nguyên tính toán và bạn muốn có thể dừng chúng lại.

Khi sử dụng một io.Reader, bạn không có sự đảm bảo nào về tốc độ, nó có thể mất 1 nano giây hoặc hàng trăm giờ. Bạn có thể thấy việc có thể hủy các loại tác vụ này trong ứng dụng của mình là hữu ích và đó là điều Mat và David đã viết.

Họ đã kết hợp hai sự trừu tượng đơn giản (context.Contextio.Reader) để giải quyết vấn đề này.

Hãy thử áp dụng TDD cho một số chức năng để chúng ta có thể bao bọc một io.Reader sao cho nó có thể bị hủy bỏ.

Việc kiểm thử điều này đặt ra một thách thức thú vị. Thông thường khi sử dụng một io.Reader, bạn thường cung cấp nó cho một hàm khác và bạn không thực sự bận tâm đến các chi tiết; chẳng hạn như json.NewDecoder hoặc io.ReadAll.

Những gì chúng ta muốn chứng minh là thứ gì đó tương tự như:

Cho một io.Reader với nội dung "ABCDEF", khi tôi gửi tín hiệu hủy bỏ ở giữa quá trình đọc, thì khi tôi cố gắng tiếp tục đọc, tôi sẽ không nhận được thêm gì nữa, vì vậy tất cả những gì tôi nhận được chỉ là "ABC".

Hãy xem lại interface một lần nữa.

Phương thức Read của Reader sẽ đọc nội dung nó có vào một []byte mà chúng ta cung cấp.

Thay vì đọc mọi thứ, chúng ta có thể:

  • Cung cấp một mảng byte có kích thước cố định không chứa hết toàn bộ nội dung.

  • Gửi một tín hiệu hủy bỏ.

  • Thử đọc lại và việc này sẽ trả về một lỗi với 0 byte được đọc.

Hiện tại, hãy viết một bài kiểm thử "happy path" (trường hợp thuận lợi) nơi không có sự hủy bỏ, chỉ để chúng ta làm quen với vấn đề mà chưa cần viết bất kỳ mã nguồn thực tế nào.

  • Tạo một io.Reader từ một chuỗi với một số dữ liệu.

  • Một mảng byte để đọc vào, có kích thước nhỏ hơn nội dung của reader.

  • Gọi read, kiểm tra nội dung, lặp lại.

Từ ví dụ này, chúng ta có thể hình dung việc gửi một loại tín hiệu hủy bỏ nào đó trước lần đọc thứ hai để thay đổi hành vi.

Bây giờ chúng ta đã thấy nó hoạt động như thế nào, chúng ta sẽ áp dụng TDD cho các chức năng còn lại.

Viết bài kiểm thử trước

Chúng ta muốn có thể kết hợp một io.Reader với một context.Context.

Với TDD, tốt nhất là bắt đầu bằng việc hình dung API mong muốn và viết một bài kiểm thử cho nó.

Từ đó, hãy để trình biên dịch và kết quả kiểm thử thất bại dẫn dắt chúng ta đến một giải pháp.

Thử chạy bài kiểm thử

Viết lượng mã tối thiểu để bài kiểm thử có thể chạy và kiểm tra kết quả lỗi

Chúng ta sẽ cần định nghĩa hàm này và nó nên trả về một io.Reader.

Nếu bạn thử chạy nó:

Đúng như dự đoán.

Viết đủ mã để bài kiểm thử vượt qua

Hiện tại, chúng ta sẽ chỉ trả về io.Reader mà chúng ta truyền vào.

Bài kiểm thử bây giờ sẽ vượt qua.

Tôi biết, tôi biết, điều này có vẻ kỳ quặc và máy móc, nhưng trước khi bắt tay vào phần việc phức tạp, điều quan trọng là chúng ta phải có một số sự xác nhận rằng chúng ta không làm hỏng hành vi "thông thường" của một io.Reader và bài kiểm thử này sẽ mang lại cho chúng ta sự tự tin khi tiến xa hơn.

Viết bài kiểm thử trước

Tiếp theo, chúng ta cần thử thực hiện việc hủy bỏ.

Chúng ta ít nhiều có thể sao chép bài kiểm thử đầu tiên nhưng bây giờ chúng ta đang làm thêm các việc:

  • Tạo một context.Context với khả năng hủy bỏ để chúng ta có thể gọi cancel sau lần đọc đầu tiên.

  • Để mã của chúng ta hoạt động, chúng ta cần truyền ctx vào hàm của mình.

  • Sau đó, chúng ta khẳng định (assert) rằng sau khi gọi cancel, không có gì được đọc thêm.

Thử chạy bài kiểm thử

Viết lượng mã tối thiểu để bài kiểm thử có thể chạy và kiểm tra kết quả lỗi

Trình biên dịch đang cho chúng ta biết phải làm gì; hãy cập nhật chữ ký hàm (signature) của chúng ta để chấp nhận một context.

(Bạn cũng sẽ cần cập nhật bài kiểm thử đầu tiên để truyền vào context.Background.)

Bây giờ bạn sẽ thấy kết quả bài kiểm thử thất bại rất rõ ràng:

Viết đủ mã để bài kiểm thử vượt qua

Tại thời điểm này, chúng ta sẽ sao chép từ bài viết gốc của Mat và David nhưng chúng ta vẫn sẽ thực hiện chậm rãi và theo từng bước lặp.

Chúng ta biết mình cần có một kiểu dữ liệu đóng gói io.Reader mà chúng ta đọc từ đó và context.Context, vì vậy hãy tạo kiểu đó và thử trả về nó từ hàm của chúng ta thay vì trả về io.Reader gốc.

Như tôi đã nhấn mạnh nhiều lần trong cuốn sách này, hãy đi chậm và để trình biên dịch giúp bạn.

Sự trừu tượng hóa có vẻ đúng, nhưng nó chưa triển khai interface mà chúng ta cần (io.Reader), vì vậy hãy thêm phương thức đó vào.

Chạy các bài kiểm thử và chúng sẽ được biên dịch thành công nhưng sẽ gây ra lỗi panic. Đây vẫn là một bước tiến.

Hãy làm cho bài kiểm thử đầu tiên vượt qua bằng cách chỉ cần ủy nhiệm (delegate) lệnh gọi cho io.Reader bên dưới.

Tại thời điểm này, bài kiểm thử happy path của chúng ta đã vượt qua trở lại và có cảm giác như chúng ta đã trừu tượng hóa mọi thứ một cách ổn thỏa.

Để làm cho bài kiểm thử thứ hai vượt qua, chúng ta cần kiểm tra context.Context để xem nó đã bị hủy bỏ hay chưa.

Bây giờ tất cả các bài kiểm thử sẽ vượt qua. Bạn sẽ nhận thấy cách chúng ta trả về lỗi từ context.Context. Điều này cho phép những người gọi mã kiểm tra các lý do khác nhau dẫn đến việc hủy bỏ đã xảy ra và điều này được đề cập kỹ hơn trong bài viết gốc.

Tổng kết

Trong kỹ thuật phần mềm, mẫu ủy nhiệm là một mẫu thiết kế hướng đối tượng cho phép kết hợp đối tượng để đạt được khả năng tái sử dụng mã tương tự như kế thừa.

  • Một cách dễ dàng để bắt đầu loại công việc này là bao bọc đối tượng ủy nhiệm của bạn và viết một bài kiểm thử khẳng định rằng nó hoạt động giống như cách đối tượng ủy nhiệm thường làm trước khi bạn bắt đầu kết hợp các phần khác để thay đổi hành vi. Điều này sẽ giúp bạn duy trì mọi thứ hoạt động chính xác khi bạn viết mã hướng tới mục tiêu của mình.

Last updated