Bagaimana saya mendesain antarmuka sedemikian rupa sehingga jelas properti mana yang dapat mengubah nilainya, dan mana yang akan tetap konstan?

12

Saya mengalami masalah desain tentang properti .NET.

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

Masalah:

Antarmuka ini memiliki dua properti hanya-baca, Iddan IsInvalidated. Namun, fakta bahwa itu hanya baca-saja, tidak dengan sendirinya menjamin bahwa nilai-nilai mereka akan tetap konstan.

Katakanlah itu maksud saya untuk membuatnya sangat jelas bahwa ...

  • Id mewakili nilai konstan (yang karenanya dapat di-cache dengan aman), sementara
  • IsInvalidatedmungkin mengubah nilainya selama masa pakai IXobjek (dan karenanya tidak perlu di-cache).

Bagaimana saya bisa memodifikasi interface IXagar kontrak itu cukup eksplisit?

Tiga upaya saya sendiri pada solusi:

  1. Antarmuka sudah dirancang dengan baik. Kehadiran metode yang disebut Invalidate()memungkinkan seorang programmer untuk menyimpulkan bahwa nilai properti dengan nama yang sama IsInvalidatedmungkin dipengaruhi olehnya.

    Argumen ini hanya berlaku dalam kasus di mana metode dan properti diberi nama yang sama.

  2. Tambahkan antarmuka ini dengan sebuah acara IsInvalidatedChanged:

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;
    

    Kehadiran …Changedacara untuk IsInvalidatedmenyatakan bahwa properti ini dapat mengubah nilainya, dan tidak adanya acara serupa untuk Idmerupakan janji bahwa properti itu tidak akan mengubah nilainya.

    Saya suka solusi ini, tetapi banyak hal tambahan yang mungkin tidak digunakan sama sekali.

  3. Ganti properti IsInvalidateddengan metode IsInvalidated():

    bool IsInvalidated();

    Ini mungkin perubahan yang terlalu halus. Seharusnya petunjuk bahwa nilai dihitung baru setiap kali - yang tidak perlu jika itu adalah konstanta. Topik MSDN "Memilih Antara Properti dan Metode" memiliki ini untuk mengatakan tentang hal itu:

    Gunakan metode, daripada properti, dalam situasi berikut. […] Operasi mengembalikan hasil yang berbeda setiap kali dipanggil, bahkan jika parameternya tidak berubah.

Jawaban apa yang saya harapkan?

  • Saya paling tertarik dengan solusi yang sama sekali berbeda untuk masalah ini, bersama dengan penjelasan bagaimana mereka mengalahkan upaya saya di atas.

  • Jika upaya saya secara logika cacat atau memiliki kerugian yang signifikan belum disebutkan, sehingga hanya satu solusi (atau tidak ada) yang tersisa, saya ingin mendengar tentang kesalahan saya.

    Jika kekurangannya kecil, dan lebih dari satu solusi tetap setelah dipertimbangkan, silakan komentar.

  • Paling tidak, saya ingin umpan balik yang merupakan solusi pilihan Anda, dan untuk alasan apa.

stakx
sumber
Tidak IsInvalidatedperlu private set?
James
2
@ James: Tidak harus, baik dalam deklarasi antarmuka, atau dalam implementasi:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx
Properti @James di antarmuka tidak sama dengan properti otomatis, meskipun mereka menggunakan sintaks yang sama.
svick
Saya bertanya-tanya: apakah antarmuka "memiliki kekuatan" untuk memaksakan perilaku seperti itu? Tidakkah perilaku ini harus didelegasikan ke setiap implementasi? Saya berpikir bahwa jika Anda ingin menegakkan hal-hal ke tingkat itu, Anda harus mempertimbangkan untuk membuat kelas abstrak sehingga subtipe akan mewarisi darinya, jika tidak menerapkan antarmuka yang diberikan. (
Omong
@heltonbiker Sayangnya, sebagian besar bahasa (saya tahu) tidak mengizinkan untuk mengekspresikan batasan pada jenis, tetapi mereka pasti harus . Ini adalah masalah spesifikasi, bukan implementasi. Dalam opition saya, yang hilang adalah kemungkinan untuk mengekspresikan invarian kelas dan post-kondisi langsung dalam bahasa. Idealnya, kita seharusnya tidak perlu khawatir tentang InvalidStateExceptions, misalnya, tetapi saya tidak yakin apakah ini secara teori mungkin. Akan menyenangkan.
proskor

Jawaban:

2

Saya lebih suka solusi 3 daripada 1 dan 2.

Masalah saya dengan solusi 1 adalah : apa yang terjadi jika tidak ada Invalidatemetode? Asumsikan sebuah antarmuka dengan IsValidproperti yang mengembalikan DateTime.Now > MyExpirationDate; Anda mungkin tidak memerlukan SetInvalidmetode eksplisit di sini. Bagaimana jika suatu metode memengaruhi banyak properti? Asumsikan tipe koneksi dengan IsOpendan IsConnectedproperti - keduanya dipengaruhi oleh Closemetode.

Solusi 2 : Jika satu-satunya tujuan acara ini adalah untuk memberi tahu pengembang bahwa properti dengan nama yang sama dapat mengembalikan nilai yang berbeda pada setiap panggilan, maka saya akan sangat menyarankan untuk tidak melakukannya. Anda harus menjaga antarmuka Anda singkat dan jelas; Selain itu, tidak semua implementasi mungkin dapat memecat acara itu untuk Anda. Saya akan menggunakan kembali IsValidcontoh saya dari atas: Anda harus menerapkan Timer dan menjalankan suatu peristiwa ketika Anda mencapai MyExpirationDate. Lagipula, jika acara itu adalah bagian dari antarmuka publik Anda, maka pengguna antarmuka akan mengharapkan acara itu berfungsi.

Yang dikatakan tentang solusi ini: mereka tidak buruk. Kehadiran metode atau peristiwa AKAN menunjukkan bahwa properti dengan nama yang sama dapat mengembalikan nilai yang berbeda pada setiap panggilan. Yang saya katakan adalah bahwa mereka sendiri tidak cukup untuk selalu menyampaikan makna itu.

Solusi 3 adalah tujuan saya. Seperti yang disebutkan aviv, ini hanya dapat bekerja untuk pengembang C #. Bagi saya sebagai C # -dev, fakta bahwa IsInvalidateditu bukan properti segera menyampaikan makna "itu bukan hanya pengakses sederhana, ada sesuatu yang terjadi di sini". Ini tidak berlaku untuk semua orang, dan seperti yang ditunjukkan MainMa, kerangka .NET itu sendiri tidak konsisten di sini.

Jika Anda ingin menggunakan solusi 3, saya akan merekomendasikan untuk menyatakannya sebagai konvensi dan minta seluruh tim mengikutinya. Dokumentasi juga penting, saya percaya. Menurut pendapat saya itu sebenarnya cukup sederhana untuk petunjuk pada mengubah nilai-nilai: " kembali benar jika nilai masih berlaku ", " menunjukkan apakah sambungan sudah dibuka " vs " kembali benar jika objek ini adalah valid ". Tidak ada ruginya sama sekali untuk menjadi lebih eksplisit dalam dokumentasi.

Jadi saran saya adalah:

Nyatakan solusi 3 sebuah konvensi dan ikuti secara konsisten. Jelaskan dalam dokumentasi properti apakah properti memiliki nilai yang berubah atau tidak. Pengembang yang bekerja dengan properti sederhana akan dengan benar menganggap mereka tidak berubah (yaitu hanya berubah ketika keadaan objek diubah). Pengembang menghadapi sebuah metode yang suara seperti itu bisa menjadi sebuah properti ( Count(), Length(), IsOpen()) tahu bahwa someting yang terjadi dan (mudah-mudahan) membaca metode dokumentasi untuk memahami apa sebenarnya metode yang dilakukan dan bagaimana berperilaku.

enzi
sumber
Pertanyaan ini telah menerima jawaban yang sangat bagus, dan sangat disayangkan saya hanya dapat menerima salah satunya. Saya telah memilih jawaban Anda karena memberikan ringkasan yang bagus dari beberapa poin yang muncul dalam jawaban lain, dan karena itu kurang lebih apa yang telah saya putuskan untuk dilakukan. Terima kasih kepada semua orang yang telah mengirim jawaban!
stakx
8

Ada solusi keempat: mengandalkan dokumentasi .

Bagaimana Anda tahu stringkelas itu tidak berubah? Anda hanya tahu itu, karena Anda sudah membaca dokumentasi MSDN. StringBuilder, di sisi lain, bisa berubah, karena, sekali lagi, dokumentasi memberi tahu Anda hal itu.

/// <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();
}

Solusi ketiga Anda tidak buruk, tetapi .NET Framework tidak mengikutinya . Sebagai contoh:

StringBuilder.Length
DateTime.Now

adalah properti, tetapi jangan berharap mereka tetap konstan setiap saat.

Apa yang secara konsisten digunakan di .NET Framework itu sendiri adalah ini:

IEnumerable<T>.Count()

adalah metode, sementara:

IList<T>.Length

adalah properti. Dalam kasus pertama, melakukan hal-hal mungkin memerlukan pekerjaan tambahan, termasuk, misalnya, menanyakan database. Pekerjaan tambahan ini mungkin memakan beberapa waktu. Dalam kasus properti, diperkirakan akan memakan waktu singkat : memang, panjangnya dapat berubah selama masa daftar, tetapi praktis tidak diperlukan apa pun untuk mengembalikannya.


Dengan kelas, hanya dengan melihat kode jelas menunjukkan bahwa nilai properti tidak akan berubah selama masa objek. Contohnya,

public class Product
{
    private readonly int price;

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

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

jelas: harga akan tetap sama.

Sayangnya, Anda tidak dapat menerapkan pola yang sama ke antarmuka, dan mengingat bahwa C # tidak cukup ekspresif dalam kasus tersebut, dokumentasi (termasuk diagram Dokumentasi XML dan UML) harus digunakan untuk menutup celah.

Arseni Mourzenko
sumber
Bahkan pedoman “tidak ada pekerjaan tambahan” tidak digunakan sepenuhnya secara konsisten. Misalnya, Lazy<T>.Valuemungkin perlu waktu yang sangat lama untuk menghitung (saat diakses untuk pertama kali).
svick
1
Menurut pendapat saya, tidak masalah jika .NET framework mengikuti konvensi solusi 3. Saya berharap pengembang cukup pintar untuk mengetahui apakah mereka bekerja dengan tipe in-house atau tipe framework. Pengembang yang tidak tahu yang tidak membaca dokumen dan dengan demikian solusi 4 juga tidak akan membantu. Jika solusi 3 diimplementasikan sebagai konvensi perusahaan / tim, maka saya percaya tidak masalah apakah API lain juga mengikutinya (walaupun tentu saja itu akan sangat dihargai).
enzi
4

2 sen saya:

Opsi 3: Sebagai non-C # -er, itu tidak akan menjadi petunjuk; Namun, menilai dari kutipan yang Anda bawa, harus jelas bagi para programmer C # yang mahir bahwa ini berarti sesuatu. Anda tetap harus menambahkan dokumen tentang ini.

Opsi 2: Kecuali Anda berencana untuk menambahkan acara ke setiap hal yang dapat berubah, jangan pergi ke sana.

Pilihan lain:

  • Mengganti nama antarmuka IXMutable, jadi jelas dapat mengubah kondisinya. Satu-satunya nilai yang tidak berubah dalam contoh Anda adalah id, yang biasanya dianggap tidak berubah bahkan dalam objek yang bisa berubah.
  • Tidak menyebutkannya. Antarmuka tidak sebaik menggambarkan perilaku seperti yang kita inginkan; Sebagian, itu karena bahasa tidak benar-benar membiarkan Anda menggambarkan hal-hal seperti "Metode ini akan selalu mengembalikan nilai yang sama untuk objek tertentu," atau "Memanggil metode ini dengan argumen x <0 akan menimbulkan pengecualian."
  • Kecuali bahwa ada mekanisme untuk memperluas bahasa dan memberikan informasi yang lebih tepat tentang konstruksi - Anotasi / Atribut . Mereka kadang-kadang membutuhkan beberapa pekerjaan, tetapi memungkinkan Anda untuk menentukan hal-hal rumit yang sewenang-wenang tentang metode Anda. Temukan atau temukan anotasi yang mengatakan "Nilai metode / properti ini dapat berubah sepanjang masa objek", dan menambahkannya ke metode. Anda mungkin perlu memainkan beberapa trik untuk mendapatkan metode implementasi untuk "mewarisi" itu, dan pengguna mungkin perlu membaca dokumen untuk anotasi itu, tetapi itu akan menjadi alat yang memungkinkan Anda mengatakan dengan tepat apa yang ingin Anda katakan, alih-alih mengisyaratkan itu
aviv
sumber
3

Pilihan lain adalah dengan membagi antarmuka: dengan asumsi bahwa setelah memanggil Invalidate()setiap doa IsInvalidatedharus mengembalikan nilai yang sama true, tampaknya tidak ada alasan untuk memanggil IsInvalidateduntuk bagian yang sama yang dipanggil Invalidate().

Jadi, saya sarankan, apakah Anda memeriksa tidak valid atau Anda menyebabkannya , tetapi tampaknya tidak masuk akal untuk melakukan keduanya. Oleh karena itu masuk akal untuk menawarkan satu antarmuka yang berisi Invalidate()operasi ke bagian pertama dan antarmuka lain untuk memeriksa ( IsInvalidated) ke yang lain. Karena cukup jelas dari tanda tangan antarmuka pertama yang akan menyebabkan instance menjadi tidak valid, pertanyaan yang tersisa (untuk antarmuka kedua) memang cukup umum: bagaimana menentukan apakah jenis yang diberikan tidak dapat diubah .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

Saya tahu, ini tidak menjawab pertanyaan secara langsung, tetapi setidaknya mengurangi pertanyaan tentang bagaimana menandai beberapa tipe yang tidak dapat diubah , yang merupakan masalah umum yang dapat dipahami. Misalnya, dengan mengasumsikan bahwa semua jenis bisa berubah kecuali ditentukan lain, Anda dapat menyimpulkan bahwa antarmuka kedua ( IsInvalidated) dapat berubah dan karena itu dapat mengubah nilai IsInvalidateddari waktu ke waktu. Di sisi lain, anggaplah antarmuka pertama tampak seperti ini (pseudo-sintaks):

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

Karena ditandai tidak berubah, Anda tahu bahwa ID tidak akan berubah. Tentu saja, panggilan yang tidak valid akan menyebabkan keadaan instance berubah, tetapi perubahan ini tidak akan terlihat melalui antarmuka ini.

proskor
sumber
Bukan ide yang buruk! Namun, saya berpendapat bahwa salah untuk menandai antarmuka [Immutable]selama itu mencakup metode yang tujuan utamanya adalah menyebabkan mutasi. Saya akan memindahkan Invalidate()metode ke Invalidatableantarmuka.
stakx
1
@stakx saya tidak setuju. :) Saya pikir Anda bingung konsep kekekalan dan kemurnian (tidak adanya efek samping). Bahkan, dari sudut pandang antarmuka IX yang diusulkan, tidak ada mutasi yang dapat diamati sama sekali. Setiap kali Anda menelepon Id, Anda akan mendapatkan nilai yang sama setiap kali. Hal yang sama berlaku untuk Invalidate()(Anda tidak mendapatkan apa-apa, karena jenis pengembaliannya adalah void). Oleh karena itu, jenisnya tidak berubah. Tentu saja, Anda akan memiliki efek samping , tetapi Anda tidak akan dapat mengamatinya melalui antarmuka IX, sehingga mereka tidak akan berdampak pada klien IX.
proskor
OK, kita bisa sepakat itu IXtidak berubah. Dan itu adalah efek samping; mereka hanya didistribusikan di antara dua antarmuka terpisah sehingga klien tidak perlu peduli (dalam hal IX) atau jelas apa efek sampingnya (dalam kasus Invalidatable). Tapi mengapa tidak pindah Invalidateke Invalidatable? Itu akan membuat IXkekal dan murni, dan Invalidatabletidak akan menjadi lebih bisa berubah, atau lebih tidak murni (karena keduanya sudah ada).
stakx
(Saya mengerti bahwa dalam skenario dunia nyata, sifat dari pemisahan antarmuka mungkin akan lebih bergantung pada kasus penggunaan praktis daripada pada masalah teknis, tapi saya mencoba untuk sepenuhnya memahami alasan Anda di sini.)
stakx
1
@stakx Pertama-tama, Anda bertanya tentang kemungkinan lebih lanjut untuk membuat semantik antarmuka Anda lebih eksplisit. Ide saya adalah untuk mengurangi masalah itu menjadi kekekalan, yang membutuhkan pemisahan antarmuka. Tetapi tidak hanya perpecahan yang diperlukan untuk membuat satu antarmuka tidak berubah, itu juga akan masuk akal, karena tidak ada alasan untuk memanggil keduanya Invalidate()dan IsInvalidatedoleh konsumen antarmuka yang sama . Jika Anda menelepon Invalidate(), Anda harus tahu itu menjadi tidak berlaku, jadi mengapa memeriksanya? Dengan demikian Anda dapat menyediakan dua antarmuka yang berbeda untuk tujuan yang berbeda.
proskor