Menguasai OOP: Panduan Praktis untuk Warisan, Antarmuka, dan Kelas Abstrak
Diterbitkan: 2022-03-10Sejauh yang saya tahu, jarang menemukan konten pendidikan di bidang pengembangan perangkat lunak yang menyediakan campuran yang sesuai antara informasi teoretis dan praktis. Jika saya harus menebak mengapa, saya berasumsi itu karena individu yang fokus pada teori cenderung masuk ke dalam pengajaran, dan individu yang fokus pada informasi praktis cenderung dibayar untuk memecahkan masalah tertentu, menggunakan bahasa dan alat tertentu.
Ini, tentu saja, generalisasi yang luas, tetapi jika kita menerimanya secara singkat untuk kepentingan argumen, maka banyak orang (tidak semua orang) yang mengambil peran guru, cenderung miskin, atau sama sekali tidak mampu. menjelaskan pengetahuan praktis yang relevan dengan konsep tertentu.
Pada artikel ini, saya akan melakukan yang terbaik untuk membahas tiga mekanisme inti yang akan Anda temukan di sebagian besar bahasa Pemrograman Berorientasi Objek (OOP): Inheritance , interfaces (alias protokol ), dan abstract class . Daripada memberi Anda penjelasan verbal teknis dan kompleks tentang apa masing-masing mekanisme itu , saya akan melakukan yang terbaik untuk fokus pada apa yang mereka lakukan , dan kapan menggunakannya.
Namun, sebelum membahasnya satu per satu, saya ingin membahas secara singkat apa artinya memberikan penjelasan yang secara teoritis masuk akal, namun secara praktis tidak berguna. Harapan saya adalah Anda dapat menggunakan informasi ini untuk membantu Anda menyaring berbagai sumber pendidikan dan menghindari menyalahkan diri sendiri ketika ada hal yang tidak masuk akal.
Derajat Pengetahuan yang Berbeda
Mengetahui Nama
Mengetahui nama sesuatu bisa dibilang merupakan bentuk mengetahui yang paling dangkal. Faktanya, sebuah nama hanya berguna secara umum sejauh nama itu biasa digunakan oleh banyak orang untuk merujuk pada hal yang sama dan/atau membantu untuk mendeskripsikan hal tersebut. Sayangnya, seperti yang telah ditemukan oleh siapa pun yang telah menghabiskan waktu di bidang ini, banyak orang menggunakan nama yang berbeda untuk hal yang sama (misalnya antarmuka dan protokol ), nama yang sama untuk hal yang berbeda (misalnya modul dan komponen ), atau nama yang bersifat esoterik untuk titik menjadi tidak masuk akal (mis. Entah Monad ). Pada akhirnya, nama hanyalah penunjuk (atau referensi) untuk model mental, dan mereka dapat memiliki berbagai tingkat kegunaan.
Untuk membuat bidang ini lebih sulit untuk dipelajari, saya berani menebak bahwa bagi kebanyakan individu, menulis kode adalah (atau setidaknya) pengalaman yang sangat unik. Yang lebih rumit lagi adalah memahami bagaimana kode itu akhirnya dikompilasi ke dalam bahasa mesin, dan direpresentasikan dalam realitas fisik sebagai serangkaian impuls listrik yang berubah seiring waktu. Bahkan jika seseorang dapat mengingat nama proses, konsep, dan mekanisme yang digunakan dalam suatu program, tidak ada jaminan bahwa model mental yang dibuat seseorang untuk hal-hal seperti itu konsisten dengan model individu lain; apalagi apakah mereka akurat secara objektif.
Karena alasan inilah, di samping fakta bahwa saya tidak memiliki ingatan yang baik secara alami untuk jargon, saya menganggap nama sebagai aspek yang paling tidak penting untuk mengetahui sesuatu. Itu bukan untuk mengatakan bahwa nama tidak berguna, tetapi di masa lalu saya telah mempelajari dan menggunakan banyak pola desain dalam proyek saya, hanya untuk mempelajari nama bulan yang umum digunakan, atau bahkan bertahun-tahun kemudian.
Mengetahui Definisi Verbal Dan Analoginya
Definisi verbal adalah titik awal alami untuk menggambarkan konsep baru. Namun, seperti halnya nama, mereka dapat memiliki tingkat kegunaan dan relevansi yang berbeda-beda; banyak dari itu tergantung pada apa tujuan akhir pelajar. Masalah paling umum yang saya lihat dalam definisi verbal adalah asumsi pengetahuan biasanya dalam bentuk jargon.
Misalkan misalnya, saya harus menjelaskan bahwa utas sangat mirip dengan proses , kecuali bahwa utas menempati ruang alamat yang sama dari proses yang diberikan. Untuk seseorang yang sudah akrab dengan proses dan ruang alamat , saya pada dasarnya menyatakan bahwa benang dapat dikaitkan dengan pemahaman mereka tentang suatu proses (yaitu mereka memiliki banyak karakteristik yang sama), tetapi mereka dapat dibedakan berdasarkan karakteristik yang berbeda.
Untuk seseorang yang tidak memiliki pengetahuan itu, saya paling tidak masuk akal, dan paling buruk menyebabkan pelajar merasa tidak mampu dalam beberapa hal karena tidak mengetahui hal-hal yang saya anggap mereka harus tahu. Sejujurnya, ini dapat diterima jika pelajar Anda benar-benar harus memiliki pengetahuan seperti itu (seperti mengajar mahasiswa pascasarjana atau pengembang berpengalaman), tetapi saya menganggapnya sebagai kegagalan monumental untuk melakukannya dalam materi tingkat pengantar apa pun.
Seringkali sangat sulit untuk memberikan definisi verbal yang baik dari suatu konsep ketika tidak seperti apa pun yang pernah dilihat pelajar sebelumnya. Dalam hal ini, sangat penting bagi guru untuk memilih analogi yang mungkin akrab bagi kebanyakan orang, dan juga relevan sejauh itu menyampaikan banyak kualitas konsep yang sama.
Misalnya, sangat penting bagi pengembang perangkat lunak untuk memahami apa artinya ketika entitas perangkat lunak (bagian yang berbeda dari suatu program) digabungkan dengan erat atau digabungkan secara longgar . Saat membangun gudang taman, seorang tukang kayu junior mungkin berpikir bahwa lebih cepat dan lebih mudah untuk menyatukannya menggunakan paku daripada sekrup. Ini berlaku sampai titik di mana kesalahan dibuat, atau perubahan dalam desain gudang taman mengharuskan pembangunan kembali bagian gudang.
Pada titik ini, keputusan untuk menggunakan paku untuk menyatukan bagian - bagian gudang kebun, telah membuat proses konstruksi secara keseluruhan lebih sulit, mungkin lebih lambat, dan mencabut paku dengan palu berisiko merusak struktur. Sebaliknya, sekrup membutuhkan sedikit waktu ekstra untuk dirakit, tetapi sekrup mudah dilepas dan berisiko kecil merusak bagian gudang terdekat. Inilah yang saya maksud dengan loosely-coupled . Secara alami, ada kasus di mana Anda benar-benar hanya membutuhkan paku, tetapi keputusan itu harus dipandu oleh pemikiran dan pengalaman kritis.
Seperti yang akan saya bahas secara rinci nanti, ada mekanisme yang berbeda untuk menghubungkan bagian-bagian dari sebuah program bersama-sama yang menyediakan berbagai tingkat kopling ; seperti paku dan sekrup . Sementara analogi saya mungkin telah membantu Anda memahami apa arti istilah yang sangat penting ini, saya belum memberi Anda ide tentang bagaimana menerapkannya di luar konteks membangun gudang taman. Ini membawa saya pada jenis pengetahuan yang paling penting, dan kunci untuk memahami secara mendalam konsep-konsep yang kabur dan sulit dalam bidang penyelidikan apa pun; meskipun kami akan tetap menulis kode di artikel ini.
Mengetahui Dalam Kode
Menurut pendapat saya, secara ketat mengenai pengembangan perangkat lunak, bentuk paling penting untuk mengetahui suatu konsep berasal dari kemampuan untuk menggunakannya dalam kode aplikasi yang berfungsi. Bentuk mengetahui ini dapat dicapai hanya dengan menulis banyak kode dan memecahkan banyak masalah yang berbeda; nama jargon dan definisi verbal tidak perlu disertakan.
Dalam pengalaman saya sendiri, saya ingat memecahkan masalah komunikasi dengan database jarak jauh, dan database lokal melalui satu antarmuka (Anda akan tahu apa artinya segera jika Anda belum melakukannya); daripada klien (kelas apa pun yang berbicara dengan antarmuka ) yang perlu memanggil remote dan lokal (atau bahkan database pengujian) secara eksplisit. Faktanya, klien tidak tahu apa yang ada di balik antarmuka, jadi saya tidak perlu mengubahnya terlepas dari apakah itu berjalan di aplikasi produksi atau lingkungan pengujian. Sekitar setahun setelah saya memecahkan masalah ini, saya menemukan istilah "Pola Fasad", dan tidak lama setelah istilah "Pola Repositori", yang merupakan kedua nama yang digunakan orang untuk solusi yang dijelaskan sebelumnya.
Semua pembukaan ini diharapkan dapat menjelaskan beberapa kekurangan yang paling sering dibuat dalam menjelaskan topik seperti pewarisan , antarmuka , dan kelas abstrak . Dari ketiganya, pewarisan kemungkinan adalah yang paling sederhana untuk digunakan dan dipahami. Dalam pengalaman saya baik sebagai siswa pemrograman, dan guru, dua lainnya hampir selalu menjadi masalah bagi pelajar kecuali perhatian yang sangat khusus diberikan untuk menghindari kesalahan yang dibahas sebelumnya. Mulai saat ini, saya akan melakukan yang terbaik untuk membuat topik ini sesederhana yang seharusnya, tetapi tidak lebih sederhana.
Catatan Tentang Contoh
Karena saya sendiri yang paling fasih dalam pengembangan aplikasi seluler Android, saya akan menggunakan contoh yang diambil dari platform itu sehingga saya dapat mengajari Anda tentang membangun aplikasi GUI sekaligus memperkenalkan fitur bahasa Java. Namun, saya tidak akan membahas terlalu detail bahwa contoh tersebut seharusnya tidak dapat dipahami oleh seseorang yang memiliki pemahaman sepintas tentang Java EE, Swing atau JavaFX. Tujuan utama saya dalam membahas topik-topik ini adalah untuk membantu Anda memahami apa artinya dalam konteks pemecahan masalah di hampir semua jenis aplikasi.
Saya juga ingin memperingatkan Anda, pembaca yang budiman, bahwa kadang-kadang saya tampak seperti filosofis dan bertele-tele yang tidak perlu tentang kata-kata tertentu dan definisinya. Alasannya, memang diperlukan landasan filosofis yang mendalam untuk memahami perbedaan antara sesuatu yang konkret (nyata), dan sesuatu yang abstrak (kurang detail dari sesuatu yang nyata). Pemahaman ini berlaku untuk banyak hal di luar bidang komputasi, tetapi sangat penting bagi setiap pengembang perangkat lunak untuk memahami sifat abstraksi . Bagaimanapun, jika kata-kata saya mengecewakan Anda, contoh-contoh dalam kode mudah-mudahan tidak.
Warisan Dan Implementasi
Ketika datang untuk membangun aplikasi dengan antarmuka pengguna grafis (GUI), pewarisan bisa dibilang mekanisme paling penting untuk memungkinkan membangun aplikasi dengan cepat.
Meskipun ada manfaat yang kurang dipahami untuk menggunakan warisan yang akan dibahas nanti, manfaat utama adalah untuk berbagi implementasi antar kelas . Kata “implementasi” ini, setidaknya untuk tujuan artikel ini, memiliki arti tersendiri. Untuk memberikan definisi umum dari kata dalam bahasa Inggris, saya dapat mengatakan bahwa untuk mengimplementasikan sesuatu, adalah membuatnya nyata .
Untuk memberikan definisi teknis khusus untuk pengembangan perangkat lunak, saya dapat mengatakan bahwa untuk mengimplementasikan suatu perangkat lunak, adalah menulis baris kode konkret yang memenuhi persyaratan perangkat lunak tersebut. Sebagai contoh, misalkan saya menulis metode penjumlahan : private double sum(double first, double second){
private double sum(double first, double second){ //TODO: implement }
Cuplikan di atas, meskipun saya telah membuatnya sejauh menulis tipe pengembalian ( double
) dan deklarasi metode yang menentukan argumen ( first, second
) dan nama yang dapat digunakan untuk memanggil metode tersebut ( sum
), telah tidak dilaksanakan . Untuk mengimplementasikannya , kita harus melengkapi badan metode seperti:
private double sum(double first, double second){ return first + second; }
Secara alami, contoh pertama tidak dapat dikompilasi, tetapi kita akan melihat sejenak bahwa antarmuka adalah cara di mana kita dapat menulis fungsi-fungsi yang tidak diimplementasikan ini tanpa kesalahan.
Warisan Di Jawa
Agaknya, jika Anda membaca artikel ini, Anda telah menggunakan kata kunci extends
Java setidaknya sekali. Mekanisme kata kunci ini sederhana dan paling sering dijelaskan menggunakan contoh yang berkaitan dengan berbagai jenis hewan atau bentuk geometris; Dog
dan Cat
memperpanjang Animal
, dan lain sebagainya. Saya akan berasumsi bahwa saya tidak perlu menjelaskan teori tipe dasar kepada Anda, jadi mari kita langsung ke manfaat utama pewarisan di Jawa, melalui kata kunci extends
.
Membangun aplikasi “Hello World” berbasis konsol di Java sangat sederhana. Dengan asumsi Anda memiliki Java Compiler ( javac ) dan lingkungan runtime ( jre ), Anda dapat menulis kelas yang berisi fungsi utama seperti:
public class JavaApp{ public static void main(String []args){ System.out.println("Hello World"); } }
Membangun aplikasi GUI di Java di hampir semua platform utamanya (Android, Enterprise/Web, Desktop), dengan sedikit bantuan dari IDE untuk menghasilkan kode kerangka/boilerplate dari aplikasi baru, juga relatif mudah berkat extends
kata kunci.
Misalkan kita memiliki Layout XML yang disebut activity_main.xml
(biasanya kita membangun antarmuka pengguna secara deklaratif di Android, melalui file Layout) yang berisi TextView
(seperti label teks) yang disebut tvDisplay
:
<?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>
Juga, misalkan kita ingin tvDisplay
mengatakan "Halo Dunia!" Untuk melakukannya, kita hanya perlu menulis kelas yang menggunakan kata kunci extends
untuk mewarisi dari kelas Activity
:
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"); }
Efek dari mewarisi implementasi kelas Activity
dapat paling dihargai dengan melihat sekilas kode sumbernya. Saya sangat ragu bahwa Android akan menjadi platform seluler yang dominan jika seseorang perlu mengimplementasikan bahkan sebagian kecil dari 8000+ baris yang diperlukan untuk berinteraksi dengan sistem, hanya untuk menghasilkan jendela sederhana dengan beberapa teks. Warisan adalah apa yang memungkinkan kami untuk tidak perlu membangun kembali kerangka kerja Android , atau platform apa pun yang Anda gunakan, dari awal.
Warisan Dapat Digunakan Untuk Abstraksi
Sejauh dapat digunakan untuk berbagi implementasi di seluruh kelas, pewarisan relatif mudah dipahami. Namun, ada cara penting lain di mana pewarisan dapat digunakan, yang secara konseptual terkait dengan antarmuka dan kelas abstrak yang akan segera kita bahas.
Jika Anda berkenan, anggaplah untuk beberapa saat berikutnya bahwa sebuah abstraksi, yang digunakan dalam pengertian yang paling umum, adalah representasi yang kurang rinci dari suatu hal . Alih-alih memenuhi syarat itu dengan definisi filosofis yang panjang, saya akan mencoba menunjukkan bagaimana abstraksi bekerja dalam kehidupan sehari-hari, dan segera setelah itu membahasnya secara tegas dalam hal pengembangan perangkat lunak.
Misalkan Anda bepergian ke Australia, dan Anda menyadari bahwa wilayah yang Anda kunjungi adalah tuan rumah bagi kepadatan tinggi ular taipan pedalaman (mereka tampaknya cukup beracun). Anda memutuskan untuk berkonsultasi dengan Wikipedia untuk mempelajari lebih lanjut tentang mereka dengan melihat gambar dan informasi lainnya. Dengan melakukan itu, Anda sekarang benar-benar menyadari jenis ular tertentu yang belum pernah Anda lihat sebelumnya.
Abstraksi, ide, model, atau apa pun yang Anda ingin menyebutnya, adalah representasi yang kurang rinci dari suatu hal. Penting bahwa mereka kurang detail daripada yang asli karena ular asli bisa menggigit Anda; gambar di halaman Wikipedia biasanya tidak. Abstraksi juga penting karena baik komputer maupun otak manusia memiliki kapasitas yang terbatas untuk menyimpan, berkomunikasi, dan memproses informasi. Memiliki cukup detail untuk menggunakan informasi ini dengan cara yang praktis, tanpa menghabiskan terlalu banyak ruang di memori, adalah hal yang memungkinkan komputer dan otak manusia untuk memecahkan masalah.
Untuk mengikat ini kembali ke dalam pewarisan , ketiga topik utama yang saya diskusikan di sini dapat digunakan sebagai abstraksi , atau mekanisme abstraksi . Misalkan dalam file tata letak aplikasi "Hello World", kami memutuskan untuk menambahkan ImageView
, Button
, dan ImageButton
:
<?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>
Anggap juga bahwa Aktivitas kita telah mengimplementasikan View.OnClickListener
untuk menangani klik:
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... } }
Prinsip kuncinya di sini adalah Button
, ImageButton
, dan ImageView
mewarisi dari kelas View
. Hasilnya adalah fungsi onClick
ini dapat menerima peristiwa klik dari elemen UI yang berbeda (meskipun terkait secara hierarkis) dengan merujuknya sebagai kelas induk yang kurang detail. Ini jauh lebih nyaman daripada harus menulis metode berbeda untuk menangani setiap jenis widget di platform Android (belum lagi widget khusus).
Antarmuka Dan Abstraksi
Anda mungkin menemukan contoh kode sebelumnya agak membosankan, bahkan jika Anda mengerti mengapa saya memilihnya. Mampu berbagi implementasi di seluruh hierarki kelas sangat berguna, dan saya berpendapat bahwa menjadi utilitas utama dari inheritance . Untuk memungkinkan kita memperlakukan sekumpulan kelas yang memiliki kelas induk yang sama sebagai tipe yang sama (yaitu sebagai kelas induk ), fitur pewarisan tersebut memiliki penggunaan yang terbatas.
Secara terbatas, saya berbicara tentang persyaratan kelas anak berada dalam hierarki kelas yang sama untuk direferensikan melalui, atau dikenal sebagai kelas induk. Dengan kata lain, pewarisan adalah mekanisme abstraksi yang sangat membatasi. Bahkan, jika saya menganggap bahwa abstraksi adalah spektrum yang bergerak di antara berbagai tingkat detail (atau informasi), saya dapat mengatakan bahwa pewarisan adalah mekanisme abstraksi yang paling tidak abstrak di Jawa.
Sebelum saya melanjutkan untuk membahas antarmuka , saya ingin menyebutkan bahwa pada Java 8 , dua fitur yang disebut Metode Default dan Metode Statis telah ditambahkan ke antarmuka . Saya akan membahasnya pada akhirnya, tetapi untuk saat ini saya ingin kita berpura-pura bahwa mereka tidak ada. Ini agar saya lebih mudah menjelaskan tujuan utama penggunaan interface , yang pada awalnya, dan bisa dibilang masih, merupakan mekanisme abstraksi paling abstrak di Java .
Lebih Sedikit Detail Berarti Lebih Banyak Kebebasan
Di bagian pewarisan , saya memberikan definisi kata implementasi , yang dimaksudkan untuk membedakan dengan istilah lain yang sekarang akan kita bahas. Untuk lebih jelasnya, saya tidak peduli dengan kata-kata itu sendiri, atau apakah Anda setuju dengan penggunaannya; hanya bahwa Anda memahami apa yang mereka tunjuk secara konseptual.
Sedangkan pewarisan terutama alat untuk berbagi implementasi di satu set kelas, kita dapat mengatakan bahwa antarmuka terutama mekanisme untuk berbagi perilaku di satu set kelas. Perilaku yang digunakan dalam pengertian ini sebenarnya hanyalah kata non-teknis untuk metode abstrak . Metode abstrak adalah metode yang, pada kenyataannya tidak dapat, berisi badan metode :
public interface OnClickListener { void onClick(View v); }
Reaksi alami bagi saya, dan sejumlah individu yang telah saya bimbing, setelah pertama kali melihat interface , adalah bertanya-tanya apa kegunaan berbagi hanya tipe pengembalian , nama metode , dan daftar parameter . Di permukaan, sepertinya cara yang bagus untuk membuat pekerjaan ekstra untuk diri sendiri, atau siapa pun yang mungkin menulis kelas yang implements
antarmuka . Jawabannya, adalah bahwa antarmuka sempurna untuk situasi di mana Anda ingin sekumpulan kelas berperilaku dengan cara yang sama (yaitu mereka memiliki metode abstrak publik yang sama), tetapi Anda mengharapkan mereka mengimplementasikan perilaku itu dengan cara yang berbeda.
Untuk mengambil contoh sederhana namun relevan, platform Android memiliki dua kelas yang terutama dalam bisnis membuat dan mengelola bagian dari antarmuka pengguna: Activity
dan Fragment
. Oleh karena itu, kelas-kelas ini akan sangat sering memiliki persyaratan untuk mendengarkan acara yang muncul ketika widget diklik (atau berinteraksi dengan pengguna). Demi argumen, mari kita luangkan waktu sejenak untuk menghargai mengapa warisan hampir tidak pernah menyelesaikan masalah seperti itu:
public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... } }
Tidak hanya akan membuat Aktivitas dan Fragmen kami mewarisi dari OnClickManager
membuat tidak mungkin untuk menangani acara dengan cara yang berbeda, tetapi kickernya adalah bahwa kami bahkan tidak dapat melakukannya jika kami mau. Baik Aktivitas dan Fragmen sudah memperluas kelas induk , dan Java tidak mengizinkan banyak kelas induk . Jadi masalah kita adalah kita ingin sekumpulan kelas berperilaku dengan cara yang sama, tetapi kita harus memiliki fleksibilitas tentang bagaimana kelas mengimplementasikan perilaku itu. Ini membawa kita kembali ke contoh sebelumnya dari View.OnClickListener
:
public interface OnClickListener { void onClick(View v); }
Ini adalah kode sumber aktual (yang bersarang di kelas View
), dan beberapa baris ini memungkinkan kita untuk memastikan perilaku yang konsisten di berbagai widget ( Views ) dan pengontrol UI ( Aktivitas, Fragmen, dll . ).
Abstraksi Mempromosikan Longgar-Coupling
Mudah-mudahan saya telah menjawab pertanyaan umum tentang mengapa antarmuka ada di Jawa; di antara banyak bahasa lainnya. Dari satu perspektif, mereka hanyalah sarana untuk berbagi kode antar kelas, tetapi mereka sengaja dibuat kurang rinci untuk memungkinkan implementasi yang berbeda . Tetapi sama seperti pewarisan dapat digunakan baik sebagai mekanisme untuk berbagi kode dan abstraksi (walaupun dengan batasan pada hierarki kelas), maka antarmuka menyediakan mekanisme yang lebih fleksibel untuk abstraction .
Di bagian awal artikel ini, saya memperkenalkan topik kopling longgar/ketat dengan analogi perbedaan antara menggunakan paku dan sekrup untuk membangun semacam struktur. Untuk rekap, ide dasarnya adalah bahwa Anda akan ingin menggunakan sekrup dalam situasi di mana mengubah struktur yang ada (yang dapat menjadi akibat dari memperbaiki kesalahan, perubahan desain, dan sebagainya) mungkin terjadi. Kuku boleh digunakan jika Anda hanya perlu mengencangkan bagian-bagian struktur menjadi satu dan tidak terlalu khawatir untuk memisahkannya dalam waktu dekat.
Paku dan sekrup dimaksudkan untuk dianalogikan dengan referensi konkret dan abstrak (istilah dependensi juga berlaku) antar kelas. Agar tidak bingung, contoh berikut akan menunjukkan apa yang saya maksud:
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; }
Di sini, kami memiliki kelas yang disebut Client
yang memiliki dua jenis referensi . Perhatikan bahwa, dengan asumsi Client
tidak ada hubungannya dengan pembuatan referensinya (seharusnya tidak), itu dipisahkan dari detail implementasi adaptor jaringan tertentu.
Ada beberapa implikasi penting dari kopling longgar ini. Sebagai permulaan, saya dapat membangun Client
dalam isolasi mutlak dari setiap implementasi INetworkAdapter
. Bayangkan sejenak bahwa Anda bekerja dalam tim yang terdiri dari dua pengembang; satu untuk membangun ujung depan, satu untuk membangun ujung belakang. Selama kedua pengembang tetap mengetahui antarmuka yang menggabungkan kelas masing-masing, mereka dapat melanjutkan pekerjaan secara virtual satu sama lain.
Kedua, bagaimana jika saya memberi tahu Anda bahwa kedua pengembang dapat memverifikasi bahwa implementasi masing-masing berfungsi dengan baik, juga terlepas dari kemajuan masing-masing? Ini sangat mudah dengan antarmuka; cukup buat Test Double yang implements
antarmuka yang sesuai:
class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); } }
Pada prinsipnya, apa yang dapat diamati adalah bahwa bekerja dengan referensi abstrak membuka pintu untuk peningkatan modularitas, kemampuan pengujian, dan beberapa pola desain yang sangat kuat seperti Pola Fasad , Pola Pengamat , dan banyak lagi. Mereka juga dapat memungkinkan pengembang menemukan keseimbangan yang menyenangkan dalam merancang berbagai bagian sistem berdasarkan perilaku ( Program To An Interface ), tanpa terjebak dalam detail implementasi .
Titik Akhir Pada Abstraksi
Abstraksi tidak ada dengan cara yang sama sebagai hal yang konkret . Hal ini tercermin dalam bahasa Pemrograman Java oleh fakta bahwa kelas abstrak dan antarmuka tidak dapat dipakai.
Misalnya, ini pasti tidak akan dikompilasi:
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{} }
Faktanya, gagasan mengharapkan antarmuka yang tidak diimplementasikan atau kelas abstrak berfungsi saat runtime sama masuk akalnya dengan mengharapkan seragam UPS melayang di sekitar pengiriman paket. Sesuatu yang konkret harus berada di balik abstraksi agar berguna; bahkan jika kelas pemanggil tidak perlu tahu apa yang sebenarnya ada di balik referensi abstrak .
Kelas Abstrak: Menyatukan Semuanya
Jika Anda telah sampai sejauh ini, dengan senang hati saya memberi tahu Anda bahwa saya tidak memiliki lagi garis singgung filosofis atau frasa jargon untuk diterjemahkan. Sederhananya, kelas abstrak adalah mekanisme untuk berbagi implementasi dan perilaku di satu set kelas. Sekarang, saya akan langsung mengakui bahwa saya tidak sering menggunakan kelas abstrak . Meski begitu, harapan saya adalah bahwa pada akhir bagian ini Anda akan tahu persis kapan mereka dipanggil.
Studi Kasus Log Latihan
Kira-kira setahun dalam membangun aplikasi Android di Java, saya membangun kembali aplikasi Android pertama saya dari awal. Versi pertama adalah jenis kode yang mengerikan yang Anda harapkan dari pengembang otodidak dengan sedikit panduan. Pada saat saya ingin menambahkan fungsionalitas baru, menjadi jelas bahwa struktur erat yang saya buat secara eksklusif dengan paku , sangat tidak mungkin untuk dipertahankan sehingga saya harus membangunnya kembali sepenuhnya.
Aplikasi ini adalah log latihan yang dirancang untuk memudahkan perekaman latihan Anda, dan kemampuan untuk menampilkan data latihan sebelumnya sebagai file teks atau gambar. Tanpa terlalu detail, saya menyusun model data aplikasi sedemikian rupa sehingga ada objek Workout
, yang terdiri dari kumpulan objek Exercise
(di antara bidang lain yang tidak relevan dengan diskusi ini).
Saat saya menerapkan fitur untuk mengeluarkan data latihan ke beberapa jenis media visual, saya menyadari bahwa saya harus menghadapi masalah: Jenis latihan yang berbeda akan memerlukan jenis keluaran teks yang berbeda.
Untuk memberikan gambaran kasar, saya ingin mengubah output tergantung pada jenis latihan seperti:
- Barbel: 10 REPS @ 100 LBS
- Halter: 10 REPS @ 50 LBS x2
- Berat Badan: 10 REPS @ Berat Badan
- Berat Badan +: 10 REPS @ Berat Badan + 45 LBS
- Jangka waktu: 60 DETIK @ 100 LBS
Sebelum saya melanjutkan, perhatikan bahwa ada jenis lain (berolahraga bisa menjadi rumit) dan bahwa kode yang akan saya tunjukkan telah dipangkas dan diubah agar sesuai dengan artikel.
Sesuai dengan definisi saya sebelumnya, tujuan menulis abstract class , adalah untuk mengimplementasikan semuanya (bahkan status seperti variabel dan konstanta ) yang dibagikan di semua kelas anak di abstract class . Kemudian, untuk apa pun yang berubah di seluruh kelas anak tersebut, buat metode abstrak :
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 }
Saya mungkin menyatakan yang sudah jelas, tetapi jika Anda memiliki pertanyaan tentang apa yang harus atau tidak boleh diterapkan di abstract class , kuncinya adalah melihat bagian mana pun dari implementasi yang telah diulang di semua kelas anak.
Sekarang kita telah menetapkan apa yang umum di antara semua latihan, kita dapat mulai membuat kelas anak dengan spesialisasi untuk setiap jenis keluaran String:
Latihan Barbel:
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"); } } }
Latihan halter:
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"); } } }
Latihan Berat Badan:
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"); } } }
Saya yakin bahwa beberapa pembaca yang cerdik akan menemukan hal-hal yang dapat diabstraksikan dengan cara yang lebih efisien, tetapi tujuan dari contoh ini (yang telah disederhanakan dari sumber aslinya) adalah untuk mendemonstrasikan pendekatan umum. Tentu saja, tidak ada artikel pemrograman yang lengkap tanpa sesuatu yang dapat dieksekusi. Ada beberapa kompiler Java online yang dapat Anda gunakan untuk menjalankan kode ini jika Anda ingin mengujinya (kecuali jika Anda sudah memiliki IDE):
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); //... } }
Contekan
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 .