Asla kamu üyelerini sanal / soyut yapmayın - gerçekten?


20

2000'li yıllarda bir meslektaşım bana kamu yöntemlerini sanal veya soyut yapmanın bir anti-desen olduğunu söyledi.

Örneğin, iyi tasarlanmamış böyle bir sınıfı düşündü:

public abstract class PublicAbstractOrVirtual
{
  public abstract void Method1(string argument);

  public virtual void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    // default implementation
  }
}

Dedi ki;

  • uygulanan Method1ve geçersiz kılan türetilmiş bir sınıfın geliştiricisi Method2, argüman doğrulamasını tekrarlamak zorundadır.
  • temel sınıf geliştiricisinin Method1veya Method2daha sonra özelleştirilebilir kısmına bir şey eklemeye karar vermesi durumunda bunu yapamaz.

Bunun yerine meslektaşım bu yaklaşımı önerdi:

public abstract class ProtectedAbstractOrVirtual
{
  public void Method1(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method1Core(argument);
  }

  public void Method2(string argument)
  {
    if (argument == null) throw new ArgumentNullException(nameof(argument));
    this.Method2Core(argument);
  }

  protected abstract void Method1Core(string argument);

  protected virtual void Method2Core(string argument)
  {
    // default implementation
  }
}

Bana kamusal yöntemleri (veya mülkleri) sanal veya soyut yapmanın alanları kamusal yapmak kadar kötü olduğunu söyledi. Alanları mülklere sararak, gerekirse daha sonra bu alanlara herhangi bir erişimi engelleyebilir. Aynı durum genel sanal / soyut üyeler için de geçerlidir: onları ProtectedAbstractOrVirtualsınıfta gösterildiği gibi sarmalamak temel sınıf geliştiricisinin sanal / soyut yöntemlere giden çağrıları durdurmasına izin verir.

Ama bunu bir tasarım kılavuzu olarak görmüyorum. Microsoft bile bunu izlemez: Bunu Streamdoğrulamak için sınıfa bir göz atın .

Bu rehber hakkında ne düşünüyorsun? Herhangi bir anlam ifade ediyor mu veya API'yı aşırı karmaşıklaştırdığını mı düşünüyorsunuz?


5
Yöntemler yapmak virtual isteğe bağlı geçersiz kılmaya izin verir. Yönteminiz büyük olasılıkla herkese açık olmalıdır, çünkü geçersiz kılmayabilir. Yöntemler yapmak abstract onları geçersiz kılmaya zorlar; muhtemelen olmalı protected, çünkü bir publicbağlamda özellikle yararlı değiller .
Robert Harvey

4
Aslında, protectedsoyut sınıfın özel üyelerini türetilmiş sınıflara maruz bırakmak istediğinizde en kullanışlıdır. Her durumda, özellikle arkadaşınızın görüşü hakkında endişelenmiyorum; özel durumunuz için en anlamlı erişim değiştiriciyi seçin.
Robert Harvey

4
İş arkadaşınız Şablon Yöntem Kalıbı'nı savunuyordu . İki yöntemin birbirine bağlı olmasına bağlı olarak, her iki yöntem için de kullanım örnekleri olabilir.
Greg Burghardt

8
@GregBurghardt: OP'nin meslektaşı , gerekli olup olmadığına bakılmaksızın her zaman şablon yöntemi desenini kullanmanızı önerdiği gibi geliyor . Bu tipik bir desen aşırı kullanımı - eğer bir çekiç varsa, er ya da geç her sorun çivi gibi görünmeye başlar ;-)
Doc Brown

3
@PeterPerot: Sadece kamuya açık alanları olan basit DTO'larla başlamak için hiçbir sorunum olmadı ve böyle bir DTO iş mantığı olan üyelere ihtiyaç duyduğunda, bunları özellikli sınıflara göre yeniden düzenledi. Bir kütüphane satıcısı olarak çalıştığında ve genel API'ları değiştirmemekle ilgilenmesi gerektiğinde, işler farklıdır, daha sonra ortak bir alanı aynı adda bir ortak mülke dönüştürmek bile sorunlara neden olabilir.
Doc Brown

Yanıtlar:


30

söz

Yöntem1'i uygulayan ve Yöntem2'nin geçersiz kılma işlemini yinelemesi gereken türetilmiş bir sınıfın geliştiricisi nedeniyle genel yöntemleri sanal veya soyut hale getirmenin bir anti-desen olduğunu

neden ve etkiyi karıştırıyor. Her geçersiz kılınabilen yöntemin özelleştirilemez bir bağımsız değişken doğrulaması gerektirdiği varsayımını yapar. Ama bu tam tersi:

Eğer (- Daha genel - veya özelleştirilebilir ve olmayan özelleştirilebilir parçası) bir bütün sınıfın sözcükler bazı sabit argüman doğrulamaları sağlayan bir şekilde bir yöntem tasarlamak istiyor sonra o giriş noktası olmayan sanal hale getirmek için mantıklı bunun yerine dahili olarak adlandırılan özelleştirilebilir bölüm için sanal veya soyut bir yöntem sağlayın.

Ancak, kişiselleştirilemeyen sabit bir parça olmadığından, genel bir sanal yönteme sahip olmanın mükemmel mantıklı olduğu birçok örnek vardır: ToStringveya gibi standart yöntemlere bakın Equalsveya GetHashCode- objectsınıfın halka açık olmaması için tasarımını geliştirir mi ve aynı anda sanal mı? Ben öyle düşünmüyorum.

Veya kendi kodunuz açısından: temel sınıftaki kod nihayet ve bilerek böyle göründüğünde

 public void Method1(string argument)
 {
    // nothing to validate here, all strings including null allowed
    this.Method1Core(argument);
 }

görünürde hiçbir neden olmaksızın bu ayrımı sadece bir şey arasında yapmak Method1ve Method1Corekarmaşıklaştırmak.


1
ToString()Yöntem durumunda , Microsoft bunu sanal olmayan hale getirmiş ve sanal bir şablon yöntemi sunmuştur ToStringCore(). Neden: bundan dolayı: ToString()- mirasçılara not . Bunun ToString()null döndürmemesi gerektiğini belirtirler . Bu talebi uygulayarak zorlayabilirlerdi ToString() => ToStringCore() ?? string.Empty.
Peter Perot

9
@PeterPerot: Bağlandığınız bu kılavuz da geri dönmemeyi önerir string.Empty, fark ettiniz mi? Ve bir ToStringCoreyöntem gibi bir şey getirerek kodda uygulanamayan diğer birçok şeyi önerir . Yani bu teknik muhtemelen doğru araç değildir ToString.
Doc Brown

3
@Theraot: ToString ve Equals veya GetHashcode'un neden farklı şekilde tasarlandığına dair bazı nedenler veya argümanlar bulabilir, ancak bugün oldukları gibi (en azından tasarımlarının iyi bir örnek oluşturmak için yeterince iyi olduğunu düşünüyorum).
Doc Brown

1
@PeterPerot "Bunun anyObjectIGot.ToString()yerine bir sürü kod gördüm anyObjectIGot?.ToString()" - bu nasıl alakalı? Sizin ToStringCore()boş bir dize önleyecek bir yaklaşım iade edilen, ama yine de atmak istiyorum NullReferenceExceptionnesnesi boş ise.
IMil

1
@PeterPerot Otoriteden bir argüman aktarmaya çalışmıyordum. Microsoft'un kullandığı çok şey değil public virtual, ancak iyi olduğu durumlar var public virtual. Kodun gelecekteki kanıtı haline getirilmesi için boş, özelleştirilemeyen bir parça olduğunu iddia edebiliriz ... Geri gitmek ve değiştirmek türetilmiş türleri bozabilir. Böylece hiçbir şey kazanmaz.
Theraot

6

Bunu meslektaşınızın önerdiği şekilde yapmak, temel sınıfın uygulayıcısı için daha fazla esneklik sağlar. Ancak bununla birlikte, varsayılan olarak varsayılan faydalarla doğrulanmayan daha karmaşıktır.

Temel sınıf uygulayıcısı için artan esnekliğin , öncelikli taraf için daha az esneklik pahasına olduğunu unutmayın . Özellikle umursamayacakları bazı dayatılan davranışlar kazanırlar. Onlara göre işler daha da katılaştı. Bu haklı ve yararlı olabilir, ancak bunların hepsi senaryoya bağlıdır.

Bunu uygulamak için adlandırma kuralı (bildiğim), genel arabirimin iyi adını ayırmak ve dahili yöntemin adını "Do" ile önek olarak kullanmaktır.

Yararlı bir durum, gerçekleştirilen eylemin bazı kurulumlara ve bazı kapanmalara ihtiyaç duymasıdır. Bir akışı açma ve overrider yapıldıktan sonra onu kapatma gibi. Genel olarak, aynı tür başlatma ve sonlandırma. Kullanmak için geçerli bir örüntüdür, ancak tüm soyut ve sanal senaryolarda kullanımını zorunlu kılmak anlamsız olacaktır.


1
Do yöntemi önek bir seçenektir. Microsoft genellikle Core yöntemi postfix'i kullanır .
Peter Perot

@Peter Perot. Herhangi bir Microsoft materyalinde Core önekini hiç görmedim ama bunun nedeni son zamanlarda fazla dikkat etmemem olabilir. Son zamanlarda bunu sadece Core takma adını tanıtmak, .NET Core için bir ad oluşturmak için yapmaya başladıklarından şüpheleniyorum.
Martin Maat

Hayır, bu eski bir şapka: BindingList. Ayrıca, öneriyi bir yerde buldum, belki de Çerçeve Tasarım Yönergeleri veya benzeri bir şey. Ve: bu bir postfix . ;-)
Peter Perot

Türetilmiş sınıf için daha az esneklik önemlidir. Temel sınıf bir soyutlama sınırıdır. Temel sınıf, tüketiciye genel API'sinin ne yaptığını söyler ve bu hedeflere ulaşmak için API'nın gerekli olduğunu tanımlar. Türetilmiş bir sınıf temel sınıftaki genel bir yöntemi geçersiz kılabilirse, Liskov İkame İlkesi'ni ihlal etme riski artar.
Adrian McCarthy

2

C ++ 'da buna sanal olmayan arayüz kalıbı (NVI) denir . (Bir zamanlar, Şablon Yöntemi olarak adlandırıldı. Bu kafa karıştırıcıydı, ancak bazı eski makalelerde bu terminoloji var.) NVI, en az birkaç kez yazmış olan Herb Sutter tarafından destekleniyor. Bence en erken biri burada .

Doğru hatırlamak, öncül bir türetilmiş sınıf değişmez gerektiğidir neyi taban sınıfı yapar ama nasıl bunu yapar.

Örneğin, bir Şeklin, şeklin yerini değiştirmek için bir Move yöntemi olabilir. Somut uygulamalar (örneğin, Kare ve Çemberler gibi) Taşı'yı doğrudan geçersiz kılmamalıdır; çünkü Şekil, Hareketin ne anlama geldiğini tanımlar (kavramsal düzeyde). Bir Kare, konumun dahili olarak nasıl temsil edildiği açısından bir Circle'dan farklı uygulama ayrıntılarına sahip olabilir, bu nedenle Taşıma işlevselliğini sağlamak için bazı yöntemleri geçersiz kılmaları gerekir.

Basit örneklerde, bu genellikle tüm işleri özel bir sanal ReallyDoTheMove'a devreten herkese açık bir hamle kadar kaybolur, bu yüzden fayda için çok fazla yük gibi görünür.

Ancak bu bire bir yazışma şart değildir. Örneğin, Shape'nin genel API'sına bir Animate yöntemi ekleyebilir ve bunu bir döngüde ReallyDoTheMove öğesini çağırarak uygulayabilirsiniz. Her ikisi de tek bir soyut soyut yönteme dayanan iki genel sanal olmayan yöntem API'siyle sonuçlanırsınız. Çevrelerinizin ve Karelerinizin fazladan bir iş yapmasına veya geçersiz kılma Animate'e gerek yoktur .

Temel sınıf, tüketiciler tarafından kullanılan genel arabirimi tanımlar ve bu genel yöntemleri uygulamak için ihtiyaç duyduğu ilkel işlemler arabirimini tanımlar. Elde edilen türler, bu ilkel işlemlerin uygulamalarını sağlamaktan sorumludur.

Sınıf tasarımının bu yönünü değiştirecek C # ve C ++ arasındaki farkların farkında değilim.


İyi bulmak! Şimdi hatırlıyorum, 2000'li yıllarda 2. bağlantı noktanızı (veya bir kopyasını) tam olarak bulduğumu hatırlıyorum. Meslektaşımın iddiasının daha fazla kanıtını aradığımı ve C # bağlamında bir şey bulamadığımı, ancak C ++ olduğunu hatırlıyorum. Bu. Dır-dir. O! :-) Ama, C # topraklarında, bu desen çok kullanılmıyor gibi görünüyor. Belki insanlar daha sonra temel işlevsellik eklemenin türetilmiş sınıfları da kırabileceğini ve TMP veya NVIP'in halka açık sanal yöntemler yerine sıkı bir şekilde kullanılmasının her zaman mantıklı olmadığını fark ettiler.
Peter Perot

Kendine not: bu modelin bir adı olduğunu bilmek güzel: NVIP.
Peter Perot
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.