Mục đích của giao diện đánh dấu là gì?
Mục đích của giao diện đánh dấu là gì?
Câu trả lời:
Đây là một chút tiếp tuyến dựa trên phản hồi của "Mitch Wheat".
Nói chung, bất cứ khi nào tôi thấy mọi người trích dẫn các nguyên tắc thiết kế khung, tôi luôn muốn đề cập đến điều đó:
Bạn thường nên bỏ qua các hướng dẫn thiết kế khuôn khổ hầu hết thời gian.
Điều này không phải do bất kỳ vấn đề nào với các nguyên tắc thiết kế khung. Tôi nghĩ khung công tác .NET là một thư viện lớp tuyệt vời. Rất nhiều điều tuyệt vời đó đến từ các hướng dẫn thiết kế khung.
Tuy nhiên, các hướng dẫn thiết kế không áp dụng cho hầu hết các mã được viết bởi hầu hết các lập trình viên. Mục đích của họ là cho phép tạo ra một khuôn khổ lớn được sử dụng bởi hàng triệu nhà phát triển, chứ không phải để làm cho việc viết thư viện hiệu quả hơn.
Rất nhiều gợi ý trong đó có thể hướng dẫn bạn làm những điều:
Khung .net rất lớn, thực sự lớn. Nó lớn đến mức hoàn toàn không hợp lý nếu cho rằng bất kỳ ai cũng có kiến thức chi tiết về mọi khía cạnh của nó. Trên thực tế, sẽ an toàn hơn nhiều nếu cho rằng hầu hết các lập trình viên thường xuyên gặp phải các phần của khuôn khổ mà họ chưa bao giờ sử dụng trước đây.
Trong trường hợp đó, mục tiêu chính của nhà thiết kế API là:
Các nguyên tắc thiết kế khung thúc đẩy các nhà phát triển tạo mã hoàn thành các mục tiêu đó.
Điều đó có nghĩa là thực hiện những việc như tránh các lớp kế thừa, ngay cả khi nó có nghĩa là sao chép mã hoặc đẩy tất cả mã ném ngoại lệ ra "điểm nhập" thay vì sử dụng trình trợ giúp được chia sẻ (để dấu vết ngăn xếp có ý nghĩa hơn trong trình gỡ lỗi) và rất nhiều của những thứ tương tự khác.
Lý do chính mà các nguyên tắc đó đề xuất sử dụng các thuộc tính thay vì giao diện mã là vì việc loại bỏ các giao diện đánh dấu làm cho cấu trúc kế thừa của thư viện lớp dễ tiếp cận hơn nhiều. Một sơ đồ lớp với 30 kiểu và 6 lớp phân cấp kế thừa là rất khó so với một sơ đồ có 15 kiểu và 2 lớp phân cấp.
Nếu thực sự có hàng triệu nhà phát triển đang sử dụng các API của bạn hoặc cơ sở mã của bạn thực sự lớn (hơn 100 nghìn LOC) thì việc làm theo các nguyên tắc đó có thể giúp ích rất nhiều.
Nếu 5 triệu nhà phát triển dành 15 phút để học một API thay vì dành 60 phút để học nó, thì kết quả là tiết kiệm ròng 428 năm công. Đó là rất nhiều thời gian.
Tuy nhiên, hầu hết các dự án không liên quan đến hàng triệu nhà phát triển hoặc 100K + LOC. Trong một dự án điển hình, với 4 nhà phát triển và khoảng 50 nghìn địa phương, tập hợp các giả định khác nhau rất nhiều. Các nhà phát triển trong nhóm sẽ hiểu rõ hơn nhiều về cách mã hoạt động. Điều đó có nghĩa là việc tối ưu hóa để tạo ra mã chất lượng cao một cách nhanh chóng sẽ có ý nghĩa hơn rất nhiều và để giảm số lượng lỗi và nỗ lực cần thiết để thực hiện thay đổi.
Dành 1 tuần để phát triển mã phù hợp với khung .net, so với 8 giờ viết mã dễ thay đổi và ít lỗi hơn có thể dẫn đến:
Nếu không có 4.999.999 nhà phát triển khác chịu chi phí thì thường là không đáng.
Ví dụ: kiểm tra giao diện điểm đánh dấu chỉ có một biểu thức "là" và dẫn đến ít mã tìm kiếm thuộc tính hơn.
Vì vậy, lời khuyên của tôi là:
virtual protected
phương pháp mẫu DoSomethingCore
thay vì DoSomething
không phải là công việc bổ sung nhiều và bạn thông báo rõ ràng rằng đó là một phương thức mẫu ... IMNSHO, những người viết ứng dụng mà không xem xét API ( But.. I'm not a framework developer, I don't care about my API!
) chính xác là những người viết nhiều bản sao ( và cũng không có tài liệu và thường không đọc được) mã, không phải ngược lại.
Marker Interfaces được sử dụng để đánh dấu khả năng của một lớp là triển khai một giao diện cụ thể tại thời điểm chạy.
Các thiết kế giao diện và .NET Loại Hướng dẫn thiết kế - Thiết kế giao diện không khuyến khích việc sử dụng các giao diện đánh dấu ủng hộ của việc sử dụng các thuộc tính trong C #, nhưng như @ Jay Bazuzi chỉ ra, đó là dễ dàng hơn để kiểm tra các giao diện đánh dấu hơn cho các thuộc tính:o is I
Vì vậy, thay vì điều này:
public interface IFooAssignable {}
public class FooAssignableAttribute : IFooAssignable
{
...
}
Nguyên tắc .NET khuyến nghị bạn làm điều này:
public class FooAssignableAttribute : Attribute
{
...
}
[FooAssignable]
public class Foo
{
...
}
Vì mọi câu trả lời khác đều nêu "chúng nên được tránh", sẽ hữu ích nếu có lời giải thích tại sao.
Thứ nhất, tại sao các giao diện đánh dấu được sử dụng: Chúng tồn tại để cho phép mã đang sử dụng đối tượng triển khai nó để kiểm tra xem chúng có triển khai giao diện đã nói hay không và đối xử với đối tượng theo cách khác nếu có.
Vấn đề với cách tiếp cận này là nó phá vỡ tính đóng gói. Bản thân đối tượng bây giờ có quyền kiểm soát gián tiếp đối với cách nó sẽ được sử dụng bên ngoài. Hơn nữa, nó có kiến thức về hệ thống mà nó sẽ được sử dụng. Bằng cách áp dụng giao diện đánh dấu, định nghĩa lớp cho thấy nó dự kiến sẽ được sử dụng ở một nơi nào đó để kiểm tra sự tồn tại của điểm đánh dấu. Nó có kiến thức ngầm về môi trường mà nó được sử dụng và đang cố gắng xác định cách nó nên được sử dụng. Điều này đi ngược lại với ý tưởng đóng gói bởi vì nó có kiến thức về việc thực hiện một phần của hệ thống tồn tại hoàn toàn bên ngoài phạm vi của chính nó.
Ở cấp độ thực tế, điều này làm giảm tính di động và khả năng tái sử dụng. Nếu lớp được sử dụng lại trong một ứng dụng khác, giao diện cũng cần được sao chép và nó có thể không có bất kỳ ý nghĩa nào trong môi trường mới, khiến nó hoàn toàn dư thừa.
Như vậy, "điểm đánh dấu" là siêu dữ liệu về lớp. Siêu dữ liệu này không được sử dụng bởi chính lớp và chỉ có ý nghĩa đối với (một số!) Mã máy khách bên ngoài để nó có thể xử lý đối tượng theo một cách nhất định. Vì nó chỉ có ý nghĩa đối với mã máy khách, siêu dữ liệu phải nằm trong mã máy khách, không phải API lớp.
Sự khác biệt giữa "giao diện đánh dấu" và giao diện bình thường là giao diện có các phương thức cho thế giới bên ngoài biết cách nó có thể được sử dụng trong khi giao diện trống ngụ ý nó nói với thế giới bên ngoài cách nó nên được sử dụng.
IConstructableFromString<T>
quy định cụ thể rằng một lớp T
chỉ có thể thực hiện IConstructableFromString<T>
nếu nó có một thành viên tĩnh ...
public static T ProduceFromString(String params);
, một lớp đồng hành với giao diện có thể đưa ra một phương thức public static T ProduceFromString<T>(String params) where T:IConstructableFromString<T>
; nếu mã máy khách có một phương thức như thế nào T[] MakeManyThings<T>() where T:IConstructableFromString<T>
, người ta có thể xác định các kiểu mới có thể hoạt động với mã máy khách mà không cần phải sửa đổi mã máy khách để xử lý chúng. Nếu siêu dữ liệu nằm trong mã ứng dụng khách, thì sẽ không thể tạo các loại mới để ứng dụng khách hiện tại sử dụng.
T
và lớp sử dụng nó là IConstructableFromString<T>
nơi bạn có một phương thức trong giao diện mô tả một số hành vi nên nó không phải là giao diện đánh dấu.
ProduceFromString
trong ví dụ trên sẽ không liên quan đến giao diện theo bất kỳ cách nào, ngoại trừ giao diện sẽ được sử dụng như một điểm đánh dấu để chỉ ra những lớp nào nên được mong đợi để thực hiện chức năng cần thiết.
Các giao diện đánh dấu đôi khi có thể là một điều xấu cần thiết khi một ngôn ngữ không hỗ trợ các loại liên minh phân biệt đối xử .
Giả sử bạn muốn xác định một phương thức mong đợi một đối số có kiểu phải chính xác là một trong A, B hoặc C. Trong nhiều ngôn ngữ đầu tiên chức năng (như F # ), kiểu như vậy có thể được định nghĩa rõ ràng là:
type Arg =
| AArg of A
| BArg of B
| CArg of C
Tuy nhiên, trong ngôn ngữ OO-first như C #, điều này là không thể. Cách duy nhất để đạt được điều gì đó tương tự ở đây là xác định giao diện IArg và "đánh dấu" A, B và C với nó.
Tất nhiên, bạn có thể tránh sử dụng giao diện đánh dấu bằng cách chỉ chấp nhận kiểu "đối tượng" làm đối số, nhưng sau đó bạn sẽ mất đi tính biểu cảm và một số mức độ an toàn của kiểu.
Các kiểu liên minh phân biệt đối xử cực kỳ hữu ích và đã tồn tại trong các ngôn ngữ chức năng trong ít nhất 30 năm. Thật kỳ lạ, cho đến ngày nay, tất cả các ngôn ngữ OO chính thống đều bỏ qua tính năng này - mặc dù nó thực sự không liên quan gì đến lập trình chức năng, nhưng thuộc về kiểu hệ thống.
Foo<T>
sẽ có một tập hợp các trường tĩnh riêng biệt cho mọi kiểu T
, nên không khó để có một lớp chung chứa các trường tĩnh chứa các đại diện để xử lý a T
và điền trước các trường đó với các hàm để xử lý mọi kiểu mà lớp đó phải làm việc với. Việc sử dụng ràng buộc giao diện chung theo kiểu T
sẽ kiểm tra tại thời điểm trình biên dịch rằng kiểu được cung cấp ít nhất đã được tuyên bố là hợp lệ, mặc dù nó sẽ không thể đảm bảo rằng nó thực sự là như vậy.
Giao diện đánh dấu chỉ là một giao diện trống. Một lớp sẽ triển khai giao diện này dưới dạng siêu dữ liệu được sử dụng vì một số lý do. Trong C #, bạn thường sử dụng các thuộc tính để đánh dấu một lớp vì lý do tương tự như khi bạn sử dụng giao diện đánh dấu bằng các ngôn ngữ khác.
Giao diện đánh dấu cho phép một lớp được gắn thẻ theo cách sẽ được áp dụng cho tất cả các lớp con. Một giao diện đánh dấu "thuần túy" sẽ không xác định hoặc kế thừa bất cứ thứ gì; một loại giao diện đánh dấu hữu ích hơn có thể là một loại giao diện "kế thừa" một giao diện khác nhưng không xác định thành viên mới. Ví dụ: nếu có một giao diện "IReadableFoo", người ta cũng có thể xác định một giao diện "IImmutableFoo", giao diện này sẽ hoạt động giống như một "Foo" nhưng sẽ hứa với bất kỳ ai sử dụng nó rằng sẽ không có gì thay đổi giá trị của nó. Một quy trình chấp nhận IImmutableFoo sẽ có thể sử dụng nó như một IReadableFoo, nhưng quy trình sẽ chỉ chấp nhận các lớp được khai báo là triển khai IImmutableFoo.
Tôi không thể nghĩ ra nhiều cách sử dụng cho các giao diện đánh dấu "thuần túy". Điều duy nhất tôi có thể nghĩ đến là nếu EqualityComparer (của T) .Default sẽ trả về Object.Equals cho bất kỳ kiểu nào đã triển khai IDoNotUseEqualityComparer, ngay cả khi kiểu đó cũng được triển khai IEqualityComparer. Điều này sẽ cho phép người ta có một kiểu bất biến không được niêm phong mà không vi phạm Nguyên tắc thay thế Liskov: nếu kiểu này chặn tất cả các phương pháp liên quan đến kiểm tra bình đẳng, một kiểu dẫn xuất có thể thêm các trường bổ sung và chúng có thể thay đổi được, nhưng sự đột biến của các trường đó sẽ không ' không hiển thị bằng bất kỳ phương thức kiểu cơ sở nào. Có thể không quá khủng khiếp khi có một lớp bất biến không được niêm phong và tránh mọi việc sử dụng EqualityComparer.Default hoặc tin cậy các lớp dẫn xuất không triển khai IEqualityComparer,
Hai phương pháp mở rộng này sẽ giải quyết hầu hết các vấn đề mà Scott khẳng định là ưu tiên các giao diện đánh dấu hơn các thuộc tính:
public static bool HasAttribute<T>(this ICustomAttributeProvider self)
where T : Attribute
{
return self.GetCustomAttributes(true).Any(o => o is T);
}
public static bool HasAttribute<T>(this object self)
where T : Attribute
{
return self != null && self.GetType().HasAttribute<T>()
}
Bây giờ bạn có:
if (o.HasAttribute<FooAssignableAttribute>())
{
//...
}
đấu với:
if (o is IFooAssignable)
{
//...
}
Tôi không biết việc xây dựng một API sẽ mất thời gian gấp 5 lần với mẫu đầu tiên so với mẫu thứ hai, như Scott tuyên bố.
Giao diện đánh dấu thực sự chỉ là một lập trình thủ tục bằng ngôn ngữ OO. Giao diện xác định hợp đồng giữa người triển khai và người tiêu dùng, ngoại trừ giao diện đánh dấu, bởi vì giao diện đánh dấu không định nghĩa gì ngoài chính nó. Vì vậy, ngay khi ra khỏi cổng, giao diện đánh dấu không đạt được mục đích cơ bản là một giao diện.
Điểm đánh dấu là giao diện trống. Điểm đánh dấu có ở đó hoặc không có.
lớp Foo: IConfidential
Ở đây chúng tôi đánh dấu Foo là bí mật. Không yêu cầu thuộc tính hoặc thuộc tính bổ sung thực tế.
Giao diện đánh dấu là một giao diện trống hoàn toàn không có nội dung / thành viên dữ liệu / triển khai.
Một lớp thực hiện giao diện đánh dấu khi được yêu cầu, nó chỉ để " đánh dấu "; có nghĩa là nó cho JVM biết rằng lớp cụ thể nhằm mục đích sao chép, vì vậy hãy cho phép nó sao chép. Lớp cụ thể này là Serialize các đối tượng của nó, vì vậy hãy cho phép các đối tượng của nó được serialize.