Sınıflar neden “açık” olarak tasarlanmamalı?


44

Çeşitli Yığın Taşması sorularını ve başkalarının kodlarını okurken, sınıfların nasıl tasarlandığına dair genel fikir birliği kapanır. Bu, Java ve C # 'da varsayılan olarak her şeyin özel olduğu, alanların kesin, bazı yöntemlerin kesin ve bazen sınıfların nihai olduğu anlamına gelir .

Bunun arkasındaki fikir, uygulama ayrıntılarını gizlemektir, ki bu çok iyi bir nedendir. Ancak protected, çoğu OOP dilinde ve polimorfizmde var olduğu için bu işe yaramıyor.

Her ne zaman bir sınıfa işlevsellik eklemek veya değiştirmek istediğimde, genellikle her yere özel ve finalden dolayı engel oluyorum. Burada uygulama detayları önemlidir: Sonuçların ne olduğunu tam olarak bilerek uygulamayı uygulıyor ve genişletiyorsunuz. Ancak, özel ve son alanlara ve yöntemlere erişemediğim için üç seçeneğim var:

  • Sınıfı genişletmeyin, sadece daha karmaşık olan koda neden olan sorunu çözün
  • Tüm sınıfı kopyalayıp yapıştırın, yeniden kullanılabilir kod öldürme
  • Projeyi çatal

Bunlar iyi seçenek değil. Neden protectedonu destekleyen dillerde yazılmış projelerde kullanılmıyor? Neden bazı projeler sınıflarından miras almayı açıkça yasaklıyor?


1
Kabul ediyorum, Java Swing bileşenlerinde bu sorunla karşılaştım, bu gerçekten kötü.
Jonas


3
Bu problemi yaşıyorsanız, sınıfların en başta kötü bir şekilde tasarlanması - veya belki de onları yanlış kullanmaya çalışmanız iyi bir ihtimal. Bilgi almak için bir sınıfa sormazsınız, sizden bir şey yapmasını istersiniz - bu nedenle genellikle verisine ihtiyaç duymamanız gerekir. Bu her zaman doğru olmasa da, bir sınıf verisine erişmeniz gerektiğini fark ederseniz, bir şeyler ters gitti.
Bill K,

2
"çoğu OOP dili?" Sınıfların kapatılamadığı yerlerde daha çok şey biliyorum. Dördüncü seçeneği bıraktınız: dilleri değiştir.
kevin cline

@Jonas Swing'in en büyük sorunlarından biri, çok fazla uygulamayı ortaya çıkarmasıdır. Bu gerçekten gelişimi öldürüyor.
Tom Hawtin - tackline

Yanıtlar:


55

Sınıfları genişletildiğinde düzgün çalışacak şekilde tasarlamak, özellikle de genişletmeyi yapan programcı sınıfın nasıl çalışması gerektiğini tam olarak anlamadığında, fazladan çaba sarfedilir. Özel olan her şeyi alıp herkese açık (veya korumalı) yapamaz ve buna "açık" diyemezsin. Bir başkasının bir değişkenin değerini değiştirmesine izin verirseniz, olası tüm değerlerin sınıfı nasıl etkileyeceğini göz önünde bulundurmalısınız. (Değişkeni null değerine ayarlarlarsa boş bir dizi? Negatif sayı?) Diğer kişilerin bir yöntemi çağırmasına izin verirken de aynı şey geçerlidir. Dikkatli düşünür.

Bu yüzden sınıfların açık olmaması gerektiği kadar değil, ama bazen onları açık tutma çabasına değmez.

Tabii ki, aynı zamanda kütüphane yazarlarının sadece tembel olmaları da mümkün. Hangi kütüphaneden bahsettiğinize bağlı. :-)


13
Evet, bu yüzden sınıflar varsayılan olarak mühürlendi. Müşterilerinizin sınıfı genişletme yollarını tahmin edemediğiniz için, sınıfın genişletilebileceği sınırsız sayıda yolu desteklemeyi taahhüt etmekten daha güvenlidir.
Robert Harvey,


18
Sınıfınızın nasıl geçersiz kılınacağını tahmin etmek zorunda değilsiniz, sadece sınıfınızı genişleten kişilerin ne yaptıklarını bildiklerini varsaymanız gerekir. Sınıfı genişletip boş olmaları gerektiğinde null değerine ayarlarlarsa, onların hatası değil, sizin hatalarınız olur. Bu argümanın mantıklı geldiği tek yer, eğer bir alan boşsa, bir şeyin korkunç derecede yanlış gideceği süper kritik uygulamalardır.
TheLQ

5
@TheLQ: Bu oldukça büyük bir varsayım.
Robert Harvey,

9
@TheLQ Kimin suçu oldukça öznel bir sorudur. Bir değişkeni görünüşte makul bir değere ayarladılarsa ve buna izin verilmediğini belgelendirmediyseniz ve kodunuz bir ArgumentException (veya eşdeğeri) atmadı, ancak sunucunuzun çevrimdışı kalmasına veya yönlendirmelere yol açmasına neden oldu Bir güvenlik açığına, o zaman onların olduğu kadar senin hatan olduğunu söyleyebilirim. Ve eğer geçerli değerleri belgelemişseniz ve bir argüman kontrolü gerçekleştirdiyseniz, o zaman tam olarak bahsettiğim çabayı gösterdiniz ve bu çabanın gerçekten zamanınızı iyi kullanıp kullanmadığını kendinize sormalısınız.
Aaron,

24

Her şeyi varsayılan olarak özel kılmak zor geliyor, ama diğer taraftan bakın: Her şey varsayılan olarak özel olduğunda, halka açık (veya korunan, neredeyse aynı şeydir) bilinçli bir seçimdir; sınıf yazarının sizinle, tüketiciyle, sınıfın nasıl kullanılacağına dair sözleşmesidir. Bu ikiniz için de kolaylık sağlıyor: Yazar, arayüz değişmeden kaldığı sürece sınıfın iç çalışmalarını değiştirmekte özgür; ve sınıfın hangi kısımlarına güvenebileceğinizi ve hangilerinin değişebileceğini tam olarak biliyorsunuz.

Temel fikir 'gevşek bağlanma' (ayrıca 'dar arayüzler' olarak da bilinir); değeri karmaşıklığı azaltmada yatıyor. Bileşenlerin etkileşime girme yollarının sayısını azaltarak, aralarındaki çapraz bağımlılık miktarı da azalır; ve çapraz bağımlılık, bakım ve değişim yönetimi söz konusu olduğunda en kötü karmaşıklık türlerinden biridir.

İyi tasarlanmış kütüphanelerde, miras yoluyla yayılmaya değer sınıflar korunmuş ve kamu üyelerini doğru yerlerde korumuş ve diğer her şeyi gizlemiştir.


Bu biraz mantıklı geliyor. Çok benzer bir şeye ihtiyaç duyduğunuzda ancak uygulamada 1 veya 2 şey değiştiğinde (sınıfın iç işleri) hala sorun var. Ayrıca, sınıfı geçersiz kılıyorsanız, zaten uygulamasına çok fazla bağlı olmadığınızı anlamakta zorlanıyorum?
TheLQ

5
Gevşek bağlamanın yararları için +1. @TheLQ, uygulamaya bağlı değil, yoğun bir şekilde bağlı olmanız gereken arayüz .
Karl Bielefeldt

19

Özel olmayan her şeyin, sınıfın gelecekteki her versiyonunda değişmeyen davranışlarla var olduğu tahmin edilmektedir . Aslında, belgelenmiş olsun veya olmasın API'nin bir parçası olarak kabul edilebilir. Bu nedenle, çok fazla ayrıntı göstermek muhtemelen daha sonra uyumluluk sorunlarına neden olmaktadır.

"Nihai" ile ilgili olarak "mühürlü" sınıflar, tipik bir durum değişmez sınıflardır. Çerçevenin çoğu kısmı dizgelerin değişmez olmasına bağlıdır. String sınıfı nihai olmasaydı, değişmez String sınıfı yerine kullanıldığında diğer birçok sınıftaki her tür hataya neden olan değişken bir String alt sınıfı oluşturmak kolay olurdu (hatta baştan çıkarıcı).


4
Bu gerçek cevap. Diğer cevaplar önemli konulara değiniyor, ancak asıl önemli olan şudur: yerinde olmayan finalve / veya privatekilitlenmiş olan ve asla değiştirilemeyen her şey .
Konrad Rudolph

1
@Konrad: Geliştiriciler her şeyi yenilemeye ve geriye dönük olarak uyumlu olmaya karar verinceye kadar.
JAB

Bu doğru, ancak publicüyelere göre çok daha zayıf bir gereklilik olduğunu belirtmekte fayda var ; protectedüyeler, yalnızca kendilerini ortaya çıkaran sınıf ve derhal türetilmiş sınıfları arasında bir sözleşme oluşturur; aksine, publictüm mevcut ve gelecekteki miras sınıfları adına tüm tüketicilerle sözleşmeler için üyeler.
supercat

14

OO'da mevcut koda işlev eklemenin iki yolu vardır.

İlki kalıtım yoluyladır: bir sınıf alır ve ondan türetilirsiniz. Ancak, kalıtım dikkatli kullanılmalıdır. Eğer bir varken Sen başta kamu devralma kullanmalıdır ISA tabanı ve türetilmiş sınıf arasındaki ilişkiyi (örneğin bir Dikdörtgen bir olduğunu Şekli). Bunun yerine, mevcut bir uygulamayı yeniden kullanmak için halkın mirasından kaçınmalısınız. Temel olarak, genel miras, türetilmiş sınıfın temel sınıfla (veya daha büyük bir sınıf) aynı arayüze sahip olduğundan emin olmak için kullanılır, böylece Liskov'un ikame ilkesini uygulayabilirsiniz .

İşlevsellik eklemek için başka bir yöntem, temsilci seçme veya kompozisyon kullanmaktır. Varolan sınıfı iç nesne olarak kullanan kendi sınıfınızı yazarsınız ve uygulama çalışmasının bir bölümünü ona devredersiniz.

Kullanmaya çalıştığınız kütüphanede tüm bu finallerin arkasındaki fikir ne olduğundan emin değilim. Muhtemelen programcılar kendi sınıflarından yeni bir sınıfa geçmeyeceğinizden emin olmak istedi, çünkü kodlarını genişletmenin en iyi yolu bu değildi.


5
Mirasa karşı kompozisyon ile büyük nokta.
Zsolt Török

+1, ancak miras için "şekil bir" örneği sevmedim. Bir dikdörtgen ve bir daire iki farklı şey olabilir. Kullanmayı daha çok seviyorum, bir kare sınırlı bir dikdörtgen. Dikdörtgenler, Şekiller gibi kullanılabilir.
tylermac

@ tylermac: gerçekten. Kare / Dikdörtgen kasa aslında LSP'nin örneklerinden biridir. Muhtemelen, daha etkili bir örnek düşünmeye başlamalıyım.
Knulp

3
Kare / Dikdörtgen, değişiklik yapılmasına izin verirseniz yalnızca bir karşı örnektir.
starblue

11

Cevapların çoğunda hakka sahiptir: Nesne tasarlanmamış veya uzatılmak istenmediğinde sınıflar mühürlenmelidir (final, NotOverridable vb.).

Ancak, her şeyi doğru kod tasarımını kapatmayı düşünmezdim. SOLID'deki "O", bir sınıfın modifikasyona "kapalı", "uzantıya açık" olması gerektiğini belirten "Açık-Kapalı Prensip" içindir. Fikir şu ki, bir nesnede kod var. Sadece iyi çalışıyor. İşlevsellik eklemek, bu kodu açmayı ve önceden çalışma davranışını bozabilecek cerrahi değişiklikler yapmayı gerektirmemelidir. Bunun yerine, kişi, ek şeyler yapmak için sınıfı "genişletmek" için miras veya bağımlılık enjeksiyonunu kullanabilmelidir.

Örneğin, bir "ConsoleWriter" sınıfı metin alabilir ve konsola çıkartabilir. Bunu iyi yapıyor. Bununla birlikte, belirli bir durumda, bir dosyaya yazılmış aynı çıktıya ihtiyaç duyarsınız. ConsoleWriter kodunu açmak, ana arabirime bir parametre "WriteToFile" eklemek için dış arabirimini değiştirmek ve ek dosya yazma kodunu konsol yazma kodunun yanına yerleştirmek genellikle kötü bir şey olarak kabul edilir.

Bunun yerine, iki şeyden birini yapabilirsiniz: ConsoleAndFileWriter oluşturmak için ConsoleWriter'dan türetebilir ve ilk olarak temel uygulamayı çağırmak için Write () yöntemini genişletebilir ve ardından bir dosyaya yazabilirsiniz. Veya, bir arayüz IWriter'ı ConsoleWriter'dan çıkarabilir ve bu arayüzü iki yeni sınıf oluşturmak için yeniden uygulayabilirsiniz; Bir FileWriter ve diğer IWriter'lara verilebilecek ve kendilerine verilen tüm çağrıları kendi yöntemlerine yapılan çağrıları "yayınlayacak" bir "MultiWriter". Hangisini seçeceğiniz, neye ihtiyaç duyacağınıza bağlıdır; basit türetme, daha basit, ancak daha sonra bir ağ istemcisine, üç dosya, iki adlandırılmış yöneltme ve konsol adı verilen iletiyi göndermek için gereken bir karar varsa, devam et ve ara yüzü ayıkla "Y adaptör"; o'

Şimdi, eğer Write () işlevi hiç sanal olarak ilan edilmediyse (ya da mühürlendi), şimdi kaynak kodunu kontrol etmiyorsanız başınız belada. Bazen yaparsan bile. Bu genellikle içinde olmak için kötü bir konumdur ve kapalı kaynaklı veya sınırlı kaynaklı API'leri kullananları bu durum sona ermez. Ancak, Write () ya da tüm ConsoleWriter sınıfının mühürlenmesinin meşru bir nedeni vardır; Korunan bir alanda hassas bilgiler olabilir (bu bilgilerin sağlanması için bir temel sınıftan sırayla geçersiz kılınır). Doğrulama yetersiz olabilir; Bir api yazdığınızda, kodunuzu tüketen programcıların son sistemin ortalama "son kullanıcısı" ndan daha akıllı veya yardımsever olmadığını varsaymalısınız; bu şekilde asla hayal kırıklığına uğramadın.


İyi bir nokta. Kalıtım iyi veya kötü olabilir, bu yüzden her sınıfın mühürlenmesi gerektiğini söylemek yanlıştır. Bu gerçekten eldeki soruna bağlı.
knulp

2

Muhtemelen c programlama için bir oo dili kullanan tüm insanlardan olduğunu düşünüyorum. Sana bakıyorum Java. Çekiçiniz bir nesne gibi şekilleniyorsa ancak bir prosedür sistemi istiyorsanız ve / veya sınıfları gerçekten anlamıyorsanız, projenizin kilitlenmesi gerekir.

Temel olarak, bir oo dilinde yazacaksanız, projenizin uzantısını içeren oo paradigmasını izlemesi gerekir. Elbette, eğer biri farklı bir paradigmada bir kütüphane yazdıysa, belki de yine de genişletmemelisiniz.


7
Btw, oo ve prosedürel en empatical karşılıklı değildir. Prosedürler olmadan OO'ya sahip olamazsınız. Ayrıca, kompozisyon, uzantı kadar bir OO konseptidir.
Michael K

Tabii @Michael. Prosedürel bir sistem isteyebileceğinizi , yani OO kavramlarının bulunmadığını belirtmiştim. İnsanların Java'yı yalnızca prosedürel kodlar yazmak için kullandıklarını gördüm ve onunla bir OO tarzında etkileşime girerseniz bu işe yaramaz.
Spencer Rathbun

1
Java birinci sınıf işlevlere sahipse bu çözülebilirdi. Meh.
Michael K

Java veya DOTNet Languages'ı yeni öğrencilere öğretmek zor. Ben genellikle Prosedür Pascal veya "Düz C" ile Prosedürü öğretirim ve daha sonra, Obje Pascal veya "C ++" ile OO'ya geçer. Ve sonra Java için bırakın. Tek bir (singleton) nesnenin iyi çalıştığı gibi görünen bir prosedür programlarıyla programlama öğretimi.
umlcat

@Michael K: OOP'ta birinci sınıf bir fonksiyon, tam olarak tek bir metodu olan bir nesnedir.
Giorgio

2

Çünkü daha iyisini bilmiyorlar.

Orijinal yazarlar, muhtemelen karışık ve karmaşık bir C ++ dünyasında ortaya çıkan bir SOLID ilkesinin yanlış anlaşılmasına bağlı kalıyorlar.

Yakut, piton ve perl dünyalarının buradaki cevapların sızdırmazlık nedeni olduğunu iddia ettiği sorunlara sahip olmadığını fark edersiniz. Dinamik yazarak ortogonal olduğuna dikkat edin. Erişim düzenleyicilerin çoğu (tümü?) Dilde çalışması kolaydır. C ++ alanları başka bir türe atılarak doldurulabilir (C ++ daha zayıf). Java ve C #, yansıma kullanabilir. Erişim düzenleyicileri, GERÇEKTEN istemediğiniz sürece yapmanıza engel olacak şeyleri yeterince zorlaştırır.

Sınıfların kapatılması ve üyelerin özel olarak işaretlenmesi, basit şeylerin basit olması ve zor şeylerin mümkün olması ilkesini açıkça ihlal eder. Birdenbire basit olması gereken şeyler değil.

Özgün yazarların bakış açısını anlamaya çalışmanızı tavsiye ederim. Bunların çoğu, gerçek dünyada asla mutlak bir başarı gösteremeyen akademik bir kapsülleme fikrindendir. Bir yerdeki bazı geliştiricilerin biraz daha farklı çalışmasını istemediği ve değiştirmek için iyi nedenleri olmadığı bir çerçeve veya kütüphane görmedim. Üyeleri mühürleyen ve üyelere özel kılan orijinal yazılım geliştiricileri rahatsız eden iki olasılık vardır.

  1. Kibir - gerçekten uzatma için açık ve değişiklik için kapalı olduklarına inandılar
  2. Şikayet - başka kullanım vakaları olabileceğini biliyorlardı, ancak bu kullanım durumları için yazmamaya karar verdiler

Kurumsal çerçevede wold düşünüyorum, # 2 muhtemelen durum böyledir. Bu C ++, Java ve .NET çerçeveleri "yapılmalı" ve belli kurallara uymalıdırlar. Bu kurallar, özellikle, başkalarının kullanması için yararlı olabilecek pek çok şey için bir tür hiyerarşisinin ve özel üyelerin bir parçası olarak açıkça tasarlanmadığı sürece, genellikle mühürlü tipler anlamına gelir. . Yeni bir türün çıkarılması, desteklenmesi, belgelenmesi vb. İçin çok pahalı olacaktır.

Erişim değiştiricilerin arkasındaki bütün fikir, programcıların kendisinden korunmaları gerektiğidir. "C programlama kötüdür çünkü kendini ayağından vurmana izin verir." Programcı olarak aynı fikirde olduğum bir felsefe değil.

Python'un isim yönetimi yaklaşımını tercih ederim. İhtiyacınız olduğunda kolayca (yansımadan çok daha kolay) gizleri değiştirebilirsiniz. Burada harika bir yazı var: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Ruby'nin özel değiştiricisi aslında C # 'da korunan gibidir ve özel bir C # değiştiricisi değildir. Korumalı biraz farklı. Burada harika bir açıklama var: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

Unutmayın, statik dilin geçmişte kod yazdığınız eski stillere uyması gerekmiyor.


5
"kapsülleme asla mutlak başarı göstermedi". Herkesin kapsülleme eksikliğinin çok fazla soruna yol açtığı konusunda hemfikir olacağını düşünmüştüm.
Joh,

5
-1. Aptallar meleklerin basmaktan korktukları yerlere koşarlar. Özel değişkenleri değiştirme yeteneğini kutlamak, kodlama stilinizde ciddi bir kusur olduğunu gösterir.
riwalk,

2
Tartışılabilir ve muhtemelen yanlış olmasının yanı sıra, bu ilan da oldukça kibirli ve hakaret edicidir. Güzel bitti.
Konrad Rudolph

1
Adayları iyi geçinemiyor gibi görünen iki düşünce okulu var. Statik yazarak folklorik yazılım hakkında kolay anlaşılır olmasını, dinamik folklorel fonksiyonellik eklemesinin kolay olmasını istiyor. Hangisi daha iyi temelde projenin beklenen ömrüne bağlıdır.
Joh,

1

EDIT: Derslerin açık olacak şekilde tasarlanması gerektiğine inanıyorum. Bazı kısıtlamalarla, ancak miras için kapalı olmamalıdır.

Neden: "Son sınıflar" veya "mühürlü sınıflar" bana tuhaf geliyor. Asla kendi sınıflarımdan birini "final" ("mühürlü") olarak işaretlemem gerekmiyor, çünkü daha sonra bu dersleri miras almam gerekebilir.

Sınıfları olan üçüncü parti kütüphaneleri (çoğu görsel kontroller) satın aldım / indirdim ve kodlama yaptıkları programlama dili tarafından desteklenseler bile "final" i kullanan kütüphanelerin hiçbirini minnettarım, çünkü bu sınıfları genişletmeyi bıraktım, kaynak kodu olmadan kütüphaneleri derlesem bile.

Bazen bazı zaman zaman "mühürlü" sınıflarla uğraşmak zorunda kaldım ve benzer üyeleri olan belirli bir sınıfı içeren yeni bir sınıf "sarmalayıcı" oluşturmaya başladım.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Kapsam sınıflandırıcıları, bazı özellikleri miras kısıtlamadan "mühürlemeye" izin verir.

Structured Progr. OOP'a özel sınıf üyelerini kullanmaya başladım , ancak birçok projeden sonra korumalı kullanmayı bıraktım ve nihayetinde bu özellikleri veya yöntemleri halka duyurdum.

PS C # 'da anahtar kelime olarak "son" kelimesini sevmiyorum, kalıtım metaforunu izleyerek Java gibi "mühürlü" ya da "steril" i tercih ederim. Ayrıca "final", çeşitli bağlamlarda kullanılan bir belirsiz kelimedir.


-1: Açık tasarımları sevdiğini söyledin, ancak bu soruyu cevaplamıyor, bu yüzden sınıfların açık olması için tasarlanmaması gerekiyordu.
Joh,

@Joh Üzgünüm, kendimi iyi açıklamamıştım, tam tersini ifade etmeye çalıştım, mühürlenmemeliydi. Neden katılmayacağını açıkladığın için teşekkürler.
umlcat
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.