OOP'de Ustalaşmak: Kalıtım, Arayüzler ve Soyut Sınıflar İçin Pratik Bir Kılavuz
Yayınlanan: 2022-03-10Söyleyebileceğim kadarıyla, yazılım geliştirme alanında teorik ve pratik bilgilerin uygun bir karışımını sağlayan eğitim içeriğine rastlamak nadirdir. Nedenini tahmin edecek olursam, teoriye odaklanan bireylerin öğretmeye yönelme eğiliminde olduklarını ve pratik bilgilere odaklanan kişilerin belirli dilleri ve araçları kullanarak belirli sorunları çözmek için para alma eğiliminde olduklarını varsayıyorum.
Bu, elbette, geniş bir genellemedir, ancak argümanlar uğruna kısaca kabul edersek, öğretmen rolünü üstlenen birçok insanın (hiçbir şekilde tüm insanlar değil) ya fakir ya da tamamen yetersiz olma eğiliminde olduğu sonucu çıkar. belirli bir kavramla ilgili pratik bilgiyi açıklamaktır.
Bu makalede, çoğu Nesne Yönelimli Programlama (OOP) dilinde bulacağınız üç temel mekanizmayı tartışmak için elimden gelenin en iyisini yapacağım: Kalıtım , arayüzler (aka protokoller ) ve soyut sınıflar . Size her bir mekanizmanın ne olduğuna dair teknik ve karmaşık sözlü açıklamalar vermek yerine, ne yaptıklarına ve ne zaman kullanacaklarına odaklanmak için elimden gelenin en iyisini yapacağım.
Ancak, bunları tek tek ele almadan önce, teorik olarak sağlam ama pratik olarak faydasız bir açıklama yapmanın ne anlama geldiğini kısaca tartışmak istiyorum. Umuyorum ki, bu bilgiyi farklı eğitim kaynaklarını gözden geçirmenize yardımcı olması ve bir şeyler mantıklı gelmediğinde kendinizi suçlamaktan kaçınmanız için kullanabilirsiniz.
Farklı Bilme Dereceleri
İsimleri Bilmek
Bir şeyin adını bilmek, muhtemelen bilmenin en sığ biçimidir. Aslında, bir ad, yalnızca birçok kişi tarafından aynı şeye atıfta bulunmak için yaygın olarak kullanıldığı ve/veya bir şeyi tanımlamaya yardımcı olduğu ölçüde genellikle yararlıdır. Ne yazık ki, bu alanda zaman harcayan herkesin keşfettiği gibi, birçok insan aynı şey için farklı isimler (örn. arayüzler ve protokoller ), farklı şeyler için aynı isimler (örn. modüller ve bileşenler ) veya ezoterik isimler kullanıyor. saçma olma noktası (örneğin, Ya Monad ). Sonuç olarak, isimler zihinsel modellere yalnızca işaretçilerdir (veya referanslardır) ve değişen derecelerde yararlı olabilirler.
Bu alanı çalışmayı daha da zorlaştırmak için, çoğu kişi için kod yazmanın çok benzersiz bir deneyim olduğunu (ya da en azından öyleydi) tahmin etme tehlikesiyle karşı karşıyayım. Daha da karmaşık olanı, bu kodun nihayetinde makine diline nasıl derlendiğini ve zamanla değişen bir dizi elektriksel darbe olarak fiziksel gerçeklikte temsil edildiğini anlamaktır. Bir programda kullanılan süreçlerin, kavramların ve mekanizmaların adları hatırlanabilse bile, bu tür şeyler için yaratılan zihinsel modellerin başka bir bireyin modelleriyle uyumlu olduğunun garantisi yoktur; objektif olarak doğru olup olmadıklarını bir kenara bırakın.
Bu nedenlerle, jargon için doğal olarak iyi bir hafızam olmamasının yanı sıra, isimleri bir şeyi bilmenin en az önemli yönü olarak görüyorum. Bu, isimlerin faydasız olduğu anlamına gelmez, ancak geçmişte projelerimde birçok tasarım deseni öğrendim ve kullandım, sadece yaygın olarak kullanılan isimleri öğrenmek için aylar hatta yıllar sonra.
Sözel Tanımları ve Analojileri Bilmek
Sözlü tanımlar, yeni bir kavramı tanımlamanın doğal başlangıç noktasıdır. Bununla birlikte, adlarda olduğu gibi, değişen derecelerde yararlılık ve alaka düzeyine sahip olabilirler; çoğu öğrencinin nihai hedeflerinin ne olduğuna bağlı. Sözlü tanımlarda gördüğüm en yaygın sorun, tipik olarak jargon biçimindeki varsayılan bilgidir.
Örneğin, iş parçacıklarının belirli bir işlemin aynı adres alanını işgal etmesi dışında, bir iş parçacığının bir işleme çok benzediğini açıkladığımı varsayalım. Süreçlere ve adres boşluklarına zaten aşina olan birine, temel olarak, iş parçacıklarının bir süreç anlayışıyla ilişkilendirilebileceğini (yani aynı özelliklerin çoğuna sahip olduklarını), ancak farklı bir özelliğe göre farklılaştırılabileceğini belirttim.
Bu bilgiye sahip olmayan biri için, en iyi ihtimalle hiçbir anlam ifade etmedim ve en kötü ihtimalle öğrencinin bilmesi gerektiğini varsaydığım şeyleri bilmediği için bir şekilde yetersiz hissetmesine neden oldum. Dürüst olmak gerekirse, eğer öğrenicilerinizin gerçekten bu tür bilgilere sahip olması gerekiyorsa (örneğin, lisansüstü öğrencilere veya deneyimli geliştiricilere öğretmek gibi), bu kabul edilebilir, ancak herhangi bir giriş seviyesi materyalinde bunu yapmanın muazzam bir başarısızlık olduğunu düşünüyorum.
Öğrencinin daha önce gördüğü hiçbir şeye benzemeyen bir kavramın iyi bir sözlü tanımını sağlamak çoğu zaman çok zordur. Bu durumda, öğretmenin ortalama bir kişiye aşina olması muhtemel olan ve kavramın aynı niteliklerinin çoğunu aktardığı ölçüde alakalı bir analoji seçmesi çok önemlidir.
Örneğin, bir yazılım geliştiricisinin, yazılım varlıkları (bir programın farklı bölümleri) sıkı bir şekilde birleştiğinde veya gevşek bir şekilde bağlandığında ne anlama geldiğini anlaması kritik derecede önemlidir. Küçük bir marangoz, bir bahçe kulübesi inşa ederken, onu vida yerine çivi kullanarak birleştirmenin daha hızlı ve daha kolay olduğunu düşünebilir. Bu, bir hata yapılana veya bahçe kulübesinin tasarımındaki bir değişiklik, kulübenin bir kısmının yeniden inşa edilmesini gerektirinceye kadar geçerlidir.
Bu noktada, bahçe kulübesinin parçalarını birbirine sıkıca bağlamak için çivi kullanma kararı, inşaat sürecini bir bütün olarak daha zor, muhtemelen daha yavaş hale getirdi ve çivileri çekiçle çıkarmak yapıya zarar verme riskini taşıyor. Tersine, vidaların montajı biraz fazla zaman alabilir, ancak çıkarılması kolaydır ve kulübenin yakın kısımlarına zarar verme riski çok azdır. Gevşek bağlı ile kastettiğim bu. Doğal olarak, gerçekten sadece bir çiviye ihtiyaç duyduğunuz durumlar vardır, ancak bu karara eleştirel düşünce ve deneyim rehberlik etmelidir.
Daha sonra ayrıntılı olarak tartışacağım gibi, bir programın parçalarını birbirine bağlamak için değişen derecelerde bağlantı sağlayan farklı mekanizmalar vardır; tıpkı çiviler ve vidalar gibi. Analojim, bu kritik öneme sahip terimin ne anlama geldiğini anlamanıza yardımcı olmuş olsa da, bunu bir bahçe kulübesi inşa etme bağlamı dışında nasıl uygulayacağınız konusunda size hiçbir fikir vermedim. Bu beni bilmenin en önemli türüne ve herhangi bir araştırma alanındaki belirsiz ve zor kavramları derinlemesine anlamanın anahtarına götürüyor; Yine de bu makalede kod yazmaya devam edeceğiz.
Kodda Bilmek
Bence, kesinlikle yazılım geliştirme ile ilgili olarak, bir kavramı bilmenin en önemli şekli, onu çalışan uygulama kodunda kullanabilmekten gelir. Bu bilme biçimi, sadece çok sayıda kod yazarak ve birçok farklı problemi çözerek elde edilebilir; jargon adları ve sözlü tanımların dahil edilmesi gerekmez.
Kendi deneyimlerime göre, uzak bir veritabanıyla ve yerel bir veritabanıyla tek bir arabirim aracılığıyla iletişim kurma sorununu çözdüğümü hatırlıyorum (henüz bilmiyorsanız, bunun ne anlama geldiğini yakında anlayacaksınız); uzak ve yerel (veya hatta bir test veritabanını) açıkça çağırması gereken istemci ( arayüzle konuşan sınıf ne olursa olsun) yerine. Aslında, müşterinin arayüzün arkasında ne olduğu hakkında hiçbir fikri yoktu, bu yüzden bir üretim uygulamasında mı yoksa bir test ortamında mı çalışıyor olursa olsun onu değiştirmem gerekmedi. Bu sorunu çözdükten yaklaşık bir yıl sonra, insanların daha önce açıklanan çözüm için kullandıkları isimler olan “Depo Kalıbı” teriminden çok geçmeden “Cephe Modeli” terimiyle karşılaştım.
Tüm bu önsöz, kalıtım , arayüzler ve soyut sınıflar gibi konuların açıklanmasında sıklıkla yapılan bazı kusurları umarız aydınlatır. Üçünden kalıtım , muhtemelen hem kullanması hem de anlaması en basit olanıdır. Hem bir programlama öğrencisi hem de bir öğretmen olarak deneyimlerime göre, daha önce tartışılan hatalardan kaçınmaya çok özel bir dikkat gösterilmedikçe, diğer ikisi neredeyse her zaman öğrenciler için bir sorundur. Bu noktadan sonra, bu konuları olması gerektiği kadar basit hale getirmek için elimden gelenin en iyisini yapacağım, ancak daha basit değil.
Örnekler Üzerine Bir Not
Android mobil uygulama geliştirmede en akıcı olduğum için, bu platformdan alınan örnekleri kullanacağım, böylece size GUI uygulamaları oluşturmayı öğretirken aynı zamanda Java'nın dil özelliklerini tanıtacağım. Ancak, Java EE, Swing veya JavaFX'i üstünkörü anlayan biri tarafından örneklerin anlaşılmaması için çok fazla ayrıntıya girmeyeceğim. Bu konuları tartışmaktaki nihai amacım, hemen hemen her türlü uygulamada bir sorunu çözme bağlamında ne anlama geldiklerini anlamanıza yardımcı olmaktır.
Ayrıca, sevgili okuyucu, bazen belirli kelimeler ve tanımları hakkında gereksiz yere felsefi ve bilgiçlik yapıyormuşum gibi görünebileceğim konusunda sizi uyarmak isterim. Bunun nedeni, somut (gerçek) bir şey ile soyut (gerçek bir şeyden daha az ayrıntılı) olan bir şey arasındaki farkı anlamak için gerçekten derin bir felsefi temelin gerekli olmasıdır. Bu anlayış, bilgi işlem alanı dışındaki pek çok şey için geçerlidir, ancak herhangi bir yazılım geliştiricisinin soyutlamaların doğasını kavraması özellikle çok önemlidir. Her durumda, sözlerim başarısız olursa, koddaki örnekler umarım olmaz.
Miras ve Uygulama
Bir grafik kullanıcı arabirimi (GUI) ile uygulamalar oluşturmaya gelince, kalıtım , bir uygulamayı hızlı bir şekilde oluşturmayı mümkün kılmak için tartışmasız en önemli mekanizmadır.
Daha sonra tartışılacak olan mirası kullanmanın daha az anlaşılan bir faydası olsa da , birincil fayda, uygulamayı sınıflar arasında paylaşmaktır . Bu "uygulama" kelimesi, en azından bu makalenin amaçları açısından ayrı bir anlama sahiptir. İngilizce kelimenin genel bir tanımını vermek gerekirse, bir şeyi uygulamak , onu gerçeğe dönüştürmektir diyebilirim.
Yazılım geliştirmeye özgü teknik bir tanım vermek gerekirse, bir yazılım parçasını uygulamak , söz konusu yazılım parçasının gereksinimlerini karşılayan somut kod satırları yazmaktır diyebilirim. Örneğin, bir toplama yöntemi yazdığımı varsayalım: private double sum(double first, double second){
private double sum(double first, double second){ //TODO: implement }
Yukarıdaki pasaj, bir dönüş türü ( double
) ve argümanları ( first, second
) ve adı geçen yöntemi ( sum
) çağırmak için kullanılabilecek adı belirten bir yöntem bildirimi yazmaya kadar yapmış olsam da, uygulanmadı . Bunu uygulamak için yöntem gövdesini şu şekilde tamamlamalıyız:
private double sum(double first, double second){ return first + second; }
Doğal olarak, ilk örnek derlenmeyecek, ancak bir an için arayüzlerin bu tür uygulanmamış fonksiyonları hatasız yazabilmemizin bir yolu olduğunu göreceğiz.
Java'da Kalıtım
Muhtemelen, bu makaleyi okuyorsanız, en az bir kez extends
Java anahtar sözcüğünü kullanmışsınızdır. Bu anahtar kelimenin mekaniği basittir ve çoğunlukla farklı hayvan türleri veya geometrik şekillerle ilgili örnekler kullanılarak tanımlanır; Dog
ve Cat
, Animal
öğesini genişletir ve bu şekilde devam eder. İlkel tip teorisini size açıklamam gerekmediğini varsayacağım, bu yüzden, Java'da kalıtımın birincil faydasına, extends
anahtar sözcüğü aracılığıyla doğrudan geçelim.
Java'da konsol tabanlı bir “Merhaba Dünya” uygulaması oluşturmak çok basittir. Bir Java Derleyicisine ( javac ) ve çalışma zamanı ortamına ( jre ) sahip olduğunuzu varsayarsak, şöyle bir ana işlevi içeren bir sınıf yazabilirsiniz:
public class JavaApp{ public static void main(String []args){ System.out.println("Hello World"); } }
Java'da neredeyse tüm ana platformlarında (Android, Enterprise/Web, Desktop) yeni bir uygulamanın iskelet/boilerplate kodunu oluşturmak için bir IDE'den biraz yardım alarak bir GUI uygulaması oluşturmak, aynı zamanda nispeten kolaydır. anahtar kelimeyi extends
.
activity_main.xml
adında bir XML Düzenimiz olduğunu varsayalım (genellikle Android'de, Düzen dosyaları aracılığıyla bildirimsel olarak kullanıcı arabirimleri oluştururuz) tvDisplay
adlı bir TextView
(metin etiketi gibi) içeren:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </FrameLayout>
Ayrıca, tvDisplay
“Merhaba Dünya!” demesini istediğimizi varsayalım. Bunu yapmak için, Activity
sınıfından kalıtım almak için extends
anahtar sözcüğünü kullanan bir sınıf yazmamız yeterlidir:
import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((TextView)findViewById(R.id.tvDisplay)).setText("Hello World"); }
Activity
sınıfının uygulanmasını devralmanın etkisi, en iyi kaynak koduna hızlıca göz atılarak anlaşılabilir. Sistemle etkileşim kurmak için gerekli olan 8000'den fazla satırın küçük bir bölümünü bile uygulamak , sadece biraz metin içeren basit bir pencere oluşturmak için bile Android'in baskın mobil platform haline geleceğinden şüpheliyim. Kalıtım , Android çerçevesini veya birlikte çalıştığınız herhangi bir platformu sıfırdan yeniden oluşturmak zorunda kalmamamıza izin veren şeydir.
Kalıtım Soyutlama İçin Kullanılabilir
Sınıflar arasında uygulamayı paylaşmak için kullanılabildiği sürece, mirasın anlaşılması nispeten basittir. Ancak, kalıtımın kullanılabileceği bir başka önemli yol daha vardır ki bu, kavramsal olarak, birazdan tartışacağımız arayüzler ve soyut sınıflarla ilgilidir.
Dilerseniz, bir an için, en genel anlamda kullanılan bir soyutlamanın, bir şeyin daha az ayrıntılı bir temsili olduğunu varsayalım. Bunu uzun bir felsefi tanımla nitelendirmek yerine, soyutlamaların günlük hayatta nasıl çalıştığına işaret etmeye çalışacağım ve kısa bir süre sonra bunları yazılım geliştirme açısından açık bir şekilde tartışacağım.
Avustralya'ya seyahat ettiğinizi ve ziyaret ettiğiniz bölgenin özellikle yüksek yoğunlukta iç taipan yılanlarına ev sahipliği yaptığını bildiğinizi varsayalım (görünüşe göre oldukça zehirlidirler). Resimlere ve diğer bilgilere bakarak onlar hakkında daha fazla bilgi edinmek için Wikipedia'ya başvurmaya karar verdiniz. Bunu yaparak, daha önce hiç görmediğiniz belirli bir yılan türünün kesin olarak farkında olursunuz.
Soyutlamalar, fikirler, modeller veya bunlara başka ne ad vermek isterseniz, bir şeyin daha az ayrıntılı temsilleridir. Gerçeğinden daha az ayrıntılı olmaları önemlidir çünkü gerçek bir yılan sizi ısırabilir; Wikipedia sayfalarındaki resimler genellikle böyle değildir. Soyutlamalar da önemlidir çünkü hem bilgisayarlar hem de insan beyni bilgi depolamak, iletmek ve işlemek için sınırlı bir kapasiteye sahiptir. Bu bilgileri hafızada fazla yer kaplamadan pratik bir şekilde kullanacak kadar detaya sahip olmak, bilgisayarların ve insan beyninin problem çözmesini mümkün kılıyor.
Bunu tekrar kalıtımla ilişkilendirmek için burada tartıştığım üç ana konunun tümü soyutlamalar veya soyutlama mekanizmaları olarak kullanılabilir. "Merhaba Dünya" uygulamamızın düzen dosyasına bir ImageView
, Button
ve ImageButton
eklemeye karar verdiğimizi varsayalım:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageButton android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
Ayrıca, Etkinliğimizin tıklamaları işlemek için View.OnClickListener
uyguladığını varsayalım:
public class MainActivity extends Activity implements View.OnClickListener { private Button b; private ImageButton ib; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... b = findViewById(R.id.imvDisplay).setOnClickListener(this); ib = findViewById(R.id.btnDisplay).setOnClickListener(this); iv = findViewById(R.id.imbDisplay).setOnClickListener(this); } @Override public void onClick(View view) { final int id = view.getId(); //handle click based on id... } }
Buradaki temel ilke, Button
, ImageButton
ve ImageView
öğelerinin View
sınıfından devralmasıdır. Sonuç olarak, bu onClick
işlevi, farklı (hiyerarşik olarak ilişkili olsa da) UI öğelerinden daha az ayrıntılı üst sınıfları olarak başvurarak tıklama olaylarını alabilir. Bu, Android platformunda her tür widget'ı işlemek için ayrı bir yöntem yazmak zorunda kalmaktan (özel widget'lardan bahsetmeden) çok daha uygundur.
Arayüzler ve Soyutlama
Neden seçtiğimi anlamış olsanız bile, önceki kod örneğini biraz sönük bulmuş olabilirsiniz. Uygulamayı bir sınıf hiyerarşisi arasında paylaşabilmek inanılmaz derecede faydalıdır ve bunun mirasın birincil faydası olduğunu iddia ediyorum. Ortak bir üst sınıfa sahip bir sınıflar kümesini tür bakımından eşit olarak (yani üst sınıf olarak) ele almamıza gelince, kalıtımın bu özelliğinin kullanımı sınırlıdır.
Sınırlı olarak, alt sınıfların, ana sınıf olarak bilinebilmesi veya aracılığıyla referans alınabilmesi için aynı sınıf hiyerarşisi içinde olması gerekliliğinden bahsediyorum. Başka bir deyişle, kalıtım , soyutlama için çok kısıtlayıcı bir mekanizmadır. Aslında, soyutlamanın farklı ayrıntı (veya bilgi) seviyeleri arasında hareket eden bir spektrum olduğunu varsayarsam, kalıtımın Java'daki soyutlama için en az soyut mekanizma olduğunu söyleyebilirim.
Arayüzleri tartışmaya geçmeden önce Java 8'den itibaren arayüzlere Default Methods ve Static Methods adlı iki özelliğin eklendiğini belirtmek isterim. Onları eninde sonunda tartışacağım, ama şimdilik onlar yokmuş gibi davranmamızı istiyorum. Bu, Java'daki soyutlama için başlangıçta ve muhtemelen hala en soyut mekanizma olan bir interface kullanmanın birincil amacını açıklamamı kolaylaştırmak içindir.
Daha Az Detay Daha Fazla Özgürlük Demektir
Mirasla ilgili bölümde, şimdi gireceğimiz başka bir terimle zıtlık oluşturması amaçlanan uygulama kelimesinin bir tanımını verdim. Açık olmak gerekirse, kelimelerin kendileri veya kullanımlarına katılıp katılmadığınız umurumda değil; sadece kavramsal olarak neyi işaret ettiklerini anladığınızdan emin olun.
Kalıtım öncelikle bir dizi sınıf arasında uygulamayı paylaşmak için bir araç iken, arayüzlerin öncelikle bir dizi sınıf arasında davranışı paylaşmak için bir mekanizma olduğunu söyleyebiliriz. Bu anlamda kullanılan davranış , soyut yöntemler için gerçekten teknik olmayan bir kelimedir. Soyut bir yöntem , aslında bir yöntem gövdesi içermeyen bir yöntemdir:
public interface OnClickListener { void onClick(View v); }
Arayüze ilk kez baktıktan sonra benim ve eğitim verdiğim birkaç kişinin doğal tepkisi, yalnızca bir dönüş türü , yöntem adı ve parametre listesi paylaşmanın faydasının ne olabileceğini merak etmek oldu. Yüzeyde, kendiniz veya arayüzü implements
sınıfı yazan başka biri için fazladan iş yaratmanın harika bir yolu gibi görünüyor. Cevap, arayüzlerin , bir sınıflar kümesinin aynı şekilde davranmasını istediğiniz durumlar için mükemmel olduğudur (yani, aynı genel soyut yöntemlere sahiptirler), ancak onlardan bu davranışı farklı şekillerde uygulamalarını beklersiniz.
Basit ama alakalı bir örnek vermek gerekirse, Android platformu, öncelikle kullanıcı arayüzünün bir bölümünü oluşturma ve yönetme işinde olan iki sınıfa sahiptir: Activity
ve Fragment
. Bu sınıfların çoğu zaman, bir widget tıklandığında (veya bir kullanıcı tarafından başka bir şekilde etkileşime girdiğinde) ortaya çıkan olayları dinleme gereksinimine sahip olacağı izler. Tartışmanın hatırına, mirasın neden böyle bir sorunu neredeyse hiç çözmediğini anlamak için bir dakikanızı ayıralım:
public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... } }
Aktivitelerimizi ve Fragmanlarımızı OnClickManager
devralmak, olayları farklı bir şekilde ele almayı imkansız kılmakla kalmaz, işin can alıcı noktası, istesek bunu bile yapamayacak olmamızdır. Hem Activity hem de Fragment bir üst sınıfı zaten genişletiyor ve Java birden çok üst sınıfa izin vermiyor. Dolayısıyla bizim sorunumuz, bir dizi sınıfın aynı şekilde davranmasını istememizdir, ancak sınıfın bu davranışı nasıl uygulayacağı konusunda esnekliğe sahip olmamız gerekir . Bu bizi View.OnClickListener
önceki örneğine geri getiriyor:
public interface OnClickListener { void onClick(View v); }
Bu, gerçek kaynak kodudur ( View
sınıfında iç içedir) ve bu birkaç satır, farklı widget'lar ( Views ) ve UI denetleyicileri ( Activity, Fragments, vb. ) arasında tutarlı davranış sağlamamıza olanak tanır.
Soyutlama, Gevşek Bağlantıyı Teşvik Eder
Java'da arayüzlerin neden var olduğuyla ilgili genel soruyu umarım yanıtlamış oldum; diğer birçok dil arasında. Bir açıdan, bunlar sadece sınıflar arasında kod paylaşmanın bir yoludur, ancak farklı uygulamalara izin vermek için kasıtlı olarak daha az ayrıntılıdırlar. Ancak kalıtımın hem kod hem de soyutlama için bir mekanizma olarak kullanılabilmesi gibi (sınıf hiyerarşisindeki kısıtlamalarla da olsa), arayüzlerin soyutlama için daha esnek bir mekanizma sağladığı sonucu çıkar.
Bu makalenin daha önceki bir bölümünde, bir tür yapı oluşturmak için çivi ve vida kullanma arasındaki farka benzeterek gevşek/sıkı bağlantı konusunu tanıtmıştım. Özetlemek gerekirse, temel fikir, mevcut yapının değiştirilmesinin (hataların, tasarım değişikliklerinin ve benzerlerinin bir sonucu olabilecek) muhtemel olduğu durumlarda vidaları kullanmak isteyeceğinizdir. Çiviler , yapının parçalarını birbirine tutturmanız gerektiğinde ve yakın gelecekte onları ayırmaktan özellikle endişe duymadığınızda kullanmak için uygundur.
Çiviler ve vidalar , sınıflar arasındaki somut ve soyut referanslara ( bağımlılıklar terimi de geçerlidir) benzerdir. Karışıklık olmasın diye, aşağıdaki örnek ne demek istediğimi gösterecek:
class Client { private Validator validator; private INetworkAdapter networkAdapter; void sendNetworkRequest(String input){ if (validator.validateInput(input)) { try { networkAdapter.sendRequest(input); } catch (IOException e){ //handle exception } } } } class Validator { //...validation logic boolean validateInput(String input){ boolean isValid = true; //...change isValid to false based on validation logic return isValid; } } interface INetworkAdapter { //... void sendRequest(String input) throws IOException; }
Burada, iki tür referansa sahip olan Client
adında bir sınıfımız var. Client
referanslarını oluşturmakla hiçbir ilgisi olmadığı varsayıldığında (gerçekten yapmaması gerekir), herhangi bir belirli ağ bağdaştırıcısının uygulama ayrıntılarından ayrıldığına dikkat edin.
Bu gevşek bağlantının birkaç önemli sonucu vardır. Yeni başlayanlar için, herhangi bir INetworkAdapter
uygulamasının mutlak izolasyonunda Client
oluşturabilirim. Bir an için iki geliştiriciden oluşan bir ekipte çalıştığınızı hayal edin; biri ön ucu oluşturmak için, diğeri arka ucu oluşturmak için. Her iki geliştirici de kendi sınıflarını birbirine bağlayan arayüzlerin farkında oldukları sürece, çalışmalarını neredeyse birbirinden bağımsız olarak sürdürebilirler.
İkinci olarak, ya size her iki geliştiricinin de kendi uygulamalarının düzgün çalıştığını, ayrıca birbirlerinin ilerlemesinden bağımsız olarak doğrulayabileceğini söyleseydim? Arayüzlerle bu çok kolaydır; sadece uygun arayüzü implements
bir Test Çifti oluşturun:
class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); } }
Prensip olarak, soyut referanslarla çalışmanın artan modülerlik, test edilebilirlik ve Cephe Modeli , Gözlemci Modeli ve daha fazlası gibi bazı çok güçlü tasarım modellerine kapı açtığı gözlemlenebilir. Ayrıca geliştiricilerin, uygulama ayrıntılarında boğulmadan, davranışa dayalı olarak bir sistemin farklı bölümlerini (Bir Arayüze Programlama ) tasarlama konusunda mutlu bir denge bulmalarına izin verebilirler.
Soyutlamalarda Son Nokta
Soyutlamalar , somut bir şey gibi var olmazlar. Bu, Java Programlama dilinde, soyut sınıfların ve arayüzlerin somutlaştırılamaması gerçeğiyle yansıtılır.
Örneğin, bu kesinlikle derlenmez:
public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { //ERROR x2: Foo f = new Foo(); Bar b = new Bar() } private abstract class Foo{} private interface Bar{} }
Aslında, uygulanmamış bir arabirimin veya soyut sınıfın çalışma zamanında çalışmasını beklemek, UPS üniformasının paketleri teslim etmesini beklemek kadar mantıklıdır. Yararlı olması için soyutlamanın arkasında somut bir şey olmalıdır; çağıran sınıfın aslında soyut referansların arkasında ne olduğunu bilmesi gerekmese bile.
Soyut Sınıflar: Hepsini Bir Araya Getirmek
Buraya kadar geldiyseniz, çevirmek için artık felsefi teğet veya jargon cümlem olmadığını söylemekten mutluluk duyarım. Basitçe söylemek gerekirse, soyut sınıflar , bir dizi sınıf arasında uygulama ve davranışı paylaşmak için bir mekanizmadır. Şimdi, kendimi soyut dersleri bu kadar sık kullandığımı hemen kabul edeceğim. Öyle olsa bile, bu bölümün sonunda tam olarak ne zaman çağrıldıklarını öğreneceğinizi umuyorum.
Antrenman Günlüğü Vaka Çalışması
Java'da Android uygulamaları oluşturmaya yaklaşık bir yıl sonra, ilk Android uygulamamı sıfırdan yeniden oluşturuyordum. İlk sürüm, çok az rehberlikle kendi kendini yetiştirmiş bir geliştiriciden bekleyeceğiniz türden korkunç bir kod yığınıydı. Yeni işlevler eklemek istediğimde, yalnızca çivilerle oluşturduğum sıkı bağlantılı yapıyı korumak o kadar imkansızdı ki, onu tamamen yeniden inşa etmem gerekiyordu.
Uygulama, antrenmanlarınızın kolayca kaydedilmesini ve geçmiş bir antrenmanın verilerini bir metin veya resim dosyası olarak çıktı olarak verebilmesini sağlamak için tasarlanmış bir antrenman günlüğüydü. Çok fazla ayrıntıya girmeden, uygulamanın veri modellerini, bir Exercise
nesneleri koleksiyonundan oluşan bir Workout
nesnesi olacak şekilde yapılandırdım (bu tartışmayla ilgisi olmayan diğer alanların yanı sıra).
Antrenman verilerini bir tür görsel ortama aktarma özelliğini uygularken, bir sorunla ilgilenmem gerektiğini fark ettim: Farklı türde alıştırmalar, farklı türde metin çıktıları gerektirir.
Size kabaca bir fikir vermek için, çıktıları egzersizin türüne göre şu şekilde değiştirmek istedim:
- Halter: 100 LBS'de 10 TEKRAR
- Dambıl: 10 TEKRAR @ 50 LBS x2
- Vücut Ağırlığı: 10 Tekrar @ Vücut Ağırlığı
- Vücut Ağırlığı +: 10 Tekrar @ Vücut Ağırlığı + 45 LBS
- Süreli: 60 SANİYE @ 100 LBS
Devam etmeden önce, başka türler olduğunu (egzersiz yapmanın karmaşık hale gelebileceğini) ve göstereceğim kodun kısaltıldığını ve bir makaleye güzel bir şekilde sığması için değiştirildiğini unutmayın.
Daha önceki tanımıma uygun olarak, soyut bir sınıf yazmanın amacı, soyut sınıftaki tüm alt sınıflar arasında paylaşılan her şeyi ( değişkenler ve sabitler gibi durumlar bile) uygulamaktır . Ardından, söz konusu alt sınıflar arasında değişen herhangi bir şey için soyut bir yöntem oluşturun:
abstract class Exercise { private final String type; protected final String name; protected final int[] repetitionsOrTime; protected final double[] weight; protected static final String POUNDS = "LBS"; protected static final String SECONDS = "SEC "; protected static final String REPETITIONS = "REPS "; public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) { this.type = type; this.name = name; this.repetitionsOrTime = repetitionsOrTime; this.weight = weight; } public String getFormattedOutput(){ StringBuilder sb = new StringBuilder(); sb.append(name); sb.append("\n"); getSetData(sb); sb.append("\n"); return sb.toString(); } /** * Append data appropriately based on Exercise type * @param sb - StringBuilder to Append data to */ protected abstract void getSetData(StringBuilder sb); //...Getters }
Bariz olanı belirtiyor olabilirim, ancak soyut sınıfta neyin uygulanması veya neyin uygulanmaması gerektiği hakkında herhangi bir sorunuz varsa, anahtar, tüm alt sınıflarda tekrarlanan uygulamanın herhangi bir bölümüne bakmaktır.
Artık tüm alıştırmalar arasında ortak olanı belirlediğimize göre, her tür String çıktısı için uzmanlıkları olan alt sınıflar oluşturmaya başlayabiliriz:
Halter Egzersizi:
class BarbellExercise extends Exercise { public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append("\n"); } } }
Dambıl Egzersizi:
class DumbbellExercise extends Exercise { private static final String TIMES_TWO = "x2"; public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append(TIMES_TWO); sb.append("\n"); } } }
Vücut Ağırlığı Egzersizi:
class BodyweightExercise extends Exercise { private static final String BODYWEIGHT = "Bodyweight"; public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(BODYWEIGHT); sb.append("\n"); } } }
Bazı zeki okuyucuların daha verimli bir şekilde soyutlanabilecek şeyler bulacağından eminim, ancak bu örneğin (orijinal kaynaktan basitleştirilmiş) amacı genel yaklaşımı göstermektir. Elbette, yürütülebilecek bir şey olmadan hiçbir programlama makalesi tamamlanmış sayılmaz. Test etmek istiyorsanız (zaten bir IDE'niz yoksa) bu kodu çalıştırmak için kullanabileceğiniz birkaç çevrimiçi Java derleyicisi vardır:
public class Main { public static void main(String[] args) { //Note: I actually used another nested class called a "Set" instead of an Array //to represent each Set of an Exercise. int[] reps = {10, 10, 8}; double[] weight = {70.0, 70.0, 70.0}; Exercise e1 = new BarbellExercise( "Barbell", "Barbell Bench Press", reps, weight ); Exercise e2 = new DumbbellExercise( "Dumbbell", "Dumbbell Bench Press", reps, weight ); Exercise e3 = new BodyweightExercise( "Bodyweight", "Push Up", reps, weight ); System.out.println( e1.getFormattedOutput() + e2.getFormattedOutput() + e3.getFormattedOutput() ); } }
Executing this toy application yields the following output: Barbell Bench Press
10 REPS @ 70.0LBS 10 REPS @ 70.0LBS 8 REPS @ 70.0LBS Dumbbell Bench Press 10 REPS @ 70.0LBSx2 10 REPS @ 70.0LBSx2 8 REPS @ 70.0LBSx2 Push Up 10 REPS @ Bodyweight 10 REPS @ Bodyweight 8 REPS @ Bodyweight
Further Considerations
Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementation , as opposed to behavior . These features are known as Default Methods and Static Methods .
I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.
I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods ) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance . A common pattern in Java is to write what is known as a Utility class, which is a simple class
containing the requisite implementation in a static method :
public class TimeConverterUtil { /** * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate * format such as 12, 30 -> 12:30 pm */ public static String convertTime (int hour, int minute){ String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute); DateFormat f1 = new SimpleDateFormat("HH:mm"); Date d = null; try { d = f1.parse(unformattedTime); } catch (ParseException e) { e.printStackTrace(); } DateFormat f2 = new SimpleDateFormat("h:mm a"); return f2.format(d).toLowerCase(); } }
Using this static method in an external class (or another static method ) looks like this:
public class Main { public static void main(String[] args){ //... String time = TimeConverterUtil.convertTime(12, 30); //... } }
Cheat Sheet
We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.
I Want A Set Of Child Classes To Share Implementation
Classic inheritance , which requires a child class to inherit from a parent class , is a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent class , is to see whether it is repeated in a number of different classes line for line. The acronym DRY ( Don't Repeat Yourself ) is a good mnemonic device to watch out for this situation.
While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent class , which provides a limited degree of abstraction .
I Want A Set Of Classes To Share Behavior
Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior ), but you do not expect the implementation of that behavior to be repeated across inheritors.
By definition, Java interfaces may not contain any implementation (except for Default and Static Methods ), but any class which implements an interface , must supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy .
I Want A Set Of Child Classes To Share Behavior And Implementation
Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract class
, and anything which requires flexibility may be specified as an abstract method .