Mục đích của giao diện đánh dấu là gì?


Câu trả lời:


77

Đâ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:

  1. Có thể không phải là cách đơn giản nhất để thực hiện một cái gì đó
  2. Có thể dẫn đến trùng lặp mã
  3. Có thể có thêm chi phí thời gian chạy

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à:

  1. Giữ mọi thứ nhất quán với phần còn lại của khuôn khổ
  2. Loại bỏ sự phức tạp không cần thiết trong khu vực bề mặt API

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:

  1. Dự án muộn
  2. Tiền thưởng thấp hơn
  3. Số lượng lỗi tăng lên
  4. Nhiều thời gian ở văn phòng hơn và ít thời gian trên bãi biển hơn để uống bơ thực vật.

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à:

  1. Tuân theo các nguyên tắc khung một cách tôn giáo nếu bạn đang phát triển các thư viện lớp (hoặc tiện ích giao diện người dùng) dành cho việc tiêu thụ rộng rãi.
  2. Cân nhắc áp dụng một số trong số chúng nếu bạn có hơn 100K LOC trong dự án của mình
  3. Nếu không, hãy bỏ qua chúng hoàn toàn.

12
Cá nhân tôi, tôi thấy bất kỳ mã nào tôi viết dưới dạng thư viện mà tôi sẽ cần sử dụng sau này. Tôi không thực sự quan tâm của tiêu thụ là phổ biến hay không - sau chủ trương tăng tính nhất quán, và làm giảm sự ngạc nhiên khi tôi cần phải nhìn vào mã của tôi và hiểu nó năm sau ...
Reed Copsey

16
Tôi không nói rằng các hướng dẫn là xấu. Tôi đang nói rằng chúng phải khác nhau, tùy thuộc vào kích thước cơ sở mã của bạn và số lượng người dùng bạn có. Rất nhiều hướng dẫn thiết kế dựa trên những thứ như duy trì khả năng so sánh nhị phân, điều này không quan trọng đối với các thư viện "nội bộ" được sử dụng bởi vô số dự án như đối với một cái gì đó như BCL. Các nguyên tắc khác, như những nguyên tắc liên quan đến khả năng sử dụng, hầu như luôn quan trọng. Đạo đức là không quá tôn trọng các hướng dẫn, đặc biệt là đối với các dự án nhỏ.
Scott Wisniewski

6
+1 - Không hoàn toàn trả lời câu hỏi của OP - Mục đích của MI - Nhưng dù sao cũng rất hữu ích.
bzarah

5
@ScottWisniewski: Tôi nghĩ rằng bạn đang thiếu điểm nghiêm trọng. Các hướng dẫn Khung chỉ không áp dụng cho dự án lớn mà áp dụng cho một số dự án vừa và nhỏ. Chúng trở nên quá mức khi bạn luôn cố gắng áp dụng chúng vào chương trình Hello-World. Ví dụ: giới hạn giao diện ở 5 phương thức luôn là một nguyên tắc cơ bản bất kể kích thước ứng dụng là bao nhiêu. Một điều bạn bỏ lỡ nữa, ứng dụng nhỏ hôm nay có thể trở thành ứng dụng lớn của ngày mai. Vì vậy, tốt hơn hết bạn nên xây dựng nó với các nguyên tắc tốt áp dụng cho các ứng dụng lớn để khi mở rộng quy mô, bạn không phải viết lại nhiều mã.
Phil

2
Tôi không hiểu rõ việc tuân theo (hầu hết) các hướng dẫn thiết kế sẽ dẫn đến một dự án 8 giờ đột ngột mất 1 tuần như thế nào. ví dụ: Đặt tên cho một virtual protectedphương pháp mẫu DoSomethingCorethay vì DoSomethingkhô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.
Laoujin

44

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.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 
{    
   ...
} 

26
Ngoài ra, chúng ta hoàn toàn có thể sử dụng generic với các giao diện đánh dấu, nhưng không sử dụng với các thuộc tính.
Jordão

18
Trong khi tôi yêu thích các thuộc tính và cách chúng trông từ quan điểm khai báo, chúng không phải là công dân hạng nhất trong thời gian chạy và yêu cầu một số lượng đáng kể hệ thống ống nước cấp thấp để làm việc.
Jesse C. Slicer

4
@ Jordão - Đây chính xác là suy nghĩ của tôi. Ví dụ, nếu tôi muốn trừu tượng hóa mã truy cập cơ sở dữ liệu (ví dụ Linq thành Sql), việc có một giao diện chung làm cho nó dễ dàng hơn RẤT NHIỀU. Trên thực tế, tôi không nghĩ có thể viết kiểu trừu tượng đó với các thuộc tính vì bạn không thể truyền thành thuộc tính và không thể sử dụng chúng trong generic. Tôi cho rằng bạn có thể sử dụng một lớp cơ sở trống mà các lớp khác đều lấy từ đó, nhưng điều đó ít nhiều giống với việc có một giao diện trống. Ngoài ra, nếu sau này bạn nhận ra mình cần chức năng được chia sẻ, thì cơ chế này đã có sẵn.
tandrewnichols 26/12/12

23

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.


1
Mục đích chính của bất kỳ giao diện nào là để phân biệt giữa các lớp hứa tuân thủ hợp đồng được liên kết với giao diện đó và những lớp nào không. Mặc dù một giao diện cũng chịu trách nhiệm cung cấp các chữ ký kêu gọi của bất kỳ thành viên nào cần thiết để thực hiện hợp đồng, nhưng chính hợp đồng chứ không phải các thành viên sẽ xác định xem một giao diện cụ thể có nên được thực hiện bởi một lớp cụ thể hay không. Nếu hợp đồng IConstructableFromString<T>quy định cụ thể rằng một lớp Tchỉ có thể thực hiện IConstructableFromString<T>nếu nó có một thành viên tĩnh ...
supercat

... 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.
supercat

Nhưng hợp đồng giữa Tvà 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.
Tom B

Phương thức tĩnh mà lớp bắt buộc phải có không phải là một phần của giao diện. Các thành viên tĩnh trong các giao diện được thực hiện bởi chính các giao diện; không có cách nào để một giao diện tham chiếu đến một thành viên tĩnh trong một lớp triển khai.
supercat

Phương thức có thể xác định, bằng cách sử dụng Reflection, liệu một kiểu chung có xảy ra một phương thức tĩnh cụ thể hay không và thực thi phương thức đó nếu nó tồn tại, nhưng quá trình thực sự tìm kiếm và thực thi phương thức tĩnh ProduceFromStringtrong 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.
supercat

8

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.


Cần lưu ý rằng vì a 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 Tvà đ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 Tsẽ 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.
supercat

6

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.


4

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,


4

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ố.


1
Vẫn không có generic.
Ian Kemp

1

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.


0

Đ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ế.


0

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.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.