Bir sınıf neden “soyut” veya “final / mühürlü” den başka bir şey olsun?


29

10+ yıllık java / c # programlamasından sonra kendimi yaratırken buluyorum:

  • soyut sınıflar : sözleşme olduğu gibi başlatılmak için tasarlanmamıştır.
  • son / mühürlü sınıflar : Uygulama, başka bir şeye temel sınıf olarak hizmet etme amaçlı değildir.

Basit bir "sınıf" ın (yani ne soyut ne de son / mühürlü) "akıllıca programlama" olacağı bir durum düşünemiyorum.

Bir sınıf neden "soyut" veya "final / mühürlü" den başka bir şey olsun?

DÜZENLE

Bu harika makale endişelerimi benden çok daha iyi açıklıyor.


21
Çünkü o denir Open/Closed principle, değilClosed Principle.
StuperUser 21:12

2
Ne tür şeyler profesyonelce yazıyorsunuz? Konuyla ilgili fikrinizi çok iyi etkileyebilir.

Her şeyi mühürleyen bazı platform geliştiriciler biliyorum, çünkü kalıtım arayüzünü en aza indirmek istiyorlar.
K ..

4
@StuperUser: Bu bir sebep değil, bir platitude. OP nezaketin ne olduğunu sormuyor, nedenini soruyor . Bir prensibin arkasında hiçbir sebep yoksa, buna dikkat etmek için hiçbir sebep yoktur.
Michael Shaw,

1
Window sınıfını mühürlü olarak değiştirerek kaç tane UI çerçevesinin kırılacağını merak ediyorum.
Tepki

Yanıtlar:


46

İronik olarak, bunun tam tersini buluyorum: soyut sınıfların kullanımı kuraldan ziyade istisnadır ve son / mühürlü sınıflarda kaşlarını çatma eğilimindeyim.

Arayüzler daha tipik bir tasarım sözleşmeli mekanizmadır, çünkü herhangi bir dahili belirtmezsiniz - onlar için endişelenmezsiniz. Bu sözleşmenin her uygulamasının bağımsız olmasını sağlar. Bu, birçok alanda anahtardır. Örneğin, bir ORM inşa ediyorsanız, veritabanına bir sorguyu tek tip bir şekilde iletmeniz çok önemlidir, ancak uygulamalar oldukça farklı olabilir. Bu amaç için soyut sınıflar kullanırsanız, tüm uygulamalara uygulanabilecek veya uygulanmayabilecek bileşenlere kablolama yaparsınız.

Son / mühürlü sınıflar için, onları kullanmak için görebildiğim tek bahane, geçersiz kılmaya izin vermenin gerçekten tehlikeli olduğu zaman olabilir - belki de bir şifreleme algoritması ya da başka bir şey. Bunun dışında, işlevselliği yerel nedenlerle ne zaman genişletmek isteyebileceğinizi asla bilemezsiniz. Bir sınıfı kapatmak, çoğu senaryoda bulunmayan kazançlar için seçeneklerinizi kısıtlar. Sınıflarınızı daha sonra genişletilebilecek şekilde yazmak çok daha esnektir.

Bu son görüş, benim için sınıfları mühürleyen 3. parti bileşenlerle çalışarak yaşamı çok kolaylaştıracak bazı entegrasyonları engelleyerek benimsendi.


17
Son paragraf için +1. Sık sık 3. parti kütüphanelerde (ya da standart kütüphane derslerinde!) Çok fazla kapsüllemeden daha büyük bir ağrı nedeni olamayacak kadar kapsülleme buldum.
Mason Wheeler

5
Sızdırmazlık sınıfları için olağan argüman, bir müşterinin sınıfınızı geçersiz kılabileceği birçok farklı yolu tahmin edemeyeceğinizdir ve bu nedenle davranışlarıyla ilgili hiçbir garanti veremezsiniz. Blogs.msdn.com/b/ericlippert/archive/2004/01/22/…
Robert Harvey

12
@RobertHarvey Bu tartışmaya aşinayım ve teoride kulağa harika geliyor. Kişilerin sınıfları uzatabilir nasıl forsee edemez nesne tasarımcısı olarak - onlar neden tam da bu değil mühürlenecek. Her şeyi destekleyemezsin - sorun değil. Yapma. Ama seçenekleri de ortadan kaldırmayın.
Michael

6
@RobertHarvey sadece LSP'yi kıranların hak ettiklerini elde etmelerini sağlamaz mı?
StuperUser

2
Ben de mühürlü sınıfların büyük bir hayranı değilim, ama neden Microsoft gibi bazı şirketlerin (sıklıkla kendilerini kırmadıkları şeyleri desteklemek zorunda kaldıklarını) çekici bulduklarını görebiliyorum. Bir çerçevenin kullanıcılarının mutlaka bir sınıfın iç bilgisini bilmesi gerekmez.
Robert Harvey,

11

Yani olan Eric Lippert tarafından büyük bir makalede, ama buna bakış açısı destekler sanmıyorum.

Başkaları tarafından kullanılmaya maruz kalan tüm sınıfların mühürlenmesi veya başka türlü genişletilemez hale getirilmesi gerektiğini savunuyor .

Sizin öncülünüz, tüm sınıfların soyut veya mühürlü olması gerektiğidir.

Büyük fark.

EL'in makalesi, sizin ve benim hakkında hiçbir şey bilmediğimiz, ekibinin ürettiği (muhtemelen pek çok) sınıf hakkında hiçbir şey söylemez. Genel olarak, bir çerçevedeki halka açık sınıflar, sadece bu çerçevenin uygulanmasında yer alan tüm sınıfların bir alt kümesidir.


Ancak aynı argümanı, ortak bir arayüzün parçası olmayan sınıflara da uygulayabilirsiniz. Bir sınıf finalini yapmanın temel nedenlerinden biri, özensiz kodu önlemek için güvenlik önlemi almasıdır. Bu argüman, zaman içinde farklı geliştiriciler tarafından paylaşılan herhangi bir kod temeli için ya da tek geliştirici olsanız bile geçerlidir. Sadece kodun anlamını korur.
DPM

Sınıfların soyut olması veya mühürlenmesi gerektiği fikrinin daha geniş bir ilkenin parçası olduğunu düşünüyorum. Bu tür bir kaçınma, tüm özel üyelerini miras almak zorunda kalmadan, belirli bir tip tüketiciler tarafından kullanılabilecek bir tür yaratmayı mümkün kılacaktır. Ne yazık ki, bu tür tasarımlar gerçekten public-constructor sözdizimi ile çalışmaz. Bunun yerine, kodun bunun new List<Foo>()gibi bir şeyle değiştirilmesi gerekir List<Foo>.CreateMutable().
supercat

5

Java açısından son sınıfların göründüğü kadar akıllı olmadığını düşünüyorum.

Birçok araç (özellikle AOP, JPA vb.) Yükleme süresiyle çalışır, böylece iplerinizi uzatmak zorunda kalırlar. Başka bir yol da delegeler oluşturmaktır (.NET olanlar değil), her şeyi orijinal sınıfa devrederek, kullanıcı sınıflarını genişletmekten çok daha karışık olacaktı.


5

Vanilyaya ihtiyacınız olacak iki yaygın vaka , mühürlü olmayan sınıflar:

  1. Teknik: İki seviyeden daha derin bir hiyerarşiniz varsa ve ortadaki bir şeyi başlatabilmek istiyorsanız.

  2. İlke: Bazen, güvenli bir şekilde genişletilmek üzere tasarlanan açıklık sınıfları yazmak istenebilir . (Bu, Eric Lippert gibi bir API yazarken veya büyük bir projede bir takımda çalışırken) çok olur. Bazen, kendi başına iyi çalışan bir sınıf yazmak istiyorsunuz, ancak akılda genişletilebilirlikle tasarlanmıştır.

Mühürleme markaların Eric Lippert düşünceleri bariz ama o da onlar kabul ediyor bunu "açık" sınıfı bırakarak genişletilebilirlik için tasarım.

Evet, birçok sınıf BCL'de mühürlenmiştir, ancak çok sayıda sınıf değildir ve her türlü harika yolla genişletilebilir. Akla gelen bir örnek, neredeyse her türlü Controlkalıtım yoluyla veri veya davranış ekleyebileceğiniz Windows Formları'dır . Elbette, bu başka yollarla da yapılabilirdi (dekoratör deseni, çeşitli kompozisyon türleri, vb.), Ancak kalıtım da çok iyi sonuç verir.

İki .NET özel not:

  1. Çoğu durumda, sızdırmazlık sınıfları güvenlik için çoğu zaman kritik değildir, çünkü mirasçılar virtualaçık arayüz uygulamaları hariç, işlevsel olmayan durumunuzu karıştırmazlar .
  2. Bazen uygun bir alternatif, internalYapıcıyı kod üssünüz içinde miras almanıza izin veren, ancak bunun dışında olmayan bir sınıfa mühürlemek yerine yapmaktır .

4

Ben birçok kişi sınıfları olmalıdır hissediyorum gerçek nedeni inanıyoruz final/ sealedo en olmayan soyut uzatılabilir sınıflar vardır olduğunu değil düzgün belgelenmiş .

Ayrıntılı çalışmama izin verin. Uzaktan başlayarak, bazı programcılar arasında, OOP’daki bir araç olarak kalıtımın çok fazla kullanıldığı ve kötüye kullanıldığı görüşü var. Liskov ikame prensibini hepimiz okuduk, ancak bu bizi yüzlerce (belki de binlerce) kez ihlal etmemize engel değildi.

Mesele şu ki, programcılar kodu tekrar kullanmayı seviyor. Bu iyi bir fikir olmasa bile. Ve miras, kodun bu "yeniden düzenlenmesinde" önemli bir araçtır. Son / kapalı soruya dönüş.

Son / mühürlü bir sınıf için uygun belgeler göreceli olarak küçüktür: her yöntemin ne yaptığını, argümanların ne olduğunu, dönüş değerini vb. Açıklar. Tüm olağan şeyler.

Ancak, genişletilebilir bir sınıfı düzgün şekilde belgeliyorsanız , en azından aşağıdakileri içermelisiniz :

  • Metotlar arasındaki bağımlılıklar (hangi metodu hangisi, vs. olarak adlandırır)
  • Yerel değişkenlere bağımlılıklar
  • Genişleyen sınıfın onurlandırması gereken iç sözleşmeler
  • Her bir yöntem için kongre çağırın (ör. Geçersiz kıldığınızda, superuygulamayı mu çağırıyorsunuz? Yöntemin başlangıcında mı yoksa sonunda mı?
  • ...

Bunlar sadece kafamın üstünde. Ve bunların her birinin neden önemli olduğunu ve onu atlamanın genişleyen bir sınıfı mahvedeceğini gösteren bir örnek verebilirim.

Şimdi, bunların her birini doğru bir şekilde belgelemek için ne kadar belgelendirme çalışmasının yapılması gerektiğini düşünün. 7-8 metotlı bir sınıfın (idealize edilmiş bir dünyada çok fazla olabilir, ancak gerçekte çok az) bir sınıfının sadece metinler içeren 5 sayfalık bir dokümantasyonunun olabileceğine inanıyorum. Bu yüzden, bunun yerine, yarıya doğru eğiliyoruz ve sınıfı mühürlemiyoruz, böylece diğer insanlar da kullanabilir, ancak çok fazla zaman alacağı için düzgün bir şekilde belgelendirmeyin. Zaten asla uzatılmadı, peki neden rahatsız ediyorsun?

Bir sınıf tasarlıyorsanız, insanların öngörmediğiniz (ve hazırlıklı oldukları) bir şekilde kullanamayacakları şekilde mühürlemenin cazibesini hissedebilirsiniz. Öte yandan, başka birinin kodunu kullanırken, bazen genel API’den, sınıfın kesin olması için gözle görülür bir neden yoktur ve “Kahretsin, bu sadece bir geçici çözüm arayışı için 30 dakikaya mal oldu” düşünebilirsiniz.

Bence çözümün bazı unsurları:

  • Öncelikle , kodun müşterisiyseniz uzatmanın iyi bir fikir olması ve mirasa ilişkin kompozisyonun gerçekten tercih edilmesidir.
  • İkinci olarak , belirtilen bir şeyi görmezden gelmediğinizden emin olmak için kılavuzu tamamen (yine bir müşteri olarak) okumak için .
  • Üçüncüsü, müşterinin kullanacağı bir kod parçasını yazarken, kod için uygun belgeleri yazın (evet, uzun yol). Olumlu bir örnek olarak, Apple'ın iOS belgelerini verebilirim. Kullanıcının sınıflarını her zaman uygun bir şekilde genişletmesi için yeterli değillerdir, ancak en azından devralma hakkında bazı bilgiler içerirler . Çoğu API için söyleyebileceğimden daha fazlası.
  • Dördüncüsü, aslında çalıştığından emin olmak için kendi sınıfını genişletmeye çalış. API'lere birçok örnek ve test dahil etmenin büyük bir destekçisiyim ve bir test yaparken miras zincirlerini de test edebilirsiniz: sonuçta sözleşmenizin bir parçası!
  • Beşinci, şüphe duyduğunuz durumlarda, sınıfın uzatılmayacağını ve bunu yapmanın kötü bir fikir (tm) olduğunu belirtin. Bu tür istenmeyen kullanımlardan sorumlu tutulmamalısınız, ancak yine de sınıfı mühürlemeyin. Açıkçası, bu sınıfın% 100 mühürlü olması gereken durumları kapsamıyor.
  • Son olarak, bir sınıfı mühürlerken, ara bir kanca olarak bir arabirim sağlayın, böylece müşteri sınıfın kendi değiştirilmiş halini "yeniden yazabilir" ve "mühürlü" sınıfınızın etrafında çalışabilir. Bu şekilde mühürlü sınıfı yerine uygulayabilir. Şimdi, bu açık olmalı, çünkü en basit şekliyle gevşek bağlantı oluşturuyor, ancak yine de söz edilmeye değer.

Aşağıdaki "felsefi" sorusunu bahsetmek fayda var: Bir sınıf olup olmadığını mı sealed/ finalsınıf veya bir uygulama ayrıntı için sözleşmenin parçası? Şimdi oraya basmak istemiyorum, ancak bunun cevabı bir sınıfı mühürleyip mühürlememe konusundaki kararınızı da etkilemelidir.


3

Bir sınıf ne son / mühürlü ne de soyut olmamalıdır:

  • Kendi başına işe yarar, yani o sınıfın örneklerine sahip olmak faydalıdır.
  • Bu sınıfın diğer sınıfların alt sınıf / temel sınıf olması faydalıdır.

Örneğin, ObservableCollection<T>sınıfı C # ile alın . Sadece olayların yükselişini a'nın normal operasyonlarına eklemek gerekiyor Collection<T>, bu yüzden alt sınıflarını çıkardı Collection<T>. Collection<T>kendi başına uygun bir sınıftır ve bu yüzden ObservableCollection<T>.


Eğer bundan sorumlu olsaydım, muhtemelen CollectionBase(soyut), Collection : CollectionBase(mühürlü), ObservableCollection : CollectionBase(mühürlü) yapacağım . <T> Koleksiyonuna yakından bakarsanız, bunun yarı asser soyut bir sınıf olduğunu göreceksiniz.
Nicolas Repiquet 21:12

2
Sadece iki yerine üç sınıfa sahip olmanın avantajı nedir? Ayrıca, Collection<T>bir "yarı katlı soyut sınıf" nasıl?
FishBasketGordo 21:12

Collection<T>Korunan yöntemler ve özellikler yoluyla birçok doğanın içini açığa çıkarır ve özel bir koleksiyon için temel bir sınıf olduğu açıkça ifade edilir. Ancak uygulayacağınız soyut yöntemler olmadığı için kodunuzu nereye koymanız gerektiği açık değildir. ObservableCollectionkendinden devralır Collectionve mühürlü değildir, bu yüzden yine devralabilirsiniz. Ve Erişebildiğiniz Itemssen koleksiyonunda öğeler eklemek için izin korumalı özelliği olmadan olayları yükselterek ... Nice'i.
Nicolas Repiquet 21:12

@NicolasRepiquet: Birçok dilde ve çerçevede, bir nesneyi oluşturmanın normal deyimsel aracı, nesneyi tutacak değişkenin türünün, oluşturulan örneğin türüyle aynı olmasını gerektirir. Çoğu durumda, ideal kullanım soyut bir türe referansları aktarmak olabilir, ancak bu, yapıcıları çağırırken değişkenler ve parametreler için bir tür ve farklı bir beton türü kullanmaya çok fazla kod zorlar. Neredeyse imkansız, ama biraz garip.
Süperkat

3

Son / mühürlü sınıfların sorunu, henüz gerçekleşmeyen bir sorunu çözmeye çalıştıklarıdır. Yalnızca sorun olduğunda kullanışlıdır, ancak bir üçüncü taraf bir kısıtlama getirdiğinden sinir bozucu bir durumdur. Sızdırmazlık sınıfı nadiren güncel bir problemi çözer ve bu da faydalı olduğunu iddia etmeyi zorlaştırır.

Bir sınıfın mühürlenmesi gereken durumlar vardır. Örnek olarak; Sınıf, tahsis edilmiş kaynakları / hafızayı, gelecekteki değişikliklerin bu yönetimi nasıl değiştirebileceğini tahmin edemeyeceği şekilde yönetir.

Yıllar boyunca enkapsülasyon, geri aramalar ve olayları soyutlanmış sınıflardan çok daha esnek / kullanışlı buldum. Kapsülleme ve olayların geliştirici için hayatı daha kolay hale getireceği çok geniş bir sınıf hiyerarşisine sahip kodları görüyorum.


1
Yazılımdaki son mühürlenmiş şeyler “bu kodun gelecekteki koruyucularına irademizi uygulama gereğini hissediyorum” sorununu çözmektedir.
Kaz

OOP'a eklenen bir şeyi sonlandırma / mühürleme değildi, çünkü daha küçükken etrafta olduğunu hatırlamıyorum. Düşünce sonrası bir özellik gibi görünüyor.
Tepki

Sonlandırma, OOP C ++ ve Java gibi dillerle kapatılmadan önce OOP sistemlerinde bir araç zinciri prosedürü olarak mevcuttu. Programcılar, Smalltalk, Lisp'te maksimum esneklikle çalışır: her şey genişletilebilir, her zaman yeni yöntemler eklenir. Daha sonra, sistemin derlenmiş görüntüsü bir optimizasyona tabidir: sistemin son kullanıcılar tarafından genişletilmeyeceği varsayımıyla yapılır ve bu, hangi yöntemlerin stoklarının alınmasına dayanarak metot gönderiminin optimize edilebileceği anlamına gelir. sınıflar şimdi var .
Kaz

Aynı şey olduğunu sanmıyorum, çünkü bu sadece bir optimizasyon özelliği. Java'nın tüm sürümlerinde mühürlü olduğumu hatırlamıyorum, ancak fazla kullanmadığım için yanılıyor olabilirim.
Tepki

Sırf koda girmeniz gereken bazı bildirimler olarak kendini göstermesi aynı şey olmadığı anlamına gelmez.
Kaz

2

Nesne sistemlerinde "sızdırmazlık" veya "sonlandırma", tüm gönderme grafiğinin bilindiği için belirli optimizasyonlara izin verir.

Başka bir deyişle, performansın değişmesi için sistemin başka bir şeyle değiştirilmesini zorlaştırıyoruz. (Bu, çoğu optimizasyonun özüdür.)

Diğer her bakımdan, bu bir kayıp. Sistemler varsayılan olarak açık ve genişletilebilir olmalıdır. Tüm sınıflara yeni yöntemler eklemek ve keyfi bir şekilde genişletmek kolay olmalı.

Gelecekteki uzamayı önlemek için adımlar atarak bugün yeni bir işlevsellik kazanmıyoruz.

Öyleyse önleme adına biz yaparsak, yaptığımız şey gelecekteki koruyucuların hayatlarını kontrol etmeye çalışmaktır. Bu ego ile ilgilidir. “Artık burada çalışmadığım zamanlarda bile, bu kod benim yöntemimle korunacak, kahretsin!”


1

Test dersleri akla geliyor gibi görünüyor. Bunlar, programcının / test edicinin başarmaya çalıştığı şeye göre otomatik olarak veya “isteğinde” olarak adlandırılan sınıflardır. Özel bir test bitmiş sınıfını gördüğümden veya duyduğuma emin değilim.


Sen neden bahsediyorsun Test sınıfları hangi şekilde nihai / mühürlü / soyut olmamalıdır?

1

Uzatılması amaçlanan bir sınıfı göz önünde bulundurmanız gereken zaman gelecek için gerçek bir planlama yaptığınız zamandır. İşimden size gerçek bir hayat örneği vereyim.

Ana ürünümüz ile dış sistemler arasında arayüz araçları yazmaya zamanımın çoğunu harcıyorum. Yeni bir satış yaptığımız zaman, büyük bir bileşen, o gün gerçekleşen olayları ayrıntılandıran veri dosyaları üreten düzenli aralıklarla çalıştırılmak üzere tasarlanmış bir dizi ihracatçıdır. Bu veri dosyaları daha sonra müşterinin sistemi tarafından tüketilir.

Bu, dersleri genişletmek için mükemmel bir fırsat.

Her ihracatçının temel sınıfı olan bir İhracat sınıfım var. Veritabanına nasıl bağlanacağını, en son ne zaman koştuğunu öğrendiğini ve ürettiği veri dosyalarının arşivlerini oluşturduğunu bilir. Ayrıca özellik dosyası yönetimi, günlük kaydı ve bazı basit istisnaların ele alınmasını sağlar.

Bunun üzerine, her veri türü ile çalışacak farklı bir ihracatçı var, belki de kullanıcı etkinliği, işlem verileri, nakit yönetimi verileri vb.

Bu yığının üstüne, müşterinin ihtiyacı olan veri dosyası yapısını uygulayan müşteriye özel bir katman yerleştiriyorum.

Bu şekilde, baz ihracatçısı nadiren değişir. Temel veri türü ihracatçıları bazen değişmekle birlikte, nadiren ve genellikle de yalnızca tüm müşterilere yayılması gereken veritabanı şeması değişikliklerini işlemek için kullanılır. Her müşteri için yapmam gereken tek iş, o müşteriye özgü kodun bir parçası. Mükemmel bir dünya!

Böylece yapı şöyle görünür:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

Öncelikli noktam, kodu bu şekilde yaparak, kalıtımdan öncelikle kodun yeniden kullanımı için yararlanabileceğim.

Söylemeliyim ki üç kattan geçmek için herhangi bir sebep düşünemiyorum.

İki kat kullandım, örneğin, Tableveritabanı tablo sorgularını uygulayan ortak bir sınıfa sahipken Table, her tablonun belirli ayrıntılarını bir enumalanları tanımlamak için uygularken ortak bir sınıfa sahip olmak için kullandım . Letting enumtanımlanan bir arabirim uygulamak Tablesınıfın anlamda her türlü yapar.


1

Soyut sınıfları faydalı buldum ama her zaman gerekli olmadı ve mühürlü sınıflar birim testleri uygulanırken bir sorun haline geldi. Telerik’lerin hakimi gibi bir şey kullanmazsan, mühürlü bir sınıfla alay edemezsin.


1
Bu iyi bir yorum, ancak soruyu doğrudan cevaplamıyor. Lütfen düşüncelerinizi burada genişletmeyi düşünün.

1

Görüşünle aynı fikirdeyim. Java'da varsayılan olarak, sınıfların "final" olarak bildirilmesi gerektiğini düşünüyorum. Son olarak yapmazsanız, özellikle hazırlayın ve uzatma için belgelendirin.

Bunun temel nedeni, sınıflarınızın herhangi bir örneğinin, özgün olarak tasarladığınız ve belgelendiğiniz arayüze uymasını sağlamaktır. Aksi halde, sınıflarınızı kullanan bir geliştirici potansiyel olarak kırılgan ve tutarsız kodlar oluşturabilir ve sırayla diğer geliştiricilere / projelere aktararak sınıfınızın nesnelerini güvenilir hale getiremez.

Daha pratik bir bakış açısına göre, bunun bir dezavantajı var, çünkü kütüphanenizin arayüzünün müşterileri herhangi bir ince ayar yapamayacak ve sınıflarınızı orijinal olarak düşündüğünüz daha esnek şekillerde kullanamayacaklar.

Şahsen, var olan tüm kötü kalite kodları için (ve bunu daha pratik bir düzeyde tartışıyoruz, çünkü Java gelişiminin buna daha eğilimli olduğunu düşünüyorum) Bu katılığın daha kolay olması için arayışımızda ödemek için küçük bir fiyat olduğunu düşünüyorum. kodunu koru.

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.