Tư duy C# – chương 30

30. Giao diện

Trở về Mục lục cuốn sách

30.1. Bức tranh toàn diện hơn

Cho đến lúc này, kiểu dữ liệu đã quy định trạng thái và ứng xử của nó, cũng như cách mà ta sử dụng nó. Bởi vì tess là con rùa kiểu Turtle, ta có thể gọi các phương thức rẽ Turn và đi tiến Forward, hay là nhận thuộc tính hướng quay Heading. Vì sân chơi playground là kiểu nền Canvas, ta có thể đặt nét bút vẽ nền của nó hoặc nhận kích thước của nó.

_images/interfaces.png

Thuật ngữ giao diện được dùng theo hai nghĩa trong lĩnh vực lập trình. Nghĩa đen là “cách mà ta tương tác với một đối tượng”. Điều này tách biệt với “cơ chế hoạt động của nó”, hay là cách thực hiện. Trong các lớp đã lập, ta đã đặt một số thành viên có tính công khai (đó chính là giao diện với thế giới bên goài) cũng như vài thành viên có tính riêng tư (đó là cách ta chọn để thực hiện giao diện).

_images/implementations.png

Khi người chủ nhiệm nói “Giao diện với lớp ngẫu nhiên Random là gì?”, có lẽ họ đang yêu cầu ta mô tả những phương thức và thành viên công khai của lớp đó.

Trong C# (cũng như một số ngôn ngữ khác, đặc biệt là Java) từ này còn có một nghĩa hẳn hoi khác. Giao diện là một cấu trúc ngôn ngữ trong đó chứa một số dấu ấn phương thức (và có thể còn những thứ khác) nhằm định nghĩa một hợp đồng/contract.

Sở dĩ nảy sinh tư duy hướng đối tượng là bởi mọi người nhìn thế giới xung quanh ta, nhận thấy các thực thể đối tượng, nhóm chúng lại thành những kiểu đối tượng; và nhận ra rằng chúng có những thuộc tính, hay trạng thái nhất định; cũng những chúng có khả năng ứng xử khác nhau. Bởi vậy họ nghĩ rằng đó cũng là cách hợp lý để tổ chức chương trình phần mềm!

Song ngoài đời thực, không chỉ mỗi “kiểu dữ liệu” quy định cách ta dùng đồ vật ra sao. Đối tượng có thể  một kiểu này, nhưng lại có những “vai trò” khác nhau. Một người có thể có những vai trò như “cha mẹ”, hay “người đánh cờ” hoặc “sinh viên”.

Hãy cùng nhìn vào một số thiết bị ta dùng hằng ngày: máy ảnh kĩ thuật số, điện thoại thông minh, nhà cung cung cấp dịch vụ Internet, thẻ nhớ USB, máy chơi nhạc cầm tay, TV, máy tính bảng, đồng hồ báo thức, và đài phát thanh.

Tất cả chúng đều là những thứ khác nhau, song một số trong đó lại có thể thực hiện những vai trò mà thứ khác không thể. Những thiết bị nào có thể giữ vai trò “thiết bị nhớ để lưu trữ file”? Thiết bị nào có thể giữ vai trò “máy phát nhạc”? Thứ nào đóng vai trò “máy phát video”? Thứ nào có thể đóng vai trò “camera” chụp hình? Thứ nào có thể đóng vai trò “máy đọc email”, “máy tính tay”, “đồng hồ báo thức” hay “thiết bị thu sóng radio”?

Như vậy trong lập trình, ngoài việc định nghĩa kiểu dữ liệu của một đối tượng (hay đối tượng đó  gì), ta cũng cần thêm cơ chế để mô tả và gắn những vai trò phụ thêm, hay những cách khác để sử dụng chúng nữa.

Đây chính là điều mà giao diện lập trình theo nghĩa chính thống: nó mô tả một vai trò mà đối tượng có thể đảm nhiệm khi tương tác cùng các đối tượng khác.

Khi ta cắm điện thoại vào cổng USB trên máy tính rồi copy một số file vào bộ nhớ điện thoại, thì máy tính không cần biết kiểu thiết bị vừa cắm vào là gì — dù nó là điện thoại, ổ cứng di động, camera, hay thẻ nhớ USB. Tất cả những gì nó cần biết, đó là vật đó đảm nhiệm được vai trò “thiết bị nhớ để lưu trữ file”.

Hoá ra là ta đã thấy điều này nhiều lần trong sách, nhưng chỉ mới chưa hoàn toàn nhận diện được nó xảy ra ở chỗ nào thôi.

Hãy cùng nghĩ về vòng lặp foreach. Ta dùng nó với chuỗi, với mảng, với danh sách, với các khoá từ điển, v.v. Trong C#, một lớp cần phải cung cấp phương thức có tên GetEnumerator. Phương thức này trả lại đối tượng với các phương thức cho phép foreach mỗi lượt nhận được từng giá trị. Vậy nên nếu ta nhìn vào trợ giúp IntelliSense để tìm chuỗi, mảng, danh sách, hay từ điển, ta sẽ luôn tìm thấy một phương thức có tên GetEnumerator.

_images/getenumerator.png

Đây chính là phép màu nhiệm để cho foreach duyệt được nhiều kiểu đối tượng khác nhau: chúng đều đóng vai trò cụ thể mà foreach đã yêu cầu, dù rằng những đối tượng này về bản chất là khác nhau hoàn toàn.

Để biết thêm thông tin về giao diện, hãy xem trang web của Dave

http://www.hitthebits.com/2012/11/what-are-interfaces.html

30.2. Trở lại mã lệnh cấp thấp

Để học cách định nghĩa và tự thực hiện giao diện thì vượt quá phạm vi cuốn sách này. Nhưng ta sẽ xem xét một hoặc hai giao diện đã được định nghĩa trong nền tảng .Net, và xem vài cách mà ta có thể tận dụng chúng.

Giao diện thực chất là một “hợp đồng” giữa một lớp và “người tiêu dùng” muốn sử dụng lớp đó. Trong C#, hợp đồng này cụ thể hoá rằng muốn hoàn thành hợp đồng thì cần phải thực hiện những thành viên nào.

30.2.1. Ví dụ 1

Ở chương viết về thuật toán mảng và danh sách, ta đã sắp xếp mảng chuỗi bằng Array.Sort. Ta cũng đã dùng phương thức này trong thuật toán xếp quân hậu, trong đó ta đã trộn những quân cờ bằng cách dùng số nguyên ngẫu nhiên để làm khoá phục vụ sắp xếp._images/comparable.png

Hãy nhìn kĩ vào mã lệnh này ta đã tạo nên 4 mảng, trong đó 3 đã được sắp xếp ổn thoả, nhưng ta lại bị lỗi thực thi ở mảng các hình chữ nhật Rectangle. Nó đã thất bại khi phải so sánh hai phần tử mảng. Tại sao?

Sau đây là lời gợi ý mà IntelliSense đưa ra:_images/arraysort.png

Điều mà thông điệp này muốn cho ta biết, đó là nó không câu nệ về kiểu đối tượng trong mảng, song bất kì dùng kiểu gì, đối tượng phải có một giao diện IComparable. Chính hợp đồng IComparable này đã quy định rằng phải có một phương thức mang tên CompareTo (hãy nhớ lại rằng ta đã dùng phương thức này khi phải so sánh hai chuỗi với nhau).

Như vậy số nguyên, chuỗi, kí tự, ngày giờ DateTime, tất cả đều có giao diện IComparable, (và do đó theo hợp đồng, chúng phải có một phương thức CompareTo). Nếu ta biết cách so sánh chúng thì ta sẽ biết cách sắp xếp chúng!

Nhưng ta không có cách nào so sánh được các đối tượng Rectangle. Đối tượng Rectangle không thực hiện giao diện IComparable.

Khi ta tự định nghĩa các lớp riêng, ta cần phải tự hỏi xem liệu việc có so sánh những đối tượng thuộc kiểu này có hợp logic không. Chẳng hạn, sẽ là hợp lý nếu ta cho các đối tượng sinh viên Student sắp xếp theo mã số sinh viên. Trong trường hợp đó, ta có thể quyết định rằng lớp ta viết sẽ phải thực hiện IComparable, và ta sẽ bị yêu cầu phải viết nên phương thức CompareTo trong lớp mới, nếu muốn thực hiện đúng hợp đồng này.

30.2.2. Ví dụ 2

Sau đây là một hợp đồng những gì mà đối tượng cần phải làm được nếu muốn hoàn thành vai trò của IDictionary. (Trong C# một quy ước là luôn luôn đặt tên hợp đồng giao diện với một chữ I đứng đầu trước tên gọi.)

 public interface IDictionary<TKey, TValue> :
                   ICollection<KeyValuePair<TKey, TValue>>,
                   IEnumerable<KeyValuePair<TKey, TValue>>,
                   IEnumerable
 {
     ICollection<TValue> Values { get; }
     TValue this[TKey key] { get; set; }
     void Add(TKey key, TValue value);
     bool Remove(TKey key);
     bool TryGetValue(TKey key, out TValue value);
 }

Lưu ý từ khoá interface viết trên dòng 1.

Trong hợp đồng, tất cả đều là những định nghĩa thành viên và dấu ấn kiểu. Bởi vậy, hợp đồng sẽ phải chỉ định “những gì” là sẵn có, chứ không phải một lớp chọn cách thực hiện “như thế nào”.

Các dòng 2-4 nói rằng để đóng vai trò của IDictionary, ta cũng sẽ phải đóng vai trò của cả ICollection lẫn IEnumerable. Những hợp đồng đó cần ít phương thức và thành viên hơn. Và quan trọng hơn là, nó cho thấy hợp đồng giao diện có thể phụ thuộc vào, hoặc thừa kế từ, những yêu cầu hợp đồng có ở các giao diện khác.

Bây giờ khi chúng ta (hoặc nhân viên Microsoft) viết một lớp mới, ta có thể cung cấp phần “cam kết” của hợp đồng. Ta có thể nói “Chúng tôi đang định nghĩa một lớp con rùa Turtle mới, và muốn cho các đối tượng con rùa cũng có thể đóng vai trò một từ điển, như đã chỉ định bởi giao diện từ điển IDictionary”. Như vậy là lớp thực hiện giao diện.

Bây giờ phép màu sẽ xảy đến. Nếu ta có một đối tượng con rùa, và nếu con rùa có thể đóng vai trò như những từ điển (dù vậy trong sách này con rùa không làm như vậy được), thì mã lệnh kiểu như sau sẽ hoạt động:

Turtle tess = new Turtle(...);

IDictionary<string, int> tdict = tess;
tdict["distance covered"] = 25;
tdict["year of manufacture"] = 2013;

foreach (string k in td.Keys)  ...

Dòng 3 là nơi có phép màu. Nó bảo rằng “tôi không quan tâm về việc tương tác với tess như một đối tượng chú rùa, mà chỉ quan tâm tương tác với đối tượng trong vai trò từ điển IDictionary của nó”. Bởi vậy, ta có thể định nghĩa biến tdict để cho kiểu của nó là kiểu giao diện. Bây giờ ta có thể gán bất kì đối tượng nào cho biến tdict, miễn là nó biết cách hoàn thành vai trò mà giao diện đòi hỏi.

30.3. Ba dạng đa hình

Hãy nhớ lại rằng đa hình nghĩa là “có thể làm việc với nhiều kiểu dữ liệu”.

Ta đã dùng đa hình dựa trên kiểu con: một phương thức yêu cầu một tham số Turtle thì cũng có thể làm việc được với một kiểu con của Turtle, chẳng hạn TurtleGTX mà ta lập nên. Đây là điều mà phép kế thừa đã cho ta.

Kiểu thứ hai mà ta đã thấy, đó là đa hình tham số: ta dùng tham số kiểu trong List<T> hay in Dictionary<K,V>. Một tên gọi khác để chỉ đa hình tham số là kiểu chung (generic).

Ở chương này, giờ đây ta đã thấy đa hình dựa trên giao diện (interface-based polymorphism). Ở ví dụ mã lệnh trên, ta đã có thể coi tess là một từ điển. Như vậy, tất cả những phương thức từ điển đều có thể làm việc được với nhiều kiểu dữ liệu khác nhau.

30.4. Type testing and casting

Sometimes we might need to “undo” the polymorphism. We have an object that we know is some type of Turtle, but we’d really like to a) know if it really a more special kind of TurtleGTX, and b) if it is, take advantage of its extra capabilities.

So let’s go back to our DrawSquare method from the Inheritance chapter. We saw there that it works with any turtle. Now we’ll add this new requirement: if the turtle has extra capabilities, (i.e. it is really a TurtleGTX), then get it to spin on each corner as it is draws the square.

 private void drawSquare(Turtle t, double sz)
 {
     for (int side = 0; side < 4; side++)
     {
         t.Forward(sz);
         t.Right(90);
         if (t is TurtleGTX)
         {
           TurtleGTX tgtx = (TurtleGTX) t;
           tgtx.Spin();
         }
     }
 }

Dòng 7 là một phép thử kiểu. Nó cho phép ta hỏi xem liệu con rùa t có phải thực sự là một đối tượng TurtleGTX được kế thừa (với thêm tính năng) không. Dòng 9 định nghĩa một biến mới có thể tham chiếu tới một con rùa TurtleGTX. Tên kiểu đặt trong cặp ngoặc ở vế phải được gọi là một đổi kiểu. Nó cho phép ta đối xử con rùa như thể rùa loại TurtleGTX. Một khi đã tham chiếu tới con rùa “xịn” rồi, ta có thể gọi phương thức Spin() của nó.

Bạn sẽ nhận được một biệt lệ nếu cố thử chuyển đổi một con rùa thường thành rùa TurtleGTX. Đơn giản là nó không có khả năng làm vậy! Đó là lí do tại sao đầu tiên ta phải thử ở dòng 7 để đảm bảo rằng phép chuyển đổi có hiệu lực. (Bạn có thể bọc phép chuyển đổi kiểu vào một khối try…catch, nhưng nói chung sẽ phạm phải cách viết mã lệnh không tốt nếu bạn cứ thường xuyên dự trù việc tung ra biệt lệ.)

Cơ chế đã thấy ở trên đều có hiệu lực trong C#, Java, cũng như một số ngôn ngữ khác. Song C# còn có một cơ chế khác vốn trở thành một cách viết thông dụng:

 private void drawSquare(Turtle t, double sz)
 {
     for (int side = 0; side < 4; side++)
     {
         t.Forward(sz);
         t.Right(90);

         TurtleGTX tgtx = t as TurtleGTX;
         if (tgtx != null) {
             tgtx.Spin();
         }
     }
 }

Từ khoá as thực hiện một phép đổi kiểu “an toàn”, và trả lại null nếu phép đổi không thực hiện được.

Kiểm tra kiểu và đổi kiểu cũng hiệu lực nếu như kiểu được kiểm tra và đổi là một kiểu giao diện.

30.5. Trở lại quan điểm tầm cao

Bây giờ hãy cùng trở lại ví dụ cuối cùng ở chương trước. Những cách thực hiện đặc hiệu khác nhau đối với (DictionarySortedDictionary), tất cả cả đều thực hiện giao diện IDictionary. Bởi vậy trong ví dụ đó, ta đặt kiểu phương thức trả lại là IDictionary, và thay đổi mã lệnh gọi để hoạt động được với vai trò này thay vì với kiểu của đối tượng.

Đây là một kĩ thuật rất có hiệu lực. Vai trò thì trừu tượng hơn là kiểu dữ liệu. Bằng cách chuyển dịch tư duy của ta từ “Đây là kiểu gì” thành “Mỗi đối tượng có thể đóng vai trò gì”, mà ta tạo được cơ hội cho điện thoại có thể chụp ảnh, hoặc đóng vai trò máy chơi nhạc.

Và khi đặt thêm một “hợp đồng” minh bạch riêng biệt giữa các thành phần — một mặt cam kết hoàn thành hợp đồng, mặt khác chỉ dùng những đối tượng như đã cam kết — ta sẽ tăng thêm độ tin cậy cho hệ thống lập nên.

Bây giờ đã dễ hơn nhiều để ta quay trở lại ví dụ cuối chương trước và chọn một số cách thực hiện từ điển khác nhau trong phương thức letterFreqs. Miễn là lựa chọn mới này thực hiện IDictionary, thì ta sẽ không cần thay đổi gì nữa.

Giao diện đã nêu cao cách tiếp cận cắm-là-chạy đối với mô-đun phần mềm, cũng như giao diện USB cho phép một kiểu cắm-là-chạy để kết nối các thiết bị với nhau. Miễn là giao diện được hỗ trợ thì ta không cần phải lo lắng gì về kiểu đối tượng cả.

30.6. Thuật ngữ

implement an interface (thực hiện giao diện) Một lớp thoả thuận với hợp đồng giao diện thì đồng ý cung cấp những tính năng nhất định. Nó đảm nhiệm việc cung cấp thuộc tính và phương thức (và thậm chí cả những kiểu thành viên khác nữa) nhằm hoàn thành trách nhiệm đã nhận.

giao diện (interface, cách nói thuận miệng) Cách mà một thứ tương tác với những thành phần khác ngoài nó. Cần số trong xe hơi là giao diện tới hộp số. Trong lập trình, ta dùng từ này để chỉ nôm na “các thành viên công khai của một lớp”.

giao diện (interface, thuật ngữ trong lập trình) Chỉ rõ những phương thức và thành viên nào mà lớp thực hiện sẽ cam kết với mã lệnh ứng dụng về tính năng. Một lớp có thể thực hiện nhiều giao diện khác nhau.

vai trò (role) Thứ mà đối tượng có thể làm được, chứ không phải đối tượng bản chất là gì.

đổi kiểu (type cast) Một phép chuyển từ kiểu dữ liệu này sang kiểu dữ liệu khác.

kiểm tra kiểu (type test) Phép kiểm tra cho phép ta xác định xem liệu một đối tượng có thể quy đổi thành một kiểu cụ thể hay không.

30.7. Bài tập

  1. Ta muốn một giao diện có tên IMusicPlayer. Một đối tượng nên có những phương thức và thành viên nào để cho những đối tượng khác có thể tương tác với nó trong vai trò một máy phát nhạc?
  2. Một số kiểu dữ liệu là ICloneable (“sao chép được”). Hãy rà soát giao diện của nó và xem nó cam kết những thành viên nào. Kể tên ít nhất hai kiểu có tính sao chép được và hai kiểu không sao chép được. Liệu các mảng có sao chép được không? Danh sách có sao chép được không? Thế còn ngày giờ DateTime và từ điển thì sao?
  3. Việc sê-ri hoá (Serialization) một đối tượng nghĩa là “quy đổi nó về dạng chữ”. Chẳng hạn, mã chữ XAML là cách biểu diễn sê-ri hoá từ cửa sổ giao diện GUI đã thiết kế. Nhiều kiểu đối tượng có thể sê-ri hoá được, bởi vậy mà, như ta trù định, có một giao diện mang tên ISerializable. Hãy tra cứu trợ giúp của ISerializable rồi xác định xem một lớp cần cung cấp những phần tử nào để có thể hoàn thành vai trò này.

1 bình luận

Filed under C#, Lập trình, Tin học

1 responses to “Tư duy C# – chương 30

  1. Pingback: Tư duy sắc bén bằng C# | Blog của Chiến

Bình luận về bài viết này