Alıcı ve ayarlayıcı yöntemleri neden kötüdür?

Bir "kötüdür" dizisi başlatmak niyetinde değildim, ancak birkaç okuyucu geçen ayın "Neden Evil'i uzatır" sütununda get / set yöntemlerinden kaçınmanız gerektiğinden neden bahsettiğimi sordu.

Alıcı / ayarlayıcı yöntemleri Java'da yaygın olsa da, özellikle nesne yönelimli (OO) değildirler. Aslında, kodunuzun bakımına zarar verebilirler. Dahası, çok sayıda alıcı ve ayarlayıcı yönteminin varlığı, programın OO açısından iyi tasarlanmadığı konusunda bir uyarı işaretidir.

Bu makale alıcıları ve ayarlayıcıları neden kullanmamanız gerektiğini (ve bunları ne zaman kullanabileceğinizi) açıklar ve alıcı / ayarlayıcı zihniyetinden çıkmanıza yardımcı olacak bir tasarım metodolojisi önerir.

Tasarımın doğası üzerine

Tasarımla ilgili başka bir sütuna geçmeden önce (kışkırtıcı bir başlık ile), birkaç şeyi açıklığa kavuşturmak istiyorum.

Geçen ayın "Why Extends Is Evil" başlıklı köşesinden kaynaklanan bazı okuyucu yorumları karşısında şaşkına döndüm (makalenin son sayfasındaki Talkback'e bakın). Bazı insanlar, nesne yöneliminin kötü olduğunu, çünkü extendsiki kavram birbirine denkmiş gibi sorunları olduğunu savunduğuma inanıyordu . Kesinlikle söylediğimi düşündüğüm şey bu değil, bu yüzden bazı meta sorunları açıklığa kavuşturmama izin verin.

Bu sütun ve geçen ayın makalesi tasarım hakkındadır. Tasarım, doğası gereği bir dizi ödünleşmedir. Her seçimin iyi ve kötü bir yanı vardır ve seçiminizi zorunluluk tarafından tanımlanan genel kriterler bağlamında yaparsınız. Bununla birlikte, iyi ve kötü mutlak değildir. Bir bağlamda iyi bir karar, başka bir bağlamda kötü olabilir.

Bir konunun her iki tarafını da anlamıyorsanız, akıllıca bir seçim yapamazsınız; aslında, eylemlerinizin tüm sonuçlarını anlamıyorsanız, hiç tasarlamıyorsunuz demektir. Karanlıkta tökezliyorsun. Gang of Four's Design Patterns kitabındaki her bölümde bir modelin ne zaman ve neden uygun olmadığını açıklayan bir "Sonuçlar" bölümü olması bir tesadüf değil.

Bazı dil özelliklerinin veya genel programlama deyimlerinin (erişimciler gibi) sorunları olduğunu belirtmek, bunları hiçbir koşulda asla kullanmamanız gerektiğini söylemekle aynı şey değildir. Ve bir özellik veya deyimin yaygın olarak kullanılması, onu da kullanmanız gerektiği anlamına gelmez . Bilgisiz programcılar birçok program yazar ve sadece Sun Microsystems veya Microsoft tarafından çalıştırılmak, birinin programlama veya tasarım yeteneklerini sihirli bir şekilde geliştirmez. Java paketleri çok sayıda harika kod içerir. Ancak bu kodun bazı kısımları da var, eminim yazarlar yazdıklarını kabul etmekten utanıyorlar.

Aynı şekilde, pazarlama veya politik teşvikler genellikle tasarım deyimlerini zorlar. Bazen programcılar kötü kararlar alırlar, ancak şirketler teknolojinin yapabileceklerini teşvik etmek isterler, bu yüzden bunu yapma şeklinizin idealden daha az olduğunu vurguluyorlar. Kötü bir durumdan en iyi şekilde yararlanırlar. Sonuç olarak, herhangi bir programlama uygulamasını benimsediğinizde sorumsuzca davranırsınız çünkü "işleri yapmanız gereken yol budur". Başarısız birçok Enterprise JavaBeans (EJB) projesi bu prensibi kanıtlamaktadır. EJB tabanlı teknoloji, uygun şekilde kullanıldığında harika bir teknolojidir, ancak uygun olmayan şekilde kullanıldığında bir şirketi kelimenin tam anlamıyla alt üst edebilir.

Demek istediğim, körü körüne programlama yapmamalısın. Bir özelliğin veya deyimin yol açabileceği tahribatı anlamalısınız. Bunu yaparken, bu özelliği veya deyimi kullanıp kullanmayacağınıza karar vermede çok daha iyi bir konumdasınız. Seçimleriniz hem bilgili hem de pragmatik olmalıdır. Bu makalelerin amacı, programlamanıza açık gözlerle yaklaşmanıza yardımcı olmaktır.

Veri soyutlama

OO sistemlerinin temel bir ilkesi, bir nesnenin uygulama ayrıntılarından herhangi birini açığa çıkarmaması gerektiğidir. Bu şekilde, nesneyi kullanan kodu değiştirmeden uygulamayı değiştirebilirsiniz. Bundan sonra, OO sistemlerinde alıcı ve ayarlayıcı işlevlerinden kaçınmanız gerekir çünkü bunlar çoğunlukla uygulama ayrıntılarına erişim sağlar.

Nedenini görmek için getX(), programınızda bir yönteme 1.000 çağrı olabileceğini ve her çağrının dönüş değerinin belirli bir türde olduğunu varsaydığını düşünün . getX()Örneğin, dönüş değerini yerel bir değişkende depolayabilirsiniz ve bu değişken türü, dönüş değeri türüyle eşleşmelidir. Nesnenin uygulanma şeklini X türü değişecek şekilde değiştirmeniz gerekirse başınız büyük belada demektir.

X bir ise int, ancak şimdi a olması gerekiyorsa long, 1000 derleme hatası alacaksınız. Dönüş değerini çevirerek sorunu yanlış bir intşekilde çözerseniz, kod temiz bir şekilde derlenir, ancak çalışmaz. (Dönüş değeri kesilebilir.) Değişikliği telafi etmek için bu 1.000 çağrının her birini çevreleyen kodu değiştirmeniz gerekir. Kesinlikle o kadar çok iş yapmak istemiyorum.

OO sistemlerinin temel ilkelerinden biri veri soyutlamadır . Programın geri kalanından bir nesnenin bir ileti işleyicisini uygulama şeklini tamamen gizlemelisiniz. Bu, tüm örnek değişkenlerinizin (bir sınıfın sabit olmayan alanları) olması gerektiğinin bir nedeni private.

Bir örnek değişkeni publicyaparsanız, sınıf zaman içinde geliştikçe alanı değiştiremezsiniz çünkü alanı kullanan harici kodu kırmış olursunuz. Sırf o sınıfı değiştirdiğiniz için bir sınıfın 1000 kullanımını aramak istemezsiniz.

Bu uygulama gizleme ilkesi, bir OO sisteminin kalitesinin iyi bir asit testine götürür: Sınıf tanımında büyük değişiklikler yapabilir, hatta bunu kullanan kodların hiçbirini etkilemeden tamamen farklı bir uygulama ile değiştirebilir misiniz? sınıfın nesneleri? Bu tür modülerleştirme, nesne yönlendirmenin temel dayanağıdır ve bakımı çok daha kolay hale getirir. Uygulama gizlenmeden, diğer OO özelliklerini kullanmanın pek bir anlamı yoktur.

Alıcı ve ayarlayıcı yöntemleri (erişimciler olarak da bilinir), publicalanların tehlikeli olmasıyla aynı nedenle tehlikelidir: Uygulama ayrıntılarına dışarıdan erişim sağlarlar. Erişilen alanın türünü değiştirmeniz gerekirse ne olur? Ayrıca erişimcinin dönüş türünü de değiştirmeniz gerekir. Bu dönüş değerini birçok yerde kullanırsınız, bu nedenle kodun tamamını da değiştirmeniz gerekir. Bir değişikliğin etkilerini tek bir sınıf tanımıyla sınırlamak istiyorum. Tüm programın içine dalmalarını istemiyorum.

Erişimciler kapsülleme ilkesini ihlal ettiğinden, erişimcileri çok veya uygunsuz bir şekilde kullanan bir sistemin basitçe nesne yönelimli olmadığını makul bir şekilde iddia edebilirsiniz. Yalnızca kodlamanın aksine bir tasarım sürecinden geçerseniz, programınızda neredeyse hiç erişimci bulamazsınız. Süreç önemlidir. Makalenin sonunda bu konuda söyleyecek daha çok şeyim var.

Alıcı / ayarlayıcı yöntemlerinin olmaması, bazı verilerin sistemden akmadığı anlamına gelmez. Bununla birlikte, veri hareketini olabildiğince en aza indirmek en iyisidir. Benim deneyimim, sürdürülebilirliğin nesneler arasında hareket eden veri miktarı ile ters orantılı olmasıdır. Nasıl olduğunu henüz görememiş olsanız da, bu veri hareketinin çoğunu gerçekten ortadan kaldırabilirsiniz.

Dikkatlice tasarlayarak ve onu nasıl yapacağınızdan ziyade yapmanız gerekenlere odaklanarak, programınızdaki alıcı / ayarlayıcı yöntemlerinin büyük çoğunluğunu ortadan kaldırırsınız. İşi yapmak için ihtiyacınız olan bilgileri istemeyin; İşi sizin için yapacak bilgiye sahip olan nesneden isteyin.Çoğu erişimci koda girmenin yolunu bulur çünkü tasarımcılar dinamik modeli düşünmezler: çalışma zamanı nesneleri ve işi yapmak için birbirlerine gönderdikleri mesajlar. Bir sınıf hiyerarşisi tasarlayarak (yanlış bir şekilde) başlarlar ve daha sonra bu sınıfları dinamik modele bağlamaya çalışırlar. Bu yaklaşım asla işe yaramaz. Statik bir model oluşturmak için, sınıflar arasındaki ilişkileri keşfetmeniz gerekir ve bu ilişkiler tam olarak mesaj akışına karşılık gelir. İki sınıf arasında bir ilişki, yalnızca bir sınıfın nesneleri diğerinin nesnelerine mesaj gönderdiğinde mevcuttur. Statik modelin ana amacı, siz dinamik olarak modellerken bu ilişkilendirme bilgilerini yakalamaktır.

Açıkça tanımlanmış bir dinamik model olmadan, yalnızca bir sınıfın nesnelerini nasıl kullanacağınızı tahmin edersiniz. Sonuç olarak, erişimci yöntemleri genellikle modele girer, çünkü buna ihtiyacınız olup olmayacağını tahmin edemeyeceğiniz için mümkün olduğunca çok erişim sağlamanız gerekir. Bu tür bir tahmin yoluyla tasarım stratejisi, en iyi ihtimalle verimsizdir. Yararsız yöntemler yazarak (veya sınıflara gereksiz yetenekler ekleyerek) zaman kaybedersiniz.

Erişimciler ayrıca alışkanlıktan dolayı tasarımlara girerler. Prosedürel programcılar Java'yı benimsediklerinde, tanıdık kodlar oluşturarak başlama eğilimindedirler. Prosedürel dillerin sınıfları yoktur, ancak C'ye sahiptirler struct(düşün: yöntemsiz sınıf). O halde, structneredeyse hiçbir yöntem ve alan dışında hiçbir şey olmadan sınıf tanımları oluşturarak a'yı taklit etmek doğal görünüyor public. Bu yordamsal programcılar, alanların olması gerektiğini bir yerde okurlar private, bu yüzden alanları oluştururlar privateve publicerişimci yöntemleri sağlarlar . Ancak sadece kamusal erişimi karmaşıklaştırdılar. Kesinlikle sistemi nesne odaklı yapmadılar.

Kendini çiz

Tam alan kapsüllemenin bir dalı, kullanıcı arayüzü (UI) yapısındadır. Erişimcileri kullanamıyorsanız, bir UI oluşturucu sınıfının bir getAttribute()yöntemi çağırmasına izin veremezsiniz . Bunun yerine, sınıfların drawYourself(...)yöntemler gibi öğeleri vardır .

getIdentity()Elbette bir yöntem, Identityarayüzü uygulayan bir nesne döndürmesi koşuluyla da işe yarayabilir . Bu arayüz bir drawYourself()(veya JComponentkimliğinizi-temsil eden- bana-bir-ver ) yöntemi içermelidir . Gerçi getIdentityile başlar "olsun," bu sadece bir alanı dönmez çünkü bir erişimci değil. Makul davranışa sahip karmaşık bir nesne döndürür. Bir nesnem olsa bile Identity, bir kimliğin içeride nasıl temsil edildiğine dair hiçbir fikrim yok.

Tabii ki, drawYourself()strateji, UI kodunu iş mantığına koyduğum (soluğumun!) Anlamına gelir. Kullanıcı arayüzünün gereksinimleri değiştiğinde ne olacağını düşünün. Diyelim ki özelliği tamamen farklı bir şekilde temsil etmek istiyorum. Bugün bir "kimlik" bir isimdir; yarın bir isim ve kimlik numarası; ondan sonraki gün bir isim, kimlik numarası ve resim. Bu değişikliklerin kapsamını kodda tek bir yerle sınırlıyorum. Eğer JComponentkimliğini temsil eden bir bana ver sınıfım varsa, o zaman kimliklerin temsil edilme şeklini sistemin geri kalanından izole etmişimdir.

İş mantığına aslında herhangi bir UI kodu koymadığımı unutmayın. UI katmanını, her ikisi de soyutlama katmanı olan AWT (Abstract Window Toolkit) veya Swing açısından yazdım. Gerçek UI kodu AWT / Swing uygulamasındadır. İş mantığınızı bir alt sistemin mekaniğinden ayırmak için soyutlama katmanının tüm amacı budur. Kodu değiştirmeden başka bir grafik ortamına kolayca aktarabiliyorum, bu nedenle tek sorun biraz dağınıklık. Tüm UI kodunu bir iç sınıfa taşıyarak (veya Cephe tasarım modelini kullanarak) bu karmaşayı kolayca ortadan kaldırabilirsiniz.

JavaBeans

"Peki ya JavaBeans?" Diyerek itiraz edebilirsiniz. Onlar hakkında ne? Kesinlikle alıcılar ve ayarlayıcılar olmadan JavaBeans oluşturabilirsiniz. BeanCustomizer, BeanInfoVe BeanDescriptortam olarak bu amaç için sınıflar tüm sağlıklı olur. JavaBean spec tasarımcıları, getter / setter deyimini resme attılar çünkü hızlı bir şekilde fasulye yapmanın kolay bir yolu olacağını düşündüler - nasıl doğru yapılacağını öğrenirken yapabileceğiniz bir şey. Ne yazık ki bunu kimse yapmadı.

Erişimciler yalnızca belirli özellikleri etiketlemenin bir yolu olarak oluşturuldu, böylece bir UI oluşturucu program veya eşdeğeri onları tanımlayabilir. Bu yöntemleri kendi başınıza adlandırmanız gerekmiyor. Otomatik bir araç kullanmak için varlar. Bu araç Class, yöntemleri bulmak ve yöntem adlarından belirli özelliklerin varlığını tahmin etmek için sınıftaki iç gözlem API'lerini kullanır . Pratikte, bu iç gözlem temelli deyim işe yaramadı. Kodu çok karmaşık ve prosedürel hale getirdi. Veri soyutlamasını anlamayan programcılar aslında erişimcileri çağırır ve sonuç olarak kodun bakımı daha az olur. Bu nedenle, bir meta veri özelliği Java 1.5'e dahil edilecektir (2004 ortalarında kullanıma sunulacaktır). Yani bunun yerine:

özel int mülk; public int getProperty () {dönüş özelliği; } genel void setProperty (int değer} {özellik = değer;}

Şunun gibi bir şey kullanabileceksiniz:

özel @ mülk int özelliği; 

UI oluşturma aracı veya eşdeğeri, yöntem adlarını incelemek ve bir özelliğin varlığını bir addan çıkarmak yerine, özellikleri bulmak için iç gözlem API'lerini kullanır. Bu nedenle, hiçbir çalışma zamanı erişimcisi kodunuza zarar vermez.

Bir erişimci ne zaman uygundur?

Birincisi, daha önce tartıştığım gibi, bir yöntemin, nesnenin uyguladığı bir arabirim açısından bir nesneyi döndürmesi sorun değildir, çünkü bu arabirim sizi uygulama sınıfındaki değişikliklerden ayırır. Bu tür bir yöntem (bir arayüz referansı döndürür), yalnızca bir alana erişim sağlayan bir yöntem anlamında gerçekten bir "alıcı" değildir. Sağlayıcının dahili uygulamasını değiştirirseniz, yalnızca döndürülen nesnenin tanımını değişiklikleri karşılayacak şekilde değiştirirsiniz. Nesneyi kullanan harici kodu arabirimi aracılığıyla hala korursunuz.