Generics

Trong video này, chúng ta sẽ xem xét về Generics trong C#. Tôi biết Generics là một trong những chủ đề mà có lẽ không nhiều người quan tâm đến cú pháp, mà là khi nào và tại sao, cách nhận diện tình huống khi nào thực sự áp dụng khái niệm này và bạn muốn sử dụng cú pháp đặc biệt này trong C# cho tính năng này mà bạn không hiểu cách để làm vậy. Vậy hãy cùng nhau xem xét một số vấn đề mà chúng ta đang cố gắng tránh, tại sao Generics lại xuất hiện và sau đó chúng ta sẽ xem xét cú pháp và qua một số ví dụ để thực sự hiểu ý tưởng này. Tôi sẽ nói thẳng ra rằng Generics nhằm cho phép bạn sử dụng các kiểu dữ liệu như các giá trị. Đúng vậy, như trong lập trình hàm, bạn có các hàm như các giá trị, reflection cho phép bạn sử dụng mã của mình như một giá trị, cũng như Expression API và các giá trị thông thường như các giá trị. Khái niệm thực sự là kiểu dữ liệu như một giá trị và chúng ta sẽ thấy điều đó trong giây lát.

Nhưng trước tiên, hãy xem từ đâu chúng ta bắt đầu, cách cũ là gì và cách mới là gì. Vậy hãy làm gì đó với một object, một object chỉ đại diện cho bất cứ thứ gì. Hãy tiến hành dump nó và nhân tiện nếu bạn thắc mắc ứng dụng tôi đang sử dụng là gì, nó được gọi là LINQPad. Bất kỳ object nào cũng có thể là bất cứ thứ gì, chúng ta không biết nó là gì. Hãy tiến hành dump nó, và dump là cụ thể của LINQPad, nhân tiện nó chỉ sẽ hiển thị mọi thứ lên màn hình của tôi ở đây. Chúng ta sẽ tiến hành tạo một số nguyên 5 và một chuỗi "Hello World". Chúng ta viết nó và không có vấn đề gì, chúng ta có thể tiếp tục cho đến khi sử dụng các kiểu ẩn danh, các object ẩn danh, dễ dàng. Vậy vấn đề là gì? Điểm yếu đầu tiên là hiệu suất. Tại sao lại là hiệu suất? Vì cho đến khi ứng dụng thực sự chạy, chúng ta không biết gì sẽ được đặt vào đây bởi vì chúng ta đang sử dụng object. Object thực sự chỉ là như vậy, bạn chỉ có thể dùng dấu chấm, bạn không biết kiểu dữ liệu sẽ được đặt vào đó, không có kiểm tra kiểu ở đây nên chúng ta không thể ngăn bạn mắc lỗi và chúng ta không thể thực hiện các tối ưu hóa tại thời gian biên dịch. Điều này cũng có thể thấy 

bằng cách có box này trong IL Instruction ở đây, vốn sẽ lấy số 5 của bạn, đưa nó vào object, đặt nó trên heap và sau đó kiểu tham chiếu này sẽ tham chiếu đến nó. Vậy nên đây là lý do tại sao bạn gặp suy giảm hiệu suất nếu bạn phải đóng gói và nói chung, hai điểm khác là trong khu vực mà quan trọng hơn đối với phần lớn các lập trình viên là khả năng đọc mã và khả năng bảo trì. Làm thế nào điều này là vấn đề chính xác trong phạm vi của đoạn script nhỏ này của LINQPad? Rất dễ hiểu những gì đang xảy ra, tôi chỉ đơn giản đã viết nó và giải thích. Một khi bạn có các ứng dụng khổng lồ, bạn sẽ có lớp này giữa biển các lớp khác, bất cứ nơi nào object này được gọi, bạn sẽ cần biết về tất cả các kiểu dữ liệu có sẵn trong ứng dụng của bạn để hiểu liệu bạn có làm hỏng hàm này hay không. Nếu nó không xử lý mọi kiểu dữ liệu có sẵn, hàm đó không an toàn và việc xử lý mọi kiểu dữ liệu là không hợp lý. Để hiểu liệu bạn có làm hỏng nó hay không, bạn sẽ phải xem xét mọi nơi hàm này được gọi trong mã của bạn, ví dụ như 10 nơi bạn sẽ phải xem các tham số được cung cấp ở mỗi nơi, và khi bạn đi qua nơi thứ năm, bạn đã quên nơi thứ nhất và đó chỉ là thực tế của nó. Vậy nên nó trở nên khó bảo trì vì hàm này có thể ở tất cả các trạng thái có thể này. Điều này chỉ là nhìn từ góc độ bên trong hàm và các thứ đang gọi nó từ bên ngoài.

Bây giờ hãy nhìn ngược lại, từ bên ngoài bạn sẽ muốn gọi hàm này, làm thế nào bạn biết nó sẽ xử lý kiểu dữ liệu bạn sẽ nhận được? Bạn không biết, bạn sẽ phải kiểm tra ngay tại đó. Không có ràng buộc, không có an toàn kiểu dữ liệu, không có kiểm tra tại thời gian biên dịch để ngăn bạn làm điều gì đó như chuyển đổi một chuỗi thành một số nguyên, điều đó chỉ gây ra ngoại lệ tại thời gian chạy. Vậy nên hai điều chính là tối ưu hóa và khả năng bảo trì, và dưới khả năng bảo trì là khả năng đọc hiểu mã của bạn.

Vậy hãy xem xét cú pháp và cách chúng ta có thể giải quyết điều này với Generics. Generics có cú pháp rất đơn giản, bạn có thể sử dụng dấu ngoặc nhọn để bắt đầu xác định hoặc đặt tên cho kiểu dữ liệu, giống như việc đặt tên cho một biến. Vậy biến này được đặt tên là T, bất kỳ kiểu dữ liệu nào chúng ta cung cấp vào đây sẽ được sử dụng như một giá trị, chúng ta cung cấp tên cho nó. Bây giờ chúng ta có thể sử dụng kiểu dữ liệu này ở bất kỳ nơi nào trong hàm này, chúng ta có thể sử dụng nó như một tham số. Chúng ta chỉ định rằng tham số đầu vào phải có kiểu dữ liệu này, chúng ta có thể nói rằng kiểu trả về phải là kiểu dữ liệu này. Nếu chúng ta sau đó cố gắng trả về nó, ô không, điều này không thể đúng. Và tôi cũng có thể sử dụng nó bên trong hàm này, nhưng ở thời điểm này nó không khác nhiều so với object ngoại trừ việc chúng ta không thực sự đóng gói bất cứ thứ gì. Vậy chúng ta giống như lùi lại một bước và nhìn vào bên trong và bên ngoài, chúng ta hiểu rằng chúng ta có thể có kiểu dữ liệu này là T. Vậy điều thứ hai về Generics là chúng ta bây giờ có thể xác định rằng T là gì và chúng ta có thể sử dụng nó trong phạm vi của hàm này, chúng ta có thể nói chúng ta muốn giới hạn nó như thế nào, ví dụ như chúng ta muốn giới hạn nó chỉ là struct hoặc class, struct là kiểu giá trị và class là kiểu tham chiếu. Vậy làm thế nào để chúng ta chỉ định một ràng buộc? Ràng buộc là gì? Chúng ta nói 'where' rồi sau đó bạn lấy định danh kiểu dữ liệu của bạn và nói nó kế thừa từ cái gì đó. Trong trường hợp của chúng ta, chúng ta nói là nó là một struct. Chúng ta có thể chỉ định một số từ khóa đặc biệt khác, nhưng ví dụ nếu chúng ta nói nó là một struct thì nó là một kiểu giá trị, vậy chúng ta có thể truyền vào nó các giá trị như số 5 và true, và nếu chúng ta nói rằng nó là một class, bạn có thể thấy các kiểu khác được phép và các kiểu không được phép sẽ được làm nổi bật rằng chúng không được phép. Vậy nên đây là cách bạn có thể quyết định gì bạn muốn sử dụng ở đây và nói chung bạn có thể... Hãy lấy ví dụ này trong .NET 6 và tôi chỉ sẽ điều hướng đến nó và cách bạn muốn quyết định các kiểu dữ liệu bạn muốn đặt ở đây là bạn thường sẽ xem xét một struct như integer16 hoặc integer32, kiểu này gọi là short, kiểu này là int và kiểu này là long, và tiếp tục. Integer32, bạn có thể thấy nó triển khai các interface như IComparable, IConvertible, IFormattable, IComparable, IEquatable, vậy đây là nơi bạn có thể bắt đầu so sánh các thứ với các kiểu cụ thể nữa. Nếu bạn chỉ định những điều này, nhưng tôi sẽ không đi sâu vào đó, chúng ta sẽ xem xét những thứ khác, chỉ cần hiểu rằng nếu bạn muốn có một ý tưởng hoặc thực hành điều gì đó, hãy xem xét một số struct, xem các interface mà chúng triển khai và cố gắng ràng buộc bằng các interface đó và xem những chức năng chung nào bạn có thể trích xuất từ đó vì hiện tại chúng ta chỉ đang nhìn vào bề mặt của lớp này.


Và thực sự, hãy xem xét điều gì đó khác, giả sử chúng ta có một lớp nhưng chúng ta cũng muốn tạo một kiểu mới của lớp đó. Các object ẩn danh, kiểu dữ liệu ẩn danh không có constructor, đúng không? Chúng được định nghĩa tại thời gian biên dịch và chúng chỉ tồn tại như các chuỗi, không thực sự. Hãy tiến hành tạo lớp của riêng chúng ta, chúng ta sẽ nói là A và đó sẽ chỉ là như vậy, và nếu chúng ta nói new A ở đây thì điều này sẽ ổn, đúng không? Và hãy tiến hành làm điều gì đó như public static int i = 0 và mỗi khi chúng ta tạo điều này, chúng ta sẽ tiến hành tăng nó lên một chút. Vậy nên hãy định dạng lại một chút và ở đây tôi sẽ tạo nó, tôi sẽ tạo object đầu tiên, khi chúng ta dump nó ở đây, hãy tiến hành xuất 1, và sau đó tôi sẽ tiến hành tạo new T, đúng vậy, đây là điều mà new cho phép bạn làm và giả sử rằng chúng ta đang trả về cùng một kiểu dữ liệu ở đây là class và new, vì vậy nếu chúng ta không có new ở đây thì chúng ta sẽ không thể gọi new trên kiểu dữ liệu này và đôi khi chỉ vì bạn chỉ định kiểu A không có nghĩa là bạn sẽ có new và rằng bạn sẽ có thể tạo kiểu mới đó. Vì vậy, điều này đôi khi rất hữu ích để chỉ gọi new trên kiểu dữ liệu này và hãy tiến hành xem nó, tôi không chắc menu đó là gì nhưng, tôi đoán chúng ta muốn... Hãy tiến hành dump nó ở đây, đúng vậy, 0, 1, có thể là như vậy, dấu ngoặc nhọn, đúng vậy, 1 và 2 mỗi lần chúng ta tạo A thì số sẽ tăng lên một chút. Vậy nên những thứ khác bạn có thể làm với kiểu dữ liệu cụ thể là bạn có thể thử sử dụng Activator và ý tôi là phần này vốn dĩ là về cú pháp nhưng tôi đang cố gắng làm chồng cú pháp với những gì bạn có thể tiềm năng làm với nó. Nếu bạn không có new, bạn vẫn có thể tạo kiểu dữ liệu bằng cách sử dụng Activator, tôi nghĩ rằng new chỉ có hiệu suất cao hơn một chút, tôi không hoàn toàn chắc, vì vậy đừng tin tôi về điều đó. Nhưng dù sao thì đây là các hàm generic, bạn chỉ định tham số generic, bạn cũng có thể chỉ định thêm một tham số và kiểu trả về này thực sự là những gì bạn muốn và đây là những gì bạn sẽ tạo ra. Bạn thấy việc kiểm tra kiểu dữ liệu tại thời gian biên dịch đang giúp tôi ở đây, và giả sử rằng tôi sẽ cung cấp một số nguyên và một số nguyên, và ý tôi là điều đó sẽ dừng tôi. Vậy hãy tiến hành split A ở đây và vào cuối nó, nếu chúng ta biết nó là một integer, tôi đoán nó cũng sẽ là 0, đúng vậy. Nhưng điều đó sẽ cho chúng ta là kiểu int32, tôi nghĩ ở phía dưới, vì vậy các tham số kiểu dữ liệu nhiều hơn, bạn cũng có thể như phỏng vấn chúng, vậy giả sử rằng A của chúng ta là generic và đây là nơi chúng ta sẽ mong đợi rằng A sẽ là một kiểu R, T sẽ là kiểu R, vậy trừ khi chúng ta tiến hành chỉ định nó là kiểu int, nó sẽ không cho phép chúng ta. Vậy đây là những gì chúng ta phải vượt qua đôi khi, điều này sẽ khá tiện khi nhận diện kiểu dữ liệu cho bạn đôi khi thì không, nhưng bất kỳ ràng buộc nào bạn đặt ở đây trong where clause và các tham số kiểu dữ liệu mà bạn chỉ định cho các hàm đều phải tuân theo các quy tắc này và chỉ cần hiểu rằng phạm vi của lớp là mọi thứ, vậy nên nếu bạn sẽ lấy kiểu dữ liệu này và đặt nó vào đây, bạn sẽ thấy rằng nó giống như kiểu T đang ghi đè nó, vậy nên hãy tiến hành chỉ loại bỏ T ở đây, bạn có thể thấy rằng bạn không thực sự cần phải chỉ định phần này nữa bởi vì T đến từ lớp, bạn thực sự có thể có một hàm generic trên một lớp generic và nếu điều này trông giống như nó là static, bạn có thể tiến hành nói là int và sau đó gọi Do, bởi vì đây là static, nếu bạn chỉ gọi Do thì nó sẽ không hoạt động, đây là nơi chúng ta phải chỉ định cả hai cái này để điều này hoạt động, thêm một tham số ở đó, dump. Vậy nên hy vọng rằng điều này phần lớn có ý nghĩa về cú pháp, bạn cung cấp một ký hiệu đại diện cho một kiểu dữ liệu mà bạn sẽ truyền vào đây và sau đó bạn cơ bản thực thi rằng cái gì đó phải là kiểu dữ liệu đó nhưng bằng cách sử dụng nó ở đây và ý tôi là bạn có thể truyền nó vào các hàm generic khác để củng cố ý tưởng về kiểu dữ liệu như một giá trị, tôi sẽ tạo một chương trình nhỏ nơi tôi sẽ mô phỏng việc chúng ta cho động vật ăn. Động vật sẽ được đặt ở đâu đó và chúng sẽ ăn một loại thức ăn nhất định. Tôi sẽ tiến hành cho chúng ăn. Đơn giản thật, có thể đơn giản hơn thế nào? Chúng ta sẽ có một loại động vật nào đó, có thể là một interface thực sự, tôi không quan tâm lắm. Động vật sẽ ăn một loại thức ăn nhất định và sẽ ở một vị trí nhất định. Vậy hãy tiến hành tạo động vật đầu tiên, chúng ta sẽ có một con vịt, đúng rồi, và một con vịt sẽ triển khai interface Animal. Vậy chúng ta sẽ cần một loại thức ăn, vậy vịt ăn gì? Một con vịt sẽ phải ăn thứ gì đó như bánh mì, đúng vậy, vì vậy có thể là hạt giống, tôi không biết điều gì là tốt hơn cho một con vịt, nhưng đó chắc chắn là thứ chúng ta có thể ăn. Vậy nên là bánh mì và sau đó vị trí, vị trí nơi vịt thường ở là tại hồ, đúng vậy, hãy tiến hành đặt một cái hồ ở đây. Bây giờ, bạn có thể thấy rằng có một chút vấn đề ở đây, không có gì ngăn chúng ta cố gắng cho bánh hồ cho một con vịt, vì vậy chúng ta phải thực hiện một chút kiểm tra kiểu dữ liệu vì từ ý tưởng về những gì Generics đại diện, chúng ta đang cố gắng thông báo hoặc thực thi một cấu trúc, thực sự là về việc có các thực thể và quy trình. Vậy nên, bạn sẽ có một tập hợp các thực thể mà bạn đã ràng buộc bởi where clause của bạn bằng các ràng buộc của các kiểu dữ liệu generic, vậy nên bạn chỉ cho phép một nhóm các kiểu dữ liệu nhất định vào quy trình của bạn. Trong trường hợp của chúng ta, quy trình là cho động vật ăn, những thứ thực sự đi vào là như động vật, bánh mì, hồ và những thứ tương tự, đúng vậy. Vậy nên, hãy tiến hành thực hiện một chút kiểm tra hoặc ràng buộc, lại là interface này có thể là Food và điều này sẽ kế thừa từ Food, đúng vậy, đây là nơi chúng ta thực hiện việc ràng buộc nói rằng trong interface Animal chúng ta nói rằng F kế thừa từ Food, đúng vậy. Và nếu chúng ta sau đó cố gắng cho bánh hồ vào nơi thức ăn được định nghĩa, chúng ta sẽ gặp lỗi ở đó. Vì vậy, mặc dù chúng ta đã ràng buộc điều này, chúng ta thực sự có một cái gì đó, chúng ta sẽ cho chúng ta biết rằng chúng ta thực sự đang có kiểu dữ liệu nào. Vậy nên, chúng ta sẽ phải bảo đảm rằng F là một loại Food. Vậy nên, chúng ta đang tiến hành chỉ định rằng động vật sẽ ăn một loại thức ăn là F, và nơi động vật sẽ ở là L, và L cũng phải là một loại Location. Nếu chúng ta tiến hành tạo một cái hồ, thì nó sẽ được chấp nhận, nhưng nếu chúng ta cố gắng đặt nó ở một cái khác, nó sẽ không được chấp nhận.


Vậy nên, chúng ta sẽ tiến hành tiếp tục xây dựng ứng dụng này. Chúng ta có một động vật ăn hạt giống và ngồi tại hồ, hãy tiến hành cho nó ăn. Tôi sẽ đặt một phương thức mặc định trên interface, không cần thiết, và tôi có thể chỉ triển khai nó như vậy. Vậy nên, chúng ta sẽ nói rằng động vật này đang ăn gì đó, và ở đâu nó đang ăn. Vậy nên, nếu chúng ta muốn truyền một chuỗi như 'seeds' cho nó ăn và đặt nó ở 'pond', chúng ta có thể tiến hành chạy hàm này và thấy rằng nó đang ăn hạt giống tại hồ. Tuy nhiên, chúng ta đã quên gọi hàm arrive, vì vậy hãy tiến hành gọi hàm arrive và thấy rằng nó đang ăn hạt giống tại hồ, hoặc chúng ta đang cho vịt ăn hạt giống, đúng vậy.


Điều này có thể trông xấu và chúng ta có thể tối ưu hóa nó một chút. Hãy tạo một class Factory công khai, và trong Factory này, chúng ta sẽ nói rằng tạo một động vật, F và L sẽ là động vật và địa điểm. Đây sẽ chấp nhận một động vật F và địa điểm L, và sau đó tạo ra một instance mới của động vật đó với các tham số đã cho. Nếu chúng ta không định nghĩa những ràng buộc này, chúng ta sẽ không thể tạo ra được động vật mới từ kiểu dữ liệu generic này. Điều này có thể đôi khi rất hữu ích để chỉ gọi new trên kiểu dữ liệu này và làm cho nó dễ dàng hơn một chút. Vậy nên, điểm là bạn muốn ẩn Generics đi đôi khi, nhưng cơ chế bên trong vẫn thực hiện tất cả các kiểm tra kiểu dữ liệu và bạn có được kiểm tra tại thời gian biên dịch.


Vậy nên, để tóm tắt nhanh về ứng dụng sử dụng kiểu dữ liệu như một giá trị, đúng vậy, đó là tất cả. Tôi biết đây không phải là rất thực tế, các cách tiếp cận thực tế hơn là có các container injection phụ thuộc, có danh sách các container chung, wrappers, có các lớp với cấu hình như Mediator, v.v... Có rất nhiều ví dụ sử dụng chúng và nói chung, nếu bạn có thể nhận diện một nhóm các thực thể phải đi qua một quy trình, bạn sẽ có thể sử dụng Generics ở đó. Vậy hãy tiến hành xem một vài ví dụ nơi tôi sẽ sử dụng Node Generics, tôi sẽ chuyển sang Generics và sau đó chúng ta sẽ xem xét một mô phỏng Mediator. Trong ví dụ này, nhanh chóng chúng ta có một loại nội dung như một bài đăng hoặc bình luận trên Facebook và chúng ta muốn hash. Vậy chúng ta muốn hash bình luận đó để đảm bảo rằng bình luận mà chúng ta nhận được là duy nhất, vì vậy nếu bạn phân phối bình luận spam, nếu bạn spam cùng một bình luận ở nhiều nơi, chúng ta sẽ không thể hiểu rằng nó là bình luận duy nhất trên bài đăng cụ thể đó mà không hash ID của bài đăng, nếu không chúng ta sẽ phát hiện nó như là một bản sao. Vậy nên, chúng ta có một hasher mà cụ thể phụ thuộc vào các kiểu dữ liệu cụ thể, không có Generics ở đây, tốt nhất là chúng ta nhận nội dung và sau đó kiểm tra loại nội dung đó là gì và sau đó chúng ta phải trả về một hash cụ thể.


Ở đây bạn có thể hiểu hoặc nhận diện một nơi mà bạn cần sử dụng Generics. Chúng ta có nội dung, chúng ta có post và comment, cả hai kế thừa từ content. Sau đó, content hashing strategy hash với auth text và author, và comment hashing strategy chỉ hash với author và post ID. Vậy nên chúng ta tạo ra một hashing strategy cụ thể cho mỗi nội dung. Vậy điều gì xảy ra khi chúng ta thêm một loại nội dung nữa? Chúng ta phải sửa ở một nơi khác, và đây lại là vấn đề liên quan đến khả năng bảo trì, bạn sửa mã ở một nơi và sau đó phải sửa ở nơi khác, điều này không tốt. Bạn có thể phải tạo thêm một hashing strategy mới hoặc tái sử dụng hashing strategy thường đã tồn tại. Vậy nên hy vọng điều này đã cho bạn ý tưởng, nếu bạn phải sửa mã ở hai nơi khi bạn thực hiện một thay đổi, đó là một tín hiệu đỏ để suy nghĩ về việc chia sẻ quy trình.


Vậy hãy xem cách điều này được giải quyết với Generics. Cơ bản là tương tự về tính sử dụng, chúng ta vẫn truyền dữ liệu vào đây, nhưng bây giờ chúng ta có một interface hashing strategy, nơi chúng ta nói rằng nội dung, giữa content và implementation thực tế, chúng ta định nghĩa một lớp khác nói rằng tôi vẫn còn content nhưng đối với tôi bạn sẽ phải chỉ định hashing strategy. Bây giờ khi chúng ta định nghĩa post và comment, chúng ta đã biết cách chúng ta sẽ hash chúng, vậy nên nếu bạn nhìn vào post, bạn đã biết rằng sẽ có một loại hashing nào đó xảy ra với nó, chúng ta biết cách chúng ta sẽ hash nó. Và sau đó ở đây bạn sẽ thấy rằng chúng ta đang gặp một chút vấn đề như thế nào, chúng ta không thực sự biết kiểu dữ liệu nào mà chúng ta đang nhận, vì vậy chúng ta chỉ nhận content ở đây và điều đó ổn, chúng ta sẽ cần thực hiện một chút chuyển đổi kiểu dữ liệu, nhưng cuối cùng khi nó đến hasher, chúng ta chỉ sử dụng Activator để tạo ra kiểu hasher đã được chỉ định. Vậy bây giờ nếu chúng ta viết thêm một loại nội dung nữa, chúng ta không cần phải nhớ sửa lớp này nữa, vì với giải pháp này bạn sẽ ổn, chúng ta chỉ cần một điều nữa.


Được rồi, vậy điều không đẹp mắt về giải pháp này tôi đoán bạn có thể nói là phần này ở đây nơi chúng ta không thể nhận diện được nó, nhưng đây là nơi bạn bắt đầu sử dụng thêm Generics, nơi giữa hashing strategy và hashing strategy bạn đặt một interface khác với kiểu dữ liệu và nói rằng kiểu dữ liệu là một loại content nào đó và interface này bạn tiến hành chỉ định nó cho implementation lớp abstract này và nó được bảo vệ, vì vậy bạn vẫn phải ép kiểu, mặc dù bây giờ khi bạn triển khai, bạn nói loại nội dung nào bạn có thể hash với hashing strategy này và ở đây bạn không cần phải ép kiểu nữa, hasher vẫn trông giống như cũ và tính sử dụng vẫn trông giống như cũ. Vậy nên, đây thực sự là khái niệm bạn muốn cân bằng khả năng của mình, trải nghiệm người dùng hoặc trải nghiệm lập trình viên và vẫn giữ được khả năng bảo trì. Vậy nên, đó là ba thứ mà bạn sẽ cân bằng, vì khi bạn nhìn vào đây và bạn có Generics bay khắp nơi, đây là loại logic mà bạn không muốn viết quá nhiều, bạn không muốn đi vào đó và bạn muốn chỉ định một generic trên lớp của bạn một lần khi bạn viết điều gì đó và nó sẽ trông có thể giống như một cái gì đó như thế này. Nếu bạn phải viết điều này suốt, nó không phải là trải nghiệm tốt cho lập trình viên, đúng không? Và khi chúng ta nhìn vào ví dụ này, hãy tiến hành phân tích một chút. Đây là một mô phỏng Mediator, nếu bạn đã nghe về Mediator, ý tôi là đây là một phiên bản triển khai sơ khởi của nó. Chúng ta có request handler, các request đi vào, chúng ta cung cấp một loại request, nó sẽ gọi một handler cụ thể cho nó và ý tôi là đó là tất cả. Vậy nên chúng ta có một request nơi chúng ta chỉ định kiểu dữ liệu trả về ở đây, tôi tạo hai request: GetAge và GetName, một trả về một số nguyên, cái kia trả về một chuỗi. Chúng ta sau đó tạo một handler, nơi handler là một lớp abstract của interface IHandler, tôi làm điều này chỉ để có thể lưu trữ nó một cách gọn gàng hơn trong dictionary của mình ở đây. Chúng ta có type của handler trỏ tới instance của handler, tôi tiến hành lấy type của chúng ta, tôi lấy handler F của type Handler, vậy tôi ép kiểu nó thành kiểu này, kiểu này chấp nhận kiểu request này và sau đó tôi tiến hành gọi nó, nếu không thì trả về mặc định. Bạn có thể ném một ngoại lệ thực sự nhưng tôi trả về mặc định. Vậy nên đây là cách bạn cơ bản có được kiểu dữ liệu static và thực thi một quy trình, và nếu bạn có các lập trình viên mới gia nhập đội, đây chỉ là một ví dụ khác nơi họ phải viết điều này và bây giờ họ có một quy trình cấu trúc để viết mã của mình. Bạn hiểu doanh nghiệp của mình, bạn hiểu sự lặp lại cho mỗi thứ riêng lẻ mà bạn làm, bạn biết đó là một quy trình mà bạn có thể theo dõi. Vậy nên, những thứ như sản phẩm thay đổi, tôi không biết một số thực thể thay đổi nhưng chúng đều có thể theo quy trình này. Vậy nên, đây là Mediator thực sự generic, vậy bạn có thể thực sự thích ứng với bất kỳ mô hình kinh doanh nào bởi vì request và output giống nhau. Vậy nên Mediator thực sự là một cách cấu trúc mã rất gọn gàng, nhưng hy vọng bạn đã hiểu Generics một chút hơn bây giờ. Nếu bạn không biết gì về chúng, chỉ nhớ tóm tắt là các thực thể, quy trình, giới hạn các thực thể của bạn, đảm bảo chúng phù hợp với quy trình của bạn và thực sự là trải nghiệm người dùng. Bạn muốn viết những thứ này và cấu trúc của toàn bộ hệ thống của bạn trông như thế nào. Trong trường hợp của tôi ở đây, hạ tầng handler, request handler bạn viết nó một lần, đó là framework trong đó bạn đặt các thực thể thực sự sẽ đi qua quy trình. Vậy nên quy trình không thay đổi quá nhiều, đó là những thứ bạn sẽ phát triển, vậy nên đây là các đối tượng của bạn, chức năng và request trong trường hợp của chúng ta ở đây lại là chức năng của việc hash nội dung của bạn. Thực sự là quá trình áp dụng quy trình đó cho thực thể đó được tự động hóa và bạn không còn phải tiếp tục bảo trì theo cách không generic ở một bảng khác đâu, và thực sự nếu bạn có thứ gì đó như vậy, khả năng cao nó có thể được sử dụng ở ba nơi khác nhau, vậy bạn phải thực hiện một thay đổi và cập nhật các nơi khác để thay đổi đó hoạt động đúng cách.

Dù sao, đây sẽ là phần kết thúc, cảm ơn rất nhiều vì đã xem. Nếu bạn thích nó, hãy nhấn like, đăng ký kênh. Nếu bạn có thêm câu hỏi, hãy để lại chúng trong phần bình luận hoặc đến hỏi chúng trên server Discord của tôi. Tôi cũng livestream vào các ngày thứ Tư và Chủ nhật, hãy tham gia Twitch của tôi qua liên kết trong phần mô tả. Chúc bạn một ngày tốt lành, hy vọng sẽ gặp bạn trong các video khác của tôi."

Nhận xét

Bài đăng phổ biến từ blog này

Channels

Dependency Injection