Làm cách nào để thiết kế một giao diện sao cho rõ ràng thuộc tính nào có thể thay đổi giá trị của chúng và giá trị nào sẽ không đổi?


12

Tôi có một vấn đề thiết kế liên quan đến các thuộc tính .NET.

interface IX
{
    Guid Id { get; }
    bool IsInvalidated { get; }
    void Invalidate();
}

Vấn đề:

Giao diện này có hai thuộc tính chỉ đọc IdIsInvalidated. Tuy nhiên, thực tế là chúng chỉ đọc, không có gì đảm bảo rằng giá trị của chúng sẽ không đổi.

Hãy nói rằng đó là ý định của tôi để làm cho nó rõ ràng rằng

  • Id đại diện cho một giá trị không đổi (do đó có thể được lưu trữ an toàn), trong khi
  • IsInvalidatedcó thể thay đổi giá trị của nó trong suốt vòng đời của một IXđối tượng (và do đó không nên lưu vào bộ nhớ cache).

Làm thế nào tôi có thể sửa đổi interface IXđể làm cho hợp đồng đó đủ rõ ràng?

Ba nỗ lực của riêng tôi tại một giải pháp:

  1. Giao diện đã được thiết kế tốt. Sự hiện diện của một phương thức được gọi là Invalidate()cho phép một lập trình viên suy ra rằng giá trị của thuộc tính có cùng tên IsInvalidatedcó thể bị ảnh hưởng bởi nó.

    Đối số này chỉ giữ trong trường hợp phương thức và thuộc tính được đặt tên tương tự.

  2. Tăng cường giao diện này với một sự kiện IsInvalidatedChanged:

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;

    Sự hiện diện của một …Changedsự kiện cho IsInvalidatedcác tài sản này có thể thay đổi giá trị của nó và sự vắng mặt của một sự kiện tương tự Idlà một lời hứa rằng tài sản đó sẽ không thay đổi giá trị của nó.

    Tôi thích giải pháp này, nhưng đó là rất nhiều thứ bổ sung có thể không được sử dụng.

  3. Thay thế tài sản IsInvalidatedbằng một phương pháp IsInvalidated():

    bool IsInvalidated();

    Điều này có thể là quá tinh tế một sự thay đổi. Nó được coi là một gợi ý rằng một giá trị được tính mới mỗi lần - điều này sẽ không cần thiết nếu đó là một hằng số. Chủ đề MSDN "Lựa chọn giữa các thuộc tính và phương thức" có ý nghĩa này để nói về nó:

    Sử dụng một phương pháp, thay vì một tài sản, trong các tình huống sau đây. [V]] Hoạt động trả về một kết quả khác nhau mỗi lần nó được gọi, ngay cả khi các tham số không thay đổi.

Những loại câu trả lời tôi mong đợi?

  • Tôi quan tâm nhất đến các giải pháp hoàn toàn khác nhau cho vấn đề, cùng với lời giải thích về cách họ đánh bại những nỗ lực trên của tôi.

  • Nếu những nỗ lực của tôi là thiếu sót về mặt logic hoặc có những nhược điểm đáng kể chưa được đề cập, như vậy chỉ còn một giải pháp (hoặc không có), tôi muốn nghe về nơi tôi đã sai.

    Nếu sai sót là nhỏ và vẫn còn nhiều giải pháp sau khi xem xét, vui lòng bình luận.

  • Ít nhất, tôi muốn có một số phản hồi về giải pháp ưa thích của bạn và vì lý do gì.


Không IsInvalidatedcần một private set?
James

2
@James: Không nhất thiết, không phải trong khai báo giao diện, cũng không phải trong triển khai:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

@James Thuộc tính trong giao diện không giống như thuộc tính tự động, mặc dù chúng sử dụng cùng một cú pháp.
Svick

Tôi đã tự hỏi: các giao diện "có sức mạnh" để áp đặt hành vi như vậy? Không nên ủy thác hành vi này cho mỗi thực hiện? Tôi nghĩ rằng nếu bạn muốn thực thi mọi thứ ở mức đó, bạn nên xem xét để tạo một lớp trừu tượng để các kiểu con sẽ kế thừa từ nó, trong khi thực hiện một giao diện nhất định. (nhân tiện, lý do của tôi có ý nghĩa không?)
heltonbiker

@heltonbiker Thật không may, hầu hết các ngôn ngữ (tôi biết) không cho phép thể hiện các ràng buộc như vậy đối với các loại, nhưng chúng chắc chắn nên . Đây là một vấn đề đặc điểm kỹ thuật, không thực hiện. Trong ý kiến ​​của tôi, điều còn thiếu là khả năng diễn đạt bất biến lớpđiều kiện hậu trực tiếp bằng ngôn ngữ. Lý tưởng nhất là chúng ta không bao giờ phải lo lắng về InvalidStateExceptions, chẳng hạn, nhưng tôi không chắc liệu điều này thậm chí có thể về mặt lý thuyết hay không. Sẽ tốt đẹp, mặc dù.
proskor

Câu trả lời:


2

Tôi thích giải pháp 3 hơn 1 và 2.

Vấn đề của tôi với giải pháp 1 là : điều gì xảy ra nếu không có Invalidatephương pháp? Giả sử một giao diện với một thuộc IsValidtính trả về DateTime.Now > MyExpirationDate; bạn có thể không cần một SetInvalidphương pháp rõ ràng ở đây. Điều gì nếu một phương thức ảnh hưởng đến nhiều thuộc tính? Giả sử một loại kết nối với IsOpenIsConnectedcác thuộc tính - cả hai đều bị ảnh hưởng bởi Closephương thức.

Giải pháp 2 : Nếu điểm duy nhất của sự kiện là thông báo cho các nhà phát triển rằng một thuộc tính có tên tương tự có thể trả về các giá trị khác nhau trên mỗi cuộc gọi, thì tôi sẽ khuyên bạn nên chống lại nó. Bạn nên giữ giao diện của bạn ngắn và rõ ràng; hơn nữa, không phải tất cả việc thực hiện đều có thể kích hoạt sự kiện đó cho bạn. Tôi sẽ sử dụng lại IsValidví dụ của tôi từ phía trên: bạn sẽ phải thực hiện Timer và thực hiện một sự kiện khi bạn tiếp cận MyExpirationDate. Rốt cuộc, nếu sự kiện đó là một phần của giao diện công cộng của bạn, thì người dùng giao diện sẽ mong đợi sự kiện đó hoạt động.

Điều đó đang được nói về những giải pháp này: chúng không tệ. Sự hiện diện của một phương thức hoặc một sự kiện SILL chỉ ra rằng một thuộc tính có tên tương tự có thể trả về các giá trị khác nhau trên mỗi cuộc gọi. Tất cả những gì tôi nói là một mình họ không đủ để luôn truyền đạt ý nghĩa đó.

Giải pháp 3 là những gì tôi sẽ làm. Như aviv đã đề cập, điều này chỉ có thể hoạt động cho các nhà phát triển C #. Đối với tôi với tư cách là một C # -dev, thực tế IsInvalidatedkhông phải là một tài sản ngay lập tức truyền đạt ý nghĩa của "đó không chỉ là một người truy cập đơn giản, một cái gì đó đang diễn ra ở đây". Điều này không áp dụng cho tất cả mọi người, và như MainMa đã chỉ ra, chính .NET framework không nhất quán ở đây.

Nếu bạn muốn sử dụng giải pháp 3, tôi khuyên bạn nên tuyên bố đó là quy ước và để cả nhóm tuân theo. Tài liệu là điều cần thiết quá, tôi tin. Theo tôi, thực sự khá đơn giản để gợi ý thay đổi giá trị: " trả về giá trị đúng nếu giá trị vẫn hợp lệ ", " cho biết liệu kết nối đã được mở " so với " trả về đúng nếu đối tượng này hợp lệ ". Nó sẽ không đau chút nào mặc dù rõ ràng hơn trong tài liệu.

Vì vậy, lời khuyên của tôi sẽ là:

Khai báo giải pháp 3 một quy ước và nhất quán tuân theo nó. Làm rõ trong tài liệu về các thuộc tính cho dù một thuộc tính có thay đổi giá trị hay không. Các nhà phát triển làm việc với các thuộc tính đơn giản sẽ cho rằng chúng không thay đổi (nghĩa là chỉ thay đổi khi trạng thái đối tượng được sửa đổi). Các nhà phát triển gặp phải một phương pháp mà âm thanh như nó có thể là một tài sản ( Count(), Length(), IsOpen()) biết rằng someting đang xảy ra và (hy vọng) đọc các tài liệu phương pháp để hiểu chính xác những gì các phương pháp thực hiện và cách ứng xử.


Câu hỏi này đã nhận được câu trả lời rất hay và thật đáng tiếc tôi chỉ có thể chấp nhận một trong số chúng. Tôi đã chọn câu trả lời của bạn vì nó cung cấp một bản tóm tắt tốt về một số điểm được nêu trong các câu trả lời khác, và vì đó ít nhiều là những gì tôi đã quyết định làm. Cảm ơn tất cả những người đã đăng một câu trả lời!
stakx

8

Có một giải pháp thứ tư: dựa vào tài liệu .

Làm thế nào để bạn biết rằng stringlớp học là bất biến? Bạn chỉ đơn giản biết điều đó, vì bạn đã đọc tài liệu MSDN. StringBuilder, mặt khác, là có thể thay đổi, bởi vì, một lần nữa, tài liệu cho bạn biết điều đó.

/// <summary>
/// Represents an index of an element stored in the database.
/// </summary>
private interface IX
{
    /// <summary>
    /// Gets the immutable globally unique identifier of the index, the identifier being
    /// assigned by the database when the index is created.
    /// </summary>
    Guid Id { get; }

    /// <summary>
    /// Gets a value indicating whether the index is invalidated, which means that the
    /// indexed element is no longer valid and should be reloaded from the database. The
    /// index can be invalidated by calling the <see cref="Invalidate()" /> method.
    /// </summary>
    bool IsInvalidated { get; }

    /// <summary>
    /// Invalidates the index, without forcing the indexed element to be reloaded from the
    /// database.
    /// </summary>
    void Invalidate();
}

Giải pháp thứ ba của bạn không tệ, nhưng .NET Framework không tuân theo nó . Ví dụ:

StringBuilder.Length
DateTime.Now

là tài sản, nhưng đừng hy vọng chúng sẽ không đổi mỗi lần.

Cái được sử dụng nhất quán trên .NET Framework chính là:

IEnumerable<T>.Count()

là một phương thức, trong khi:

IList<T>.Length

là một tài sản. Trong trường hợp đầu tiên, làm công cụ có thể yêu cầu công việc bổ sung, bao gồm, ví dụ, truy vấn cơ sở dữ liệu. Công việc bổ sung này có thể mất một thời gian. Trong trường hợp của một tài sản, dự kiến ​​sẽ mất một thời gian ngắn : thực sự, độ dài có thể thay đổi trong suốt thời gian tồn tại của danh sách, nhưng thực tế không có gì để trả lại.


Với các lớp, chỉ cần nhìn vào mã cho thấy rõ rằng giá trị của một thuộc tính sẽ không thay đổi trong suốt vòng đời của đối tượng. Ví dụ,

public class Product
{
    private readonly int price;

    public Product(int price)
    {
        this.price = price;
    }

    public int Price
    {
        get
        {
            return this.price;
        }
    }
}

là rõ ràng: giá sẽ giữ nguyên.

Đáng buồn thay, bạn không thể áp dụng cùng một mẫu cho giao diện và do C # không đủ biểu cảm trong trường hợp đó, nên sử dụng tài liệu (bao gồm cả sơ đồ XML và sơ đồ UML) để thu hẹp khoảng cách.


Ngay cả các hướng dẫn không có công việc bổ sung, hướng dẫn của người dùng không được sử dụng hoàn toàn nhất quán. Ví dụ, Lazy<T>.Valuecó thể mất một thời gian rất dài để tính toán (khi nó được truy cập lần đầu tiên).
Svick

1
Theo tôi, không có vấn đề gì nếu .NET framework tuân theo quy ước của giải pháp 3. Tôi hy vọng các nhà phát triển đủ thông minh để biết họ đang làm việc với các loại trong nhà hay các loại khung. Các nhà phát triển không biết rằng không đọc tài liệu và do đó, giải pháp 4 cũng không giúp được gì. Nếu giải pháp 3 được triển khai như một quy ước của công ty / nhóm, thì tôi tin rằng việc các API khác có tuân theo nó hay không (mặc dù tất nhiên nó sẽ được đánh giá rất cao).
enzi

4

2 xu của tôi:

Tùy chọn 3: Là một người không phải C # -er, nó sẽ không có nhiều manh mối; Tuy nhiên, đánh giá bằng trích dẫn bạn mang lại, cần phải rõ ràng với các lập trình viên C # thành thạo rằng điều này có nghĩa gì đó. Bạn vẫn nên thêm tài liệu về điều này mặc dù.

Tùy chọn 2: Trừ khi bạn có kế hoạch thêm các sự kiện vào mọi thứ có thể thay đổi, đừng đến đó.

Sự lựa chọn khác:

  • Đổi tên giao diện IXMutable, vì vậy rõ ràng nó có thể thay đổi trạng thái. Giá trị bất biến duy nhất trong ví dụ của bạn là id, thường được coi là bất biến ngay cả trong các đối tượng có thể thay đổi.
  • Không đề cập đến nó. Các giao diện không tốt trong việc mô tả hành vi như chúng ta mong muốn; Một phần, đó là vì ngôn ngữ không thực sự cho phép bạn mô tả những thứ như "Phương thức này sẽ luôn trả về cùng một giá trị cho một đối tượng cụ thể" hoặc "Gọi phương thức này bằng một đối số x <0 sẽ đưa ra một ngoại lệ."
  • Ngoại trừ việc có một cơ chế để mở rộng ngôn ngữ và cung cấp thông tin chính xác hơn về các cấu trúc - Chú thích / Thuộc tính . Đôi khi họ cần một số công việc, nhưng cho phép bạn chỉ định những điều phức tạp tùy ý về phương thức của bạn. Tìm hoặc phát minh một chú thích có nội dung "Giá trị của phương thức / thuộc tính này có thể thay đổi trong suốt vòng đời của một đối tượng" và thêm nó vào phương thức. Bạn có thể cần chơi một số thủ thuật để có được các phương thức triển khai để "kế thừa" nó và người dùng có thể cần đọc tài liệu cho chú thích đó, nhưng nó sẽ là một công cụ cho phép bạn nói chính xác những gì bạn muốn nói, thay vì gợi ý tại đó

3

Một tùy chọn khác sẽ là phân chia giao diện: giả sử rằng sau khi gọi Invalidate()mọi lệnh gọi IsInvalidatedsẽ trả về cùng một giá trị true, dường như không có lý do nào để gọi IsInvalidatedcho cùng một phần được gọi Invalidate().

Vì vậy, tôi đề nghị, hoặc bạn kiểm tra sự vô hiệu hoặc bạn gây ra nó, nhưng có vẻ không hợp lý để làm cả hai. Do đó, thật hợp lý khi cung cấp một giao diện chứa Invalidate()hoạt động cho phần đầu tiên và giao diện khác để kiểm tra ( IsInvalidated) cho phần khác. Vì điều khá rõ ràng từ chữ ký của giao diện đầu tiên là nó sẽ làm cho cá thể bị vô hiệu, nên câu hỏi còn lại (đối với giao diện thứ hai) thực sự khá chung chung: làm thế nào để xác định xem loại đã cho có bất biến hay không .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

Tôi biết, điều này không trả lời trực tiếp câu hỏi, nhưng ít nhất nó giảm nó thành câu hỏi làm thế nào để đánh dấu một số loại bất biến , đó là một vấn đề phổ biến và được hiểu. Ví dụ: bằng cách giả sử rằng tất cả các loại đều có thể thay đổi trừ khi được định nghĩa khác, bạn có thể kết luận rằng giao diện thứ hai ( IsInvalidated) có thể thay đổi và do đó có thể thay đổi giá trị IsInvalidatedtheo thời gian. Mặt khác, giả sử giao diện đầu tiên trông như thế này (cú pháp giả):

[Immutable]
interface IX
{
    Guid Id { get; }
    void Invalidate();
}

Vì nó được đánh dấu là bất biến, bạn biết rằng Id sẽ không thay đổi. Tất nhiên, việc gọi không hợp lệ sẽ khiến trạng thái của cá thể thay đổi, nhưng thay đổi này sẽ không thể quan sát được thông qua giao diện này.


Một ý tưởng không tồi! Tuy nhiên, tôi sẽ cho rằng thật sai lầm khi đánh dấu một giao diện [Immutable]miễn là nó bao gồm các phương thức mà mục đích duy nhất của nó là gây ra đột biến. Tôi sẽ chuyển Invalidate()phương thức sang Invalidatablegiao diện.
stakx

1
@stakx Tôi không đồng ý. :) Tôi nghĩ bạn nhầm lẫn giữa các khái niệm bất biếntinh khiết (không có tác dụng phụ). Trong thực tế, từ quan điểm của giao diện được đề xuất IX, không có đột biến nào có thể quan sát được cả. Bất cứ khi nào bạn gọi Id, bạn sẽ nhận được cùng một giá trị mỗi lần. Điều tương tự cũng áp dụng cho Invalidate()(bạn không nhận được gì, vì kiểu trả về của nó là void). Do đó, loại là bất biến. Tất nhiên, bạn sẽ có tác dụng phụ , nhưng bạn sẽ không thể quan sát chúng thông qua giao diện IX, vì vậy chúng sẽ không có bất kỳ tác động nào đến khách hàng của IX.
proskor

OK, chúng ta có thể đồng ý rằng đó IXlà bất biến. Và chúng là tác dụng phụ; chúng chỉ được phân phối giữa hai giao diện phân chia sao cho khách hàng không cần quan tâm (trong trường hợp IX) hoặc rõ ràng tác dụng phụ có thể là gì (trong trường hợp Invalidatable). Nhưng tại sao không chuyển Invalidatequa Invalidatable? Điều đó sẽ làm cho IXbất biến tinh khiết, và Invalidatablesẽ không trở nên đột biến hơn, cũng không bất tịnh hơn (vì đó là cả hai điều này rồi).
stakx

(Tôi hiểu rằng trong một kịch bản thực tế, bản chất của sự phân chia giao diện có lẽ sẽ phụ thuộc nhiều hơn về các trường hợp sử dụng thực tế hơn về các vấn đề kỹ thuật, nhưng tôi đang cố gắng để hiểu đầy đủ lập luận của bạn ở đây.)
stakx

1
@stakx Trước hết, bạn đã hỏi về các khả năng tiếp theo bằng cách nào đó làm cho ngữ nghĩa của giao diện của bạn rõ ràng hơn. Ý tưởng của tôi là giảm vấn đề về sự bất biến, đòi hỏi phải chia tách giao diện. Nhưng không chỉ sự phân chia cần thiết để làm cho một giao diện trở nên bất biến, nó cũng sẽ có ý nghĩa lớn, bởi vì không có lý do gì để gọi cả hai Invalidate()IsInvalidatedbởi cùng một người tiêu dùng giao diện . Nếu bạn gọi Invalidate(), bạn nên biết nó trở nên vô hiệu, vậy tại sao phải kiểm tra nó? Do đó, bạn có thể cung cấp hai giao diện riêng biệt cho các mục đích khác nhau.
proskor
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.