Bir iç veri yapısını her zaman tamamen kapsüllemeli miyim?


11

Lütfen bu sınıfı düşünün:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Bu sınıf, verileri depolamak için kullandığı diziyi ilgili tüm istemci kodlarına maruz bırakır.

Bunu üzerinde çalıştığım bir uygulamada yaptım. Ben s ChordProgressionbir dizi saklayan bir sınıf vardı Chord(ve başka şeyler yapar). Chord[] getChords()Akor dizisini döndüren bir yöntemi vardı . Veri yapısının değişmesi gerektiğinde (bir diziden ArrayList'e), tüm istemci kodu kırıldı.

Bu beni düşündürdü - belki de aşağıdaki yaklaşım daha iyidir:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Veri yapısının kendisini ortaya çıkarmak yerine, veri yapısı tarafından sunulan tüm işlemler artık veri yapısına delege olan genel yöntemler kullanılarak doğrudan onu çevreleyen sınıf tarafından sunulmaktadır.

Veri yapısı değiştiğinde, yalnızca bu yöntemlerin değişmesi gerekir - ancak yapıldıktan sonra, tüm istemci kodu hala çalışır.

Dizilerden daha karmaşık koleksiyonların, yalnızca dahili veri yapısına erişmek için üçten fazla yöntem uygulamayı gerektiren ek sınıf gerektirebileceğini unutmayın.


Bu yaklaşım yaygın mı? Bunun hakkında ne düşünüyorsun? Başka ne dezavantajları var? Çevreleyen sınıfın, yalnızca iç veri yapısına delege etmek için en az üç genel yöntemi uygulamasının mantıklı mı?

Yanıtlar:


14

Gibi kod:

   public Thing[] getThings(){
        return things;
    }

Erişim yönteminiz doğrudan dahili veri yapısını döndürmekten başka bir şey yapmadığı için çok mantıklı değil. Siz de öyle olduğunuzu beyan Thing[] thingsedebilirsiniz public. Bir erişim yönteminin arkasındaki fikir, istemcileri dahili değişikliklerden yalıtan ve arayüzün izin verdiği gizli yöntemler dışında gerçek veri yapısını manipüle etmelerini engelleyen bir arayüz oluşturmaktır. Tüm müşteri kodunuz kırıldığında keşfettiğiniz gibi, erişim yönteminiz bunu yapmadı - sadece boşa giden kod. Bence bir çok programcı böyle kod yazma eğilimindedir çünkü bir yerde her şeyin erişim yöntemleri ile kapsüllenmesi gerektiğini öğrendiler - ama açıkladığım nedenlerden dolayı. Erişim yöntemi herhangi bir amaca hizmet etmediğinde bunu sadece "formu takip etmek" için yapmak sadece gürültüdür.

Kapsüllemenin en önemli hedeflerinden bazılarına ulaşan önerilen çözümünüzü kesinlikle tavsiye ederim: Müşterilere, onları sınıfınızın dahili uygulama ayrıntılarından yalıtan ve dahili veri yapısına dokunmalarına izin vermeyen sağlam, gizli bir arayüz sağlamak karar verdiğiniz yolların uygun olmasını bekleyin - "en az gerekli ayrıcalık yasası". CLR, STL, VCL gibi büyük popüler OOP çerçevelerine bakarsanız, önerdiğiniz desen tam olarak bu nedenle yaygındır.

Bunu hep yapmalı mısın? Şart değil. Örneğin, asıl işçi sınıfınızın bir bileşeni olan ve "önden bakan" olmayan yardımcı veya arkadaş sınıflarınız varsa, bu gerekli değildir - bu çok fazla gereksiz kod ekleyecek bir aşırıya kaçmadır. Ve bu durumda, hiç bir erişim yöntemi kullanmazdım - açıklandığı gibi anlamsız. Veri yapısını yalnızca onu kullanan ana sınıfa dahil bir şekilde beyan edin - çoğu dil bunu yapmanın yollarını destekler - friendveya ana işçi sınıfı ile aynı dosyada beyan eder.

Teklifinizde görebildiğim tek dezavantajı , kodlamak için daha fazla iş olması (ve şimdi tüketici sınıflarınızı yeniden kodlamanız gerekecek - ama yine de bunu yapmak zorundasınız / zorundasınız.) Ama bu gerçekten dezavantaj değil - doğru yapmanız gerekir ve bazen bu daha fazla iş gerektirir.

İyi bir programcıyı iyi yapan şeylerden biri, ekstra işin ne zaman ve ne zaman değmez olduğunu bilmeleridir. Uzun vadede ekstra koymak şimdi gelecekte büyük temettüler ile ödeyecek - bu projede değilse, o zaman diğerleri üzerinde. Doğru şekilde kodlamayı öğrenin ve sadece öngörülen formları robotik olarak takip etmekle kalmayın, başınızı bu konuda kullanın.

Dizilerden daha karmaşık koleksiyonların, yalnızca dahili veri yapısına erişmek için üçten fazla yöntem uygulamayı gerektiren ek sınıf gerektirebileceğini unutmayın.

Tüm veri yapısını bir kapsayıcı sınıf aracılığıyla açığa çıkarıyorsanız, IMO, yalnızca daha güvenli bir arabirim - bir "sarmalayıcı sınıfı" sağlamak değilse, bu sınıfın neden hiç kapsüllenmediğini düşünmeniz gerekir. Kapsayıcı sınıfın bu amaç için mevcut olmadığını söylüyorsunuz - belki de tasarımınızla ilgili doğru olmayan bir şey var. Sınıflarınızı daha gizli modüllere ayırmayı ve katmanlamayı düşünün.

Bir sınıfın açık ve gizli bir amacı olmalı ve artık bu işlevselliği desteklemek için bir arayüz sağlamalıdır. Birbirine ait olmayan şeyleri bir araya toplamaya çalışıyor olabilirsiniz. Bunu yaptığınızda, her değişiklik uyguladığınızda işler bozulacaktır. Sınıflarınız ne kadar küçük ve ihtiyatlısa, bazı şeyleri değiştirmek o kadar kolay olur: LEGO düşünün.


1
Cevabın için teşekkür ederim. Bir soru: Dahili veri yapısında, belki de 5 genel yöntem varsa - bunların hepsinin sınıfımın genel arayüzü tarafından öne çıkarılması gerekiyorsa? Örneğin, bir Java ArrayList aşağıdaki yöntemleri vardır: get(index), add(), size(), remove(index), ve remove(Object). Önerilen tekniği kullanarak, bu ArrayList'i içeren sınıfın yalnızca iç koleksiyona delege etmek için beş genel yöntemi olmalıdır. Ve bu sınıfın programdaki amacı büyük olasılıkla bu ArrayList'i kapsüllemek değil, başka bir şey yapmaktır. ArrayList sadece bir ayrıntı. [...]
Aviv Cohn

İç veri yapısı, sıradan bir üyedir, yukarıdaki tekniği kullanarak - beş ortak yöntem daha içermesi için sınıf içermesini gerektirir. Sizce - bu makul mü? Ve ayrıca - bu yaygın mı?
Aviv Cohn

@Prog - İç veri yapısının belki 5 genel yöntemi varsa ... IMO, ana sınıfınızın içine bir yardımcı sınıfın tamamını sarmanız ve bu şekilde ortaya çıkarmanız gerekiyorsa, bunu yeniden düşünmeniz gerekir. tasarım -Kamu sınıfınız çok fazla şey yapıyor veya uygun arayüzü sunmuyor. Bir sınıfın çok gizli ve açıkça tanımlanmış bir rolü olmalı ve arayüzü bu rolü ve sadece bu rolü desteklemelidir. Sınıflarınızı parçalamayı ve katmanlamayı düşünün. Bir sınıf, kapsülleme adına her türlü nesneyi içeren bir "mutfak lavabosu" olmamalıdır.
Vektör

Tüm veri yapısını bir sarıcı sınıf aracılığıyla açığa çıkarıyorsanız, IMO, yalnızca daha güvenli bir arayüz sağlamak değilse, bu sınıfın neden kapsüllendiğini düşünmeniz gerekir. Kapsayıcı sınıfın bu amaç için mevcut olmadığını söylüyorsunuz - bu nedenle bu tasarımda doğru olmayan bir şey var.
Vektör

1
@Phoshi - Salt okunur anahtar kelime - Bunu kabul edebilirim. Ancak OP salt okunur hakkında konuşmuyor. örneğin remove, salt okunur değildir. Anladığım kadarıyla OP, önerilen değişiklikten önceki orijinal koddaki gibi her şeyi herkese açık hale getirmek istiyor public Thing[] getThings(){return things;}.
Vektör

2

Siz istediniz: Her zaman bir iç veri yapısını tamamen kapsüllemeli miyim?

Kısa Cevap: Evet, çoğu zaman ama her zaman değil .

Uzun Cevap: Sınıfların aşağıdaki kategorilere ayrıldığını düşünüyorum:

  1. Basit verileri kapsayan sınıflar. Örnek: 2B nokta. X ve Y koordinatlarını alma / ayarlama yeteneği sağlayan genel işlevler oluşturmak kolaydır, ancak dahili verileri çok fazla sorun olmadan kolayca gizleyebilirsiniz. Bu tür sınıflar için, iç veri yapısı ayrıntılarının gösterilmesi çağrılmaz.

  2. Koleksiyonları kapsayan kap sınıfları. STL klasik konteyner sınıflarına sahiptir. Ben düşünün std::stringve std::wstringkişiler arasında da. Onlar soyutlama başa ama zengin bir arayüz sağlamak std::vector, std::stringve std::wstringaynı zamanda ham verilere erişmek için yeteneği sağlar. Onlara kötü tasarlanmış dersler demekten acelem olmazdım. Bu sınıfların ham verilerini ortaya koyan gerekçelerini bilmiyorum. Ancak, çalışmamda, performans nedenleriyle milyonlarca örgü düğümü ve bu örgü düğümlerindeki verilerle uğraşırken ham verileri ortaya çıkarmam gerektiğini buldum.

    Bir sınıfın iç yapısını ortaya çıkarmanın en önemli yanı, ona yeşil bir sinyal vermeden önce uzun ve sıkı düşünmeniz gerektiğidir. Arayüz bir projenin içindeyse, gelecekte değiştirmek pahalı olacaktır, ancak imkansız değildir. Arabirim projenin dışındaysa (örneğin, diğer uygulama geliştiricileri tarafından kullanılacak bir kitaplık geliştirdiğinizde), istemcinizi kaybetmeden arabirimi değiştirmek mümkün olmayabilir.

  3. Doğada çoğunlukla işlevsel olan sınıflar. Örnekler: std::istream,, std::ostreamSTL kaplarının yineleyicileri. Bu sınıfların iç detaylarını açığa çıkarmak aptalca.

  4. Hibrit sınıflar. Bunlar, bazı veri yapılarını kapsayan ancak algoritmik işlevsellik sağlayan sınıflardır. Şahsen, bunların kötü düşünülmüş tasarımın bir sonucu olduğunu düşünüyorum. Ancak, bunları bulursanız, dahili verilerini vaka bazında ifşa etmenin mantıklı olup olmadığına karar vermelisiniz.

Sonuç olarak: Bir sınıfın iç veri yapısını ortaya çıkarmayı gerekli bulduğum tek zaman, performans darboğazı haline geldiği zamandır.


Bence, STL'nin dahili verilerini göstermesinin en önemli nedeni, çok fazla işaretçi bekleyen tüm fonksiyonlarla uyumluluk olmasıdır.
Siyuan Ren

0

Ham verileri doğrudan döndürmek yerine, böyle bir şey deneyin

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Yani, esas olarak, dünyaya her ne isterse sunacak özel bir koleksiyon sağlıyorsunuz. O zaman yeni uygulamanızda,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Artık doğru bir kapsüllemeye sahipsiniz, uygulama ayrıntılarını gizleyin ve geriye dönük uyumluluk sağlayın (bir ücret karşılığında).


Geriye dönük uyumluluk için akıllıca bir fikir. Ancak: Şimdi doğru kapsüllemeye sahipsiniz, uygulama ayrıntılarını gizleyin - gerçekten değil. Müşteriler hala nüansları ile uğraşmak zorundalar List. İşleri daha sağlam hale getirmek için oyuncu kadrosuyla bile veri üyelerini döndüren erişim yöntemleri gerçekten iyi bir kapsülleme IMO'su değildir. İşçi sınıfı müşteriyi değil tüm bunları idare etmelidir. Bir müşterinin sahip olması gereken "sayı" daha güçlü olacaktır. Bir kenara, sana soru ... cevap emin değilim
Vektör

1
@Vector - haklısın. Döndürülen veri yapısı hala değiştirilebilir ve yan etkiler bilgi gizlemesini öldürecektir.
BobDalgleish

Döndürülen veri yapısı hala değişkendir ve yan etkiler bilginin gizlenmesini öldürür - evet, bu da tehlikelidir. Sorunun odak noktası olan istemciden neyin gerekli olduğunu düşünüyordum.
Vektör

@BobDalgleish: neden orijinal koleksiyonun bir kopyasını döndürmüyorsunuz?
Giorgio

1
@BobDalgleish: İyi performans nedenleri olmadıkça, kullanıcılarının çok kötü bir tasarım kararı olarak değiştirmelerine izin vermek için dahili veri yapılarına bir referans döndürmeyi düşünürdüm. Bir nesnenin iç durumu yalnızca uygun genel yöntemlerle değiştirilmelidir.
Giorgio

0

Bu şeyler için arayüzler kullanmalısınız. Java dizisi bu arayüzleri uygulamadığından, şu andan itibaren yapmanız gerekir:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

Bu şekilde değiştirebileceğiniz ArrayListiçin LinkedListveya başka bir şey ve sahip (diziler yanında) tüm Java koleksiyonları beri herhangi bir kod (sözde?) Rasgele erişim muhtemelen uygulayacaktır kırmayacak List.

Ayrıca kullanabilirsiniz Collectiondaha az yöntemleri sunan, Listancak rastgele erişimi olmayan koleksiyonlarını destekleyebilir ya Iterablebile akışlarını destekleyebilir ama erişim yöntemleri vadede pek sunmuyor ...


-1 - zayıf uzlaşma ve özellikle güvenli olmayan IMO: Dahili veri yapısını istemciye sunuyorsunuz, sadece maskeleyip en iyisini umuyorsunuz çünkü "Java koleksiyonları ... muhtemelen List'i uygulayacaktır." Çözümünüz gerçekten polimorfik / kalıtım temelli olsaydı - tüm koleksiyonların her zaman Listtüretilmiş sınıflar olarak uygulanması , daha mantıklı olurdu, ama sadece "en iyisini ummak" iyi bir fikir değildir. "İyi bir programcı tek yönlü bir sokakta her iki yöne bakar".
Vektör

@Vector Evet, gelecekteki Java koleksiyonlarının uygulanacağını List( Collectionveya en azından Iterable) varsayıyorum . Bu arayüzlerin asıl noktası budur ve Java Dizilerinin bunları uygulamadığı bir utançtır, ancak Java'daki koleksiyonlar için resmi arayüzdür, bu nedenle herhangi bir Java koleksiyonunun bunları uygulayacağını varsaymak o kadar da fazla değildir - bu koleksiyon daha eski değilse daha Listve bu durumda onunla sarmak çok kolay AbstractList .
Idan Arye

Varsayımınızın gerçekte gerçekleşeceğinin neredeyse garantili olduğunu söylüyorsunuz, bu yüzden Tamam - Aşağı oyu kaldıracağım (izin verdiğimde) çünkü açıklamak için yeterince iyiydiniz ve bir Java adamı değilim ozmoz hariç. Yine de, nasıl yapıldığına bakılmaksızın dahili veri yapısını ortaya çıkarma fikrini desteklemiyorum ve OP'nin gerçekten kapsüllemeyle ilgili sorusuna doğrudan cevap vermediniz. yani dahili veri yapısına erişimi sınırlamak.
Vektör

1
@Vector Evet, kullanıcılar Listiçeri girebilir ArrayList, ancak uygulama% 100 korunabilir gibi değildir - özel alanlara erişmek için her zaman yansımayı kullanabilirsiniz. Bunu yapmak kaşlarını çattı, ancak döküm de kaşlarını çattı (o kadar da değil). Kapsülleme noktası kötü niyetli saldırıları önlemek değildir; bunun yerine, kullanıcıların değiştirmek isteyebileceğiniz uygulama ayrıntılarına bağlı olmasını önlemektir. ListArayüzü kullanmak tam olarak bunu yapar - sınıfın kullanıcıları değişebilen Listsomut ArrayListsınıf yerine arayüze güvenebilir .
Idan Arye

özel alanlara erişmek için yansımayı her zaman kullanabilirsiniz - birisi kötü kod yazmak ve bir tasarımı değiştirmek istiyorsa, bunu yapabilirler. bunun yerine, kullanıcıları önlemek ... - bu kapsüllemenin bir nedeni. Bir diğeri, sınıfınızın iç durumunun bütünlüğünü ve tutarlılığını sağlamaktır. Sorun "kötü niyetli saldırı" değil, kötü hatalara yol açan kötü organizasyon. "En az gerekli ayrıcalık yasası" -sınıfınızın tüketicisine sadece zorunlu olanı verin - artık yok. Dahili veri yapısının tamamını herkese açık hale getirmeniz zorunlu ise bir tasarım sorununuz vardır.
Vektör

-2

Bu, iç veri yapınızı dış dünyadan gizlemek için oldukça yaygındır. Bazen DTO'da özellikle aşırıya kaçıyor. Bunu etki alanı modeli için öneririm. Gösterilmesi gerekiyorsa, değiştirilemeyen kopyayı iade edin. Bununla birlikte, get, set, remove vb.Gibi yöntemlere sahip bir arayüz oluşturmanızı öneririz.


1
Bu, önceki 3
cevapta
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.