Bir arabirimi, hangi özelliklerin değerlerini değiştirebileceği ve hangilerinin sabit kalacağı açık olacak şekilde nasıl tasarlayabilirim?


12

.NET özellikleri ile ilgili bir tasarım sorunu yaşıyorum.

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

Sorun:

Bu arabirimin iki salt okunur özelliği vardır Idve IsInvalidated. Bununla birlikte, salt okunur olmaları, kendi değerlerinin sabit kalacağının garantisi değildir.

Diyelim ki çok açıklamak niyetim bu…

  • Id sabit bir değeri temsil eder (bu nedenle güvenli bir şekilde önbelleğe alınabilir)
  • IsInvalidatedbir IXnesnenin ömrü boyunca değerini değiştirebilir (ve önbelleğe alınmamalıdır).

interface IXBu sözleşmeyi yeterince açık hale getirmek için nasıl değişiklik yapabilirim?

Bir çözüm için kendi üç girişimim:

  1. Arayüz zaten iyi tasarlanmış. Denilen bir yöntemin varlığı, bir Invalidate()programcının benzer şekilde adlandırılmış özelliğin değerinin IsInvalidatedbundan etkilenebileceğini anlamasını sağlar .

    Bu argüman yalnızca yöntem ve özelliğin benzer şekilde adlandırıldığı durumlarda geçerlidir.

  2. Bu arayüzü bir etkinlikle artırın IsInvalidatedChanged:

    bool IsInvalidated { get; }
    event EventHandler IsInvalidatedChanged;

    Bir …Changedetkinliğin varlığı, IsInvalidatedbu özelliğin değerini değiştirebileceğini belirtir ve benzer bir etkinliğin olmaması, Idsöz konusu özelliğin değerini değiştirmeyeceğine dair bir vaattir.

    Bu çözümü seviyorum, ancak hiç kullanılmayabilecek birçok ek şey var.

  3. Özelliği IsInvalidatedbir yöntemle değiştirin IsInvalidated():

    bool IsInvalidated();

    Bu çok ince bir değişiklik olabilir. Her seferinde bir değerin taze olarak hesaplandığına dair bir ipucu olması gerekiyordu - bu sabit olsaydı gerekli olmazdı. "Özellikler ve Yöntemler Arasında Seçim Yapma " başlıklı MSDN konulu bu konuda şunları söyler:

    Aşağıdaki durumlarda bir özellik yerine bir yöntem kullanın. […] Parametreler değişmese bile işlem her çağrıldığında farklı bir sonuç döndürür.

Ne tür cevaplar bekliyorum?

  • En çok soruna tamamen farklı çözümlerle ve yukarıdaki girişimlerimi nasıl yendiklerine dair bir açıklama ile ilgileniyorum.

  • Eğer teşebbüslerim mantıklı bir şekilde kusurluysa veya henüz değinilmeyen önemli dezavantajları varsa, tek bir çözüm (veya hiçbiri) kalmayacaksa, nerede yanlış yaptığımı duymak isterim.

    Kusurlar küçükse ve dikkate alındıktan sonra birden fazla çözüm kalırsa, lütfen yorum yapın.

  • En azından, tercih ettiğiniz çözüm ve hangi sebep (ler) için bazı geri bildirimler istiyorum.


IsInvalidatedİhtiyaç yok mu private set?
James

2
@James: Gerek arayüz bildiriminde ne de uygulamada:class Foo : IFoo { private bool isInvalidated; public bool IsInvalidated { get { return isInvalidated; } } public void Invalidate() { isInvalidated = true; } }
stakx

@James Arayüzlerdeki özellikler, aynı sözdizimini kullansalar bile otomatik özelliklerle aynı değildir.
svick

Merak ediyordum: Arayüzler böyle bir davranışı empoze edecek güce sahip mi? Bu davranış her uygulamaya devredilmemeli mi? Eğer işleri bu seviyeye zorlamak istiyorsanız, belirli bir arabirimi uygulamak yerine alt türlerin miras alması için soyut bir sınıf yaratmayı düşünmelisiniz. (bu arada, mantığım mantıklı mı?)
heltonbiker

@heltonbiker Ne yazık ki, çoğu dil (biliyorum) türler üzerindeki bu tür kısıtlamaları ifade etmesine izin vermez, ancak kesinlikle yapmalıdırlar . Bu bir şartname meselesidir, uygulama değil. Benim tercihimde eksik olan, sınıf değişmezlerini ve post-condition'u doğrudan dilde ifade etme olasılığıdır . İdeal olarak, InvalidStateExceptionörneğin asla endişelenmemeliyiz , ancak bunun teorik olarak mümkün olup olmadığından emin değilim. Olsa iyi olurdu.
proskor

Yanıtlar:


2

Çözelti 3'ü 1 ve 2 yerine tercih ederim.

Çözüm 1 ile ilgili sorunum : Invalidateyöntem yoksa ne olur ? Dönen bir IsValidözelliğe sahip bir arabirim olduğunu varsayalım DateTime.Now > MyExpirationDate; SetInvalidburada açık bir yönteme ihtiyacınız olmayabilir . Bir yöntem birden çok özelliği etkiliyorsa ne olur? IsOpenVe IsConnectedözelliklerine sahip bir bağlantı türü olduğunu varsayalım; her ikisi de Closeyöntemden etkilenir .

Çözüm 2 : Etkinliğin tek noktası, geliştiricilere benzer adlandırılmış bir özelliğin her çağrıda farklı değerler döndürebileceğini bildirmekse, buna karşı şiddetle tavsiye ediyorum. Arayüzlerinizi kısa ve net tutmalısınız; dahası, tüm uygulamalar sizin için bu olayı başlatamayabilir. IsValidYukarıdaki örneğimi tekrar kullanacağım : Bir Zamanlayıcı uygulamanız ve ulaştığınızda bir olayı başlatmanız gerekir MyExpirationDate. Sonuçta, bu olay genel arayüzünüzün bir parçasıysa, arayüz kullanıcıları o etkinliğin çalışmasını bekler.

Bu çözümler hakkında söylenenler: fena değiller. Bir yöntemin veya olayın varlığı, benzer şekilde adlandırılmış bir özelliğin her çağrıda farklı değerler döndüğünü gösterir. Söylediğim tek şey, bu anlamı her zaman ifade etmek için tek başlarına yeterli olmadıkları.

Çözüm 3 benim için ne istediğidir. Aviv'in de belirttiği gibi, bu sadece C # geliştiricileri için işe yarayabilir. Bana göre bir C # -dev, IsInvalidatedbir özellik olmayan gerçeği hemen "bu sadece basit bir erişimci değil, burada bir şeyler oluyor" anlamını taşır. Bu herkes için geçerli değildir ve MainMa'nın belirttiği gibi, .NET çerçevesinin kendisi burada tutarlı değildir.

Çözüm 3'ü kullanmak istiyorsanız, bunu bir kongre ilan etmenizi ve tüm ekibin izlemesini tavsiye ederim. Belgeleme de çok önemli. Bence aslında değerlerini değiştirerek de ipucu oldukça basit: " ise sıfırdan döner yine geçerli ", " bağlantı olup olmadığını gösterir zaten açılmış " e karşı " bu nesne true döner olduğunu geçerlidir ". Belgelerde daha açık olmak hiç de incinmez.

Benim tavsiyem şöyle olur:

Çözüm 3'ü bir sözleşme ilan edin ve sürekli uygulayın. Özelliklerin belgelerinde, bir özelliğin değişen değerlere sahip olup olmadığını açıkça belirtin. Basit özelliklerle çalışan geliştiriciler, bunların doğru bir şekilde değişmediğini varsayar (yani yalnızca nesne durumu değiştirildiğinde değişiklik yapar). Böyle sesler bir özellik olabileceğini bir yöntem karşılaşmak Geliştiriciler ( Count(), Length(), IsOpen()) o someting dönüyor ve (umarım) tam yöntem ne ve nasıl davranacağını anlamak için yöntem dokümanlar okumak biliyorum.


Bu soru çok iyi cevaplar aldı ve bunlardan sadece birini kabul edebildiğim üzücü. Cevabınızı seçtim çünkü diğer cevaplarda ortaya çıkan bazı noktaların iyi bir özetini sunuyor ve az ya da çok yapmaya karar verdim. Yanıt gönderen herkese teşekkürler!
stakx

8

Dördüncü bir çözüm var: belgelere güvenin .

stringSınıfın değişmez olduğunu nasıl anlarsınız ? Bunu biliyorsunuz, çünkü MSDN belgelerini okudunuz. StringBuilderÖte yandan, değişebilir, çünkü yine, belgeler size bunu söylüyor.

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

Üçüncü çözümünüz kötü değil, ancak .NET Framework bunu izlemiyor . Örneğin:

StringBuilder.Length
DateTime.Now

özelliklerdir, ancak her seferinde sabit kalmalarını beklemeyin.

.NET Framework'ün kendisinde sürekli olarak kullanılan şey şudur:

IEnumerable<T>.Count()

bir yöntemdir:

IList<T>.Length

bir özelliktir. İlk durumda, bir şeyler yapmak, örneğin veritabanını sorgulamak da dahil olmak üzere ek işler gerektirebilir. Bu ek çalışma biraz zaman alabilir . Bir mülk durumunda, kısa bir süre beklenir : gerçekten, listenin ömrü boyunca uzunluk değişebilir, ancak iade etmek neredeyse hiçbir şey gerektirmez.


Sınıflarda, yalnızca koda bakmak nesnenin ömrü boyunca bir özellik değerinin değişmeyeceğini açıkça gösterir. Örneğin,

public class Product
{
    private readonly int price;

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

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

açıktır: fiyat aynı kalacaktır.

Ne yazık ki, aynı kalıbı bir arayüze uygulayamazsınız ve bu durumda C #'ın yeterince etkileyici olmadığı göz önüne alındığında, boşluğu kapatmak için belgeler (XML Belgeleri ve UML diyagramları dahil) kullanılmalıdır.


“Ek çalışma yok” kılavuzu bile kesinlikle tutarlı bir şekilde kullanılmamaktadır. Örneğin, Lazy<T>.Valuehesaplanması çok uzun zaman alabilir (ilk kez erişildiğinde).
svick

1
Bence, .NET çerçevesinin çözüm 3 kuralına uyup uymadığı önemli değil. Geliştiricilerin şirket içi türlerle mi yoksa çerçeve türleriyle mi çalıştıklarını bilecek kadar akıllı olmalarını bekliyorum. Dokümanları okumadığını bilmeyen geliştiriciler ve böylece çözüm 4 de yardımcı olmaz. Çözüm 3 bir şirket / ekip sözleşmesi olarak uygulanırsa, diğer API'ların da takip edip etmediğinin önemli olmadığına inanıyorum (elbette çok takdir edilecektir).
enzi

4

2 sentim:

Seçenek 3: C # olmayan bir kullanıcı olarak, bu bir ipucu değildir; Ancak, getirdiğiniz alıntıya bakarak, bunun bir şey ifade ettiği konusunda uzman C # programcılarına açık olmalıdır. Yine de bu konuda dokümanlar eklemelisiniz.

Seçenek 2: Değişebilecek her şeye etkinlik eklemeyi planlamıyorsanız, oraya gitmeyin.

Diğer seçenekler:

  • IXMutable arabirimini yeniden adlandırdığından, durumunu değiştirebileceği açıktır. Örneğinizdeki değişmez tek değer id, değişebilir nesnelerde bile değişmez olduğu varsayılan değerdir .
  • Bahsetmiyorum. Arayüzler, davranışı tanımlamak istediğimiz kadar iyi tanımlamazlar; Kısmen bunun nedeni, dilin "Bu yöntem her zaman belirli bir nesne için aynı değeri döndürür" veya "Bu yöntemi x <0 bağımsız değişkeniyle çağırmak" bir istisna atar.
  • Dışında dili genişletmek ve yapılar hakkında daha kesin bilgi sağlamak için bir mekanizma var - Ek Açıklamalar / Nitelikler . Bazen biraz çalışmaya ihtiyaç duyarlar, ancak yöntemleriniz hakkında rastgele karmaşık şeyler belirtmenize izin verir. "Bu yöntemin / özelliğin değeri bir nesnenin ömrü boyunca değişebilir" yazan bir açıklama bulun ve bulun ve bunu yönteme ekleyin. Uygulama yöntemlerini "devralmak" için bazı hileler oynamanız gerekebilir ve kullanıcıların bu ek açıklama için dokümanı okuması gerekebilir, ancak ipucu vermek yerine tam olarak ne söylemek istediğinizi söylemenizi sağlayan bir araç olacaktır. ona.

3

Başka bir seçenek arabirimi ayırmak olacaktır: Invalidate()her çağrıyı çağırdıktan sonra IsInvalidatedaynı değeri döndürmesi gerektiğini varsayarsak, çağrılan aynı parçayı trueçağırmak IsInvalidatediçin hiçbir neden yoktur Invalidate().

Bu nedenle, ya geçersiz kılmayı kontrol etmenizi ya da buna neden olmanızı öneririm, ancak her ikisini de yapmak mantıksız görünüyor. Bu nedenle Invalidate(), ilk parçaya işlemi içeren bir arayüz ve diğerine kontrol etmek için başka bir arayüz sunmak mantıklıdır IsInvalidated. İlk arayüzün imzasından, örneğin geçersiz kılınmasına neden olacağı oldukça açık olduğu için, kalan soru (ikinci arayüz için) gerçekten oldukça geneldir: verilen türün değişmez olup olmadığı nasıl belirlenir .

interface Invalidatable
{
    bool IsInvalidated { get; }
}

Biliyorum, bu soruya doğrudan cevap vermiyor, ama en azından bunu , ortak ve anlaşılmış bir sorun olan sessiz bir tür değişmezliği nasıl işaretleyeceği sorusuna indiriyor . Örneğin, aksi belirtilmedikçe tüm türlerin değiştirilebilir olduğunu varsayarak, ikinci arabirimin ( IsInvalidated) değiştirilebilir olduğu ve bu nedenle IsInvalidatedzaman zaman değerini değiştirebileceğiniz sonucuna varabilirsiniz . Öte yandan, ilk arayüzün şu şekilde göründüğünü varsayalım (sözde-sözdizimi):

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

Değişmez olarak işaretlendiğinden , Kimliğin değişmeyeceğini biliyorsunuz . Elbette, geçersiz kılınması, örneğin durumunun değişmesine neden olur, ancak bu değişiklik bu arabirim üzerinden gözlemlenmez .


Fena fikir değil! Bununla birlikte, [Immutable]tek amacı mutasyona neden olan yöntemleri içerdiği sürece bir arayüzü işaretlemenin yanlış olduğunu iddia ediyorum . Ben hareket edeceğini Invalidate()yöntemini Invalidatablearayüzüne.
stakx

1
@stakx Kabul etmiyorum. :) Bence değişmezlik ve saflık (yan etkilerin yokluğu) kavramlarını karıştırıyorsunuz . Aslında, önerilen arayüz IX açısından hiçbir gözlemlenebilir mutasyon yoktur. Her aradığınızda Id, her seferinde aynı değeri elde edersiniz. Aynı şey Invalidate()(dönüş türü olduğu için hiçbir şey almazsınız) için de geçerlidir void. Bu nedenle, tip değişmezdir. Tabii ki, yan etkilere sahip olacaksınız , ancak bunları arayüz aracılığıyla gözlemleyemeyeceksinizIX , böylece müşterileri üzerinde herhangi bir etkisi olmayacak IX.
proskor

Tamam, bunun IXdeğişmez olduğunu kabul edebiliriz . Ve bunların yan etkiler olduğunu; sadece iki bölünmüş arabirim arasında dağıtılırlar, böylece istemciler bakım gerektirmez (durumunda IX) veya yan etkinin ne olabileceği açıktır (durumunda Invalidatable). Ama neden hareket etmiyor Invalidateyanına Invalidatable? Bu IXdeğişmez ve saf Invalidatablehale gelir ve daha değişmez veya daha saf olmazdı (çünkü bunların ikisi de zaten).
stakx

(Gerçek dünya senaryosunda, arayüz bölünmesinin doğasının muhtemelen teknik konulardan ziyade pratik kullanım durumlarına bağlı olacağını anlıyorum, ancak mantığınızı burada tam olarak anlamaya çalışıyorum.)
stakx

1
@stakx Her şeyden önce, arayüzünüzün anlambilimini bir şekilde daha açık hale getirmek için daha fazla olasılık sordunuz. Benim fikrimi problemi, arayüzün bölünmesi gereken değişmezlik problemine indirgemekti. Ama bir arayüz değişmez yapmak için gerekli bölünmüş aynı zamanda çünkü, büyük mantıklıdır oldu sadece ikisi çağırmak için hiçbir neden yoktur Invalidate()ve IsInvalidatedarayüzü aynı tüketici tarafından . Eğer ararsanız Invalidate(), geçersiz hale geldiğini bilmelisiniz, neden kontrol etmelisiniz? Böylece farklı amaçlar için iki ayrı arayüz sağlayabilirsiniz.
proskor
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.