GT C Sharp cơ bản – Bài 11 : Chương 10: Giao diện
Chương 10: Giao diện
Tác giả: Sưu tầm
Khái quát
Giao diện liên quan mật thiết với các lớp trừu tượng; chúng giống như một lớp trừu tượng mà tất cả các thành phần là trừu tượng.
Một ví dụ đơn giản
Đoạn mã sau đây định nghĩa một giao diện IScalable và lớp TextObject, mà nó cài đặt một giao diện, có nghĩa là nó chứa các phiên bản của tất cả của hàm được định nghĩa trong giao diện.
public class DiagramObject {
public DiagramObject() {}
}
interface Iscalable {
void ScaleX(float factor);
void ScaleY(float factor);
}
// A diagram object that also implements IScalable
public class TextObject: DiagramObject, IScalable {
public TextObject(string text) {
this.text = text;
}
// implementing ISclalable.ScaleX()
public void ScaleX(float factor) {
// scale the object here.
}
// implementing ISclalable.ScaleY()
public void ScaleY(float factor) {
// scale the object here.
}
private string text;
}
class Test {
public static void
TextObject text = new TextObject(“Hello”);
IScalable scalable = (IScalable) text;
scalable.ScaleX(0.5F);
scalable.ScaleY(0.5F);
}
}
Đoạn mã này cài đặt một hệ thống cho việc vẽ các sơ đồ. Tất cả các đối tượng dẫn xuất từ DiagramObject, để chúng có thể cài đặt các hàm ảo chung (không được viết trong ví dụ này). Một số đối tượng có thể co giãn, và điều này được biểu thị bằng sự có mặt của một cài đặt giao diện IScalable.
Việc liệt kê tên giao diện với tên lớp cơ sở cho TextObject chỉ định rằng TextObject cài đặt một giao diện. Điều này có nghĩa là TextObject phải có các hàm tương ứng với mỗi hàm có trong giao diện. Các thành phần giao diện không có bổ từ truy cập; lớp cài đặt giao diện sẽ thiết đặt khả năng truy cập cho các thành phần giao diện.
Khi một đối tượng cài đặt một giao diện, một tham chiếu đến giao diện có thể thu được bằng cách chuyển đổi kiểu thành một giao diện. Sau đó, nó có thể được sử dụng để gọi các hàm trong giao diện.
Ví dụ này có thể làm việc với các phương thức trừu tượng, bằng cách chuyển các phương thức ScaleX() và ScaleY() vào DiagramObject và viết chúng thành các hàm ảo. Mục “Những nguyên tắc thiết kế” ở phần sau trong chương này sẽ thảo luận khi nào sử dụng một phương thức trừu tượng và khi nào sử dụng một giao diện.
Làm việc với giao diện
Tiêu biểu, mã không biết liệu một đối tượng có hỗ trợ một giao diện không, nên nó cần kiểm tra xem đối tượng có cài đặt giao diện trước khi thực hiện việc chuyển đổi kiểu.
using System;
interface IScalable {
void ScaleX(float factor);
void ScaleY(float factor);
}
public class DiagramObject {
public DiagramObject() {}
}
public class TextObject: DiagramObject, IScalable {
public TextObject(string text) {
this.text = text;
}
// implementing ISclalable.ScaleX()
public void ScaleX(float factor) {
Console.WriteLine(“ScaleX: {0} {1}”, text, factor);
// scale the object here.
}
// implementing ISclalable.ScaleY()
public void ScaleY(float factor) {
Console.WriteLine(“ScaleY: {0} {1}”, text, factor);
// scale the object here.
}
private string text;
}
class Test {
public static void
DiagramObject[] dArray = new DiagramObject[100];
dArray[0] = new DiagramObject();
dArray[1] = new TextObject(“Text Dude”);
dArray[2] = new TextObject(“Text Backup”);
// array gets initialized here, with classes that
// derive from DiagramObject. Some of them implement IScalable.
foreach (DiagramObject d in dArray) {
if (d is IScalable) {
IScalable scalable = (IScalable) d;
scalable.ScaleX(0.1F);
scalable.ScaleY(10.0F);
}
}
}
}
Trước khi việc chuyển đổi kiểu được thực hiện, một kiểu được kiểm tra để chắc chắn việc chuyển đổi kiểu sẽ thành công. Nếu thành công, đối tượng được chuyển đổi kiểu thành giao diện, và các chức năng có giãn được gọi.
Không may thay cách xây dựng này kiểm tra kiểu của đối tượng hai lần; một cho toán tử is, và một cho việc chuyển đổi kiểu. Điều này là lãng phí, vì việc chuyển đổi kiểu có thể không bao giờ bị lỗi.
Một cách giải quyết điều này là tổ chức lại mã với việc xử lý ngoại lệ, nhưng đó không phải là ý tưởng tốt, bởi vì nó sẽ làm cho mã phức tạp hơn, và việc xử lý ngoại lệ thông thường nên được dành cho các điều khiện đặc biệt. Cũng không rõ ràng là nó sẽ thành nhanh hơn không, vì việc xử lý ngoại lệ có một số overhead.
Toán tử as
C# cung cấp một toán tử đặc biệt cho tình huống này, toán tử as. Sử dụng toán tử as, vòng lặp trên có thể được viết lại như sau:
using System;
interface IScalable {
void ScaleX(float factor);
void ScaleY(float factor);
}
public class DiagramObject {
public DiagramObject() {}
}
public class TextObject: DiagramObject, IScalable {
public TextObject(string text) {
this.text = text;
}
// implementing ISclalable.ScaleX()
public void ScaleX(float factor) {
Console.WriteLine(“ScaleX: {0} {1}”, text, factor);
// scale the object here.
}
// implementing ISclalable.ScaleY()
public void ScaleY(float factor) {
Console.WriteLine(“ScaleY: {0} {1}”, text, factor);
// scale the object here.
}
private string text;
}
class Test {
public static void
DiagramObject[] dArray = new DiagramObject[100];
dArray[0] = new DiagramObject();
dArray[1] = new TextObject(“Text Dude”);
dArray[2] = new TextObject(“Text Backup”);
// array gets initialized here, with classes that
// derive from DiagramObject. Some of them implement IScalable.
foreach (DiagramObject d in dArray) {
IScalable scalable = d as IScalable;
if (scalable != null) {
scalable.ScaleX(0.1F);
scalable.ScaleY(10.0F);
}
}
}
}
Toán tử as kiểm tra kiểu của toán hạng bên trái, và nếu nó có thể được chuyển đổi tường minh thành toán hạng bên phải, kết quả của toán tử là một đối tượng chuyển đổi thành toán tử bên phải. Nếu một chuyển đổi bị lỗi, toán tử trả ra giá trị null.
Cả hai toán tử is và as cũng có thể được sử dụng với lớp.
Giao diện và sự thừa kế
Khi chuyển đổi một đối tượng thành một giao diện, một phần cấp thừa kế được tìm kiếm cho đến khi nó tìm được một lớp liệt kê giao diện trong danh sách cơ sở của nó. Việc có các hàm bên phải một mình là không đủ:
using System;
interface Ihelper {
void HelpMeNow();
}
public class Base: IHelper {
public void HelpMeNow() {
Console.WriteLine(“Base.HelpMeNow()”);
}
}
// Does not implement IHelper, though it has the right form.
public class Derived: Base {
public new void HelpMeNow() {
Console.WriteLine(“Derived.HelpMeNow()”);
}
}
class Test {
public static void
Derived der = new Derived();
der.HelpMeNow();
IHelper helper = (IHelper) der;
helper.HelpMeNow();
}
}
Đoạn mã này có kết quả xuất ra là:
Derived.HelpMeNow()
Base.HelpMeNow()
Nó không gọi phiên bản HelpMeNow() của Derived khi việc gọi thông qua một giao diện, dù là Derived có một hàm có dạng thức đúng, bởi vì Derived không cài đặt giao diện.
Những nguyên tắc thiết kế
Các giao diện và lớp trừu tượng đều có những hành vi tương tự và có thể được sử dụng trong những tình huống giống nhau. Tuy nhiên, bởi vì cách chúng làm việc, giao diện có ý nghĩa trong một số tình huống, và lớp trừu tượng trong một số khác. Ở đây có một vài nguyên tắc để xác định khả năng là một giao diện hay một lớp trừu tượng.
Điều đầu tiên để kiểm tra là liệu đối tượng có được biểu thị đúng mức bằng việc sử dụng quan hệ “là-một”. Ngược lại, là khả năng một đối tượng, và có phải một lớp dẫn xuất là thể hiện của đối tượng đó hay không?
Một cách khác là liệt kê các loại đối tượng muốn sử dụng khả năng này. Nếu một khả năng trở nên hữu ích cho một phạm vi của các đối tượng khác nhau mà không thật sự liên quan đến mỗi đối tượng khác, giao diện là lựa chọn thích hợp.
Chú ý Bởi vì chỉ có thể có một lớp cơ sở trong thời gian chạy .NET, nên quyết định này là khá quan trọng. Nếu một lớp cơ sở là cần thiết, người sử dụng sẽ rất thất vọng nếu chúng thật sự có một lớp cơ sở và không thể sử dụng một đặc tính.
Khi sử dụng các giao diện, như là không có sự hỗ trợ phiên bản hoá giao diện. Nếu một hàm được thêm vào giao diện sau khi người sử dụng đang sử dụng nó, mã của họ sẽ hỏng tại thời gian chạy và lớp của họ sẽ không cài đặt đúng đắn giao diện cho đến khi nào việc sửa đổi thích hợp được thực hiện.
Đa cài đặt
Không như thừa kế đối tượng, một lớp có thể cài đặt nhiều hơn một giao diện.
interface IFoo {
void ExecuteFoo();
}
interface IBar {
void ExecuteBar();
}
class Tester: IFoo, IBar {
public void ExecuteFoo() {}
public void ExecuteBar() {}
}
Nó làm việc tốt nếu không có các xung đột tên giữa các hàm trong các giao diện. Nhưng nếu ví dụ chỉ là một khác biệt nhỏ, có thể có một vấn đề:
// error
interface IFoo {
void Execute();
}
interface IBar {
void Execute();
}
class Tester: IFoo, IBar {
// IFoo or IBar implementation?
public void Execute() {}
}
Tester.Execute() cài đặt cho IFoo.Execute() hay IBar.Execute()?
Đó là sự mơ hồ, nên trình biên dịch sẽ báo một lỗi. Nếu người sử dụng điều khiển một trong các giao diện, tên của một trong số chúng có thể được thay đổi, nhưng đó không phải là một giải pháp tốt; tại sao IFoo phải đổi tên hàm của nó chỉ vì IBar có cùng tên?
Nghiêm trọng hơn, nếu IFoo và IBar từ những nhà cung cấp khác nhau, chúng không thể thay đổi.
.NET Runtime và C# hỗ trợ một kỹ thuật được biết đến là cài đặt giao diện tường minh, mà nó cho phép một hàm chỉ định thành phần giao diện nó đang cài đặt.
Cài đặt giao diện tường mình
Để chỉ định một hàm thành phần của giao diện đang cài đặt, làm cho hàm thành phần đủ tư cách bằng cách đặt tên giao diện trước tên thành phần.
Đây là ví dụ trước, được sửa lại để sử dụng cài đặt giao diện tường mình:
using System;
interface IFoo {
void Execute();
}
interface IBar {
void Execute();
}
class Tester: IFoo, IBar {
void IFoo.Execute() {
Console.WriteLine(“IFoo.Execute implementation”);
}
void IBar.Execute() {
Console.WriteLine(“IBar.Execute implementation”);
}
}
class Test {
public static void
Tester tester = new Tester();
IFoo iFoo = (IFoo) tester;
iFoo.Execute();
IBar iBar = (IBar) tester;
iBar.Execute();
}
}
Cho ra kết quả:
IFoo.Execute implementation
IBar.Execute implementation
Đó là những gì chúng ta mong đợi. Nhưng lớp kiểm tra sau đây sẽ làm gì?
// error
using System;
interface IFoo {
void Execute();
}
interface IBar {
void Execute();
}
class Tester: IFoo, IBar {
void IFoo.Execute() {
Console.WriteLine(“IFoo.Execute implementation”);
}
void IBar.Execute() {
Console.WriteLine(“IBar.Execute implementation”);
}
}
class Test {
public static void
Tester tester = new Tester();
tester.Execute();
}
}
IFoo.Execute() được gọi, hay IBar.Execute() được gọi?
Đáp án là không cái nào được gọi cả. Không có bổ từ truy xuất trên các cài đặt IFoo.Execute() và IBar.Execute() trong lớp Tester, và do đó các hàm là private và không thể gọi.
Trong trường hợp này, hành vi này là không đúng bởi vì bổ từ public không được sử dụng trên các hàm, là bởi vì các bổ từ truy xuất được ngăn chặn trên các cài đặt giao diện tường minh, để chỉ cách này giao diện có thể được truy xuất là bằng cách chuyển đổi kiểu đối tượng thành giao diện thích hợp.
Để trình bày một trong số các hàm, một hàm chuyển tiếp được thêm vào Tester:
using System;
interface IFoo {
void Execute();
}
interface IBar {
void Execute();
}
class Tester: IFoo, IBar {
void IFoo.Execute() {
Console.WriteLine(“IFoo.Execute implementation”);
}
void IBar.Execute() {
Console.WriteLine(“IBar.Execute implementation”);
}
public void Execute() {
((IFoo)this).Execute();
}
}
class Test {
public static void
Tester tester = new Tester();
tester.Execute();
}
}
Bây giờ, lời gọi hàm Execute() trên một thể hiện của Tester sẽ chuyển đến cho Tester.IFoo.Execute().
Sự che đậy này có thể được sử dụng cho các mục đích khác, chi tiết trong mục tiếp theo.
Che dấu cài đặt
Có nhiều trường hợp mà có ý nghĩa khi cho che dấu một cài đặt của một giao diện trước người sử dụng một lớp, hoặc bởi vì nói chung là không hữu ích, hoặc chỉ để giảm một thành phần lộn xộn. Thực hiện như vậy có thể làm cho một đối tượng dễ dàng sử dụng hơn nhiều. Ví dụ:
using System;
class DrawingSurface {}
interface IRenderIcon {
void DrawIcon(DrawingSurface surface, int x, int y);
void DragIcon(DrawingSurface surface, int x, int y, int x2, int y2);
void ResizeIcon(DrawingSurface surface, int xsize, int ysize);
}
class Employee: IRenderIcon {
public Employee(int id, string name) {
this.id = id;
this.name = name;
}
void IRenderIcon.DrawIcon(DrawingSurface surface, int x, int y) {}
void IRenderIcon.DragIcon(DrawingSurface surface, int x, int y, int x2, int y2) {}
void IRenderIcon.ResizeIcon(DrawingSurface surface, int xsize, int ysize) {}
int id;
string name;
}
Nếu giao diện được cài đặt bình thường, các hàm thành phần DrawIcon(), DragIcon(), và ResizeIcon() hiển nhiên là một phần của Employee, mà có thể gây bối rối cho người sử dụng lớp. Bằng cách cài đặt chúng thông qua cài đặt tường minh, chúng chỉ có thể được truy xuất thông qua giao diện.
Giao diện dựa trên giao diện
Các giao diện cũng có thể được kết hợp với nhau để hình thành các giao diện mới. Các giao diện ISortable và ISerializable có thể được kết hợp với nhau, và các thành phần giao diện mới có thể được thêm vào.
using System.Runtime.Serialization;
using System;
interface IComparableSerializable : IComparable, ISerializable {
string GetStatusString();
}
Một lớp cài đặt IComparableSerializable là cần được cài đặt tất cả các thành phần trong IComparable, ISerializable, và hàm GetStatusString() có trong IComparableSerializable.