Halo, Habr!
Hari ini, publikasi yang diterjemahkan menunggu Anda, hingga taraf tertentu mencerminkan pencarian kami terkait dengan buku-buku baru tentang OOP dan FI. Silakan berpartisipasi dalam pemungutan suara.

Apakah paradigma OOP mati? Dapatkah dikatakan bahwa pemrograman fungsional adalah masa depan? Tampaknya banyak artikel menulis tentang ini. Saya cenderung tidak setuju dengan sudut pandang ini. Ayo bicara!
Setiap beberapa bulan saya menemukan posting di beberapa blog di mana penulis membuat klaim yang tampaknya beralasan untuk pemrograman berorientasi objek, setelah itu dia menyatakan OOP sebagai peninggalan masa lalu, dan kita semua harus beralih ke pemrograman fungsional.
Sebelumnya, saya
menulis bahwa OOP dan FI tidak saling bertentangan. Selain itu, saya dapat menggabungkan mereka dengan sangat sukses.
Mengapa penulis artikel ini memiliki begitu banyak masalah dengan OOP, dan mengapa AF bagi mereka merupakan alternatif yang jelas?
Cara mengajar OOP
Ketika kita diajari OOP, mereka biasanya menekankan bahwa itu didasarkan pada empat prinsip:
enkapsulasi ,
pewarisan ,
abstraksi ,
polimorfisme . Empat prinsip inilah yang biasanya dikritik dalam artikel di mana penulis beralasan tentang penurunan PLO.
Namun, OOP, seperti FI, adalah alat. Untuk memecahkan masalah. Itu bisa dikonsumsi, juga bisa disalahgunakan. Misalnya, dengan membuat abstraksi yang salah, Anda menyalahgunakan OOP.
Jadi, kelas
Square
tidak boleh mewarisi kelas
Rectangle
. Dalam pengertian matematika, mereka tentu saja terhubung. Namun, dari sudut pandang pemrograman, mereka tidak berada dalam hubungan pewarisan. Faktanya adalah bahwa persyaratan untuk persegi lebih ketat daripada persegi panjang. Sedangkan dalam sebuah persegi panjang ada dua pasang sisi yang sama, sebuah persegi harus memiliki semua sisi yang sama.
Warisan
Mari kita bahas warisan secara lebih rinci. Anda mungkin mengingat contoh-contoh buku teks dengan hierarki yang indah dari kelas-kelas yang diwarisi, dan semua struktur ini berfungsi untuk menyelesaikan masalah. Namun, dalam praktiknya, pewarisan tidak digunakan sesering komposisi.
Pertimbangkan sebuah contoh. Katakanlah kita memiliki kelas yang sangat sederhana, pengontrol dalam aplikasi web. Sebagian besar kerangka kerja modern mengasumsikan bahwa Anda akan bekerja dengannya seperti ini:
class BlogController extends FrameworkAbstractController { }
Diasumsikan bahwa dengan cara ini akan lebih mudah bagi Anda untuk melakukan panggilan seperti
this.renderTemplate(...)
, karena metode seperti ini diwarisi dari kelas
FrameworkAbstractController
.
Seperti yang ditunjukkan dalam banyak artikel tentang hal ini, sejumlah masalah nyata muncul di sini. Setiap fungsi internal di kelas dasar sebenarnya berubah menjadi API. Dia tidak bisa lagi berubah. Variabel apa pun yang dilindungi dari pengontrol dasar sekarang akan lebih atau kurang terkait dengan API.
Tidak ada yang membingungkan. Dan jika kita memilih pendekatan dengan komposisi dan injeksi ketergantungan, itu akan menjadi seperti ini:
class BlogController { public BlogController ( TemplateRenderer templateRenderer ) { } }
Anda lihat, Anda tidak lagi bergantung pada beberapa
FrameworkAbstractController
berkabut, tetapi bergantung pada hal yang sangat jelas dan sempit,
TemplateRenderer
. Bahkan,
BlogController
tidak mewarisi dari pengontrol lain, karena tidak mewarisi perilaku apa pun.
Enkapsulasi
Fitur OOP kedua yang sering dikritik adalah enkapsulasi. Dalam bahasa sastra, makna enkapsulasi dirumuskan sebagai berikut: data dan fungsionalitas disampaikan bersama-sama, dan keadaan internal kelas disembunyikan dari dunia luar.
Peluang ini, sekali lagi, memungkinkan untuk digunakan dan disalahgunakan. Contoh utama penyalahgunaan dalam kasus ini adalah keadaan bocor.
Secara relatif, anggaplah bahwa kelas
List<>
berisi daftar elemen, dan daftar ini dapat diubah. Mari kita buat kelas untuk memproses keranjang pesanan sebagai berikut:
class ShoppingCart { private List<ShoppingCartItem> items; public List<ShoppingCartItem> getItems() { return this.items; } }
Di sini, di sebagian besar bahasa berorientasi OOP, hal berikut akan terjadi: variabel item akan dikembalikan dengan referensi. Karena itu, selanjutnya kita dapat melakukan ini:
shoppingCart.getItems().clear();
Dengan demikian, kita benar-benar akan menghapus daftar barang di keranjang, dan ShoppingCart bahkan tidak akan mengetahuinya. Namun, jika Anda perhatikan dengan teliti contoh ini, menjadi jelas bahwa masalahnya bukan pada prinsip enkapsulasi. Prinsip ini dilanggar di sini, karena negara bagian bocor dari kelas
ShoppingCart
.
Dalam contoh khusus ini, penulis kelas
ShoppingCart
dapat menggunakan
immutability untuk menghindari masalah dan memastikan bahwa prinsip enkapsulasi tidak dilanggar.
Pemrogram berpengalaman sering melanggar prinsip enkapsulasi dengan cara lain: mereka memperkenalkan negara di mana itu tidak diperlukan. Programmer yang tidak berpengalaman seperti itu sering menggunakan variabel kelas privat untuk mentransfer data dari satu fungsi ke fungsi lain dalam kelas yang sama, sementara itu akan lebih tepat untuk menggunakan Obyek Transfer Data untuk mentransfer struktur kompleks ke fungsi lain. Sebagai hasil dari kesalahan tersebut, kode tidak perlu rumit, yang dapat menyebabkan bug.
Secara umum, alangkah baiknya untuk membuang negara sama sekali - simpan data yang bisa berubah di kelas kapan pun memungkinkan. Dengan melakukannya, Anda perlu
memastikan enkapsulasi yang andal dan memastikan tidak ada kebocoran di mana pun.
Abstraksi
Abstraksi, sekali lagi, dipahami dalam banyak hal secara tidak benar. Dalam kasus apa pun Anda harus memasukkan kode dengan kelas abstrak dan membuat hierarki yang mendalam di dalamnya.
Jika Anda melakukan ini tanpa alasan yang baik, maka Anda hanya mencari masalah di kepala Anda sendiri. Tidak masalah bagaimana abstraksi dilakukan - sebagai kelas abstrak atau sebagai antarmuka; dalam hal apa pun, kompleksitas tambahan akan muncul dalam kode. Kompleksitas ini harus dibenarkan.
Sederhananya, antarmuka hanya dapat dibuat jika Anda bersedia menghabiskan waktu dan mendokumentasikan perilaku yang diharapkan dari kelas yang mengimplementasikannya. Ya, Anda membaca saya dengan benar. Tidak cukup hanya membuat daftar fungsi yang perlu Anda terapkan - juga menggambarkan bagaimana (idealnya) mereka harus bekerja.
Polimorfisme
Akhirnya, mari kita bicara tentang polimorfisme. Dia menyarankan bahwa satu kelas dapat menerapkan banyak perilaku. Contoh buku teks yang buruk adalah menulis bahwa
Square
with polymorphism dapat berupa
Rectangle
atau
Parallelogram
. Seperti yang telah saya tunjukkan di atas, ini dalam OOP jelas mustahil, karena perilaku entitas ini berbeda.
Berbicara tentang polimorfisme, seseorang harus mengingat
perilaku , bukan
kode . Contoh yang baik adalah kelas
Soldier
di game komputer. Itu dapat menerapkan perilaku
Movable
(situasi: dapat bergerak) dan perilaku
Enemy
(situasi: menembak Anda). Sebaliknya, kelas
GunEmplacement
hanya dapat menerapkan perilaku
Enemy
.
Jadi, jika Anda menulis
Square implements Rectangle, Parallelogram
, pernyataan ini tidak menjadi kenyataan. Abstraksi Anda harus bekerja sesuai dengan logika bisnis. Anda harus lebih memikirkan perilaku daripada tentang kode.
Mengapa FP bukan peluru perak
Jadi, ketika kita mengulangi empat prinsip dasar OOP, mari kita pikirkan tentang apa fitur pemrograman fungsional, dan mengapa tidak menggunakannya untuk menyelesaikan semua masalah dalam kode Anda?
Dari sudut pandang banyak penganut FP, kelas adalah
penistaan , dan kode harus disajikan dalam bentuk
fungsi . Bergantung pada bahasa, data dapat ditransfer dari satu fungsi ke fungsi lainnya menggunakan tipe primitif, atau dalam bentuk satu atau set data terstruktur lainnya (array, kamus, dll.).
Selain itu, sebagian besar fungsi seharusnya tidak memiliki efek samping. Dengan kata lain, mereka tidak boleh mengubah data di tempat tak terduga di latar belakang, tetapi hanya bekerja dengan parameter input dan menghasilkan output.
Pendekatan ini memisahkan
data dari
fungsional - pada pandangan pertama, FP ini secara radikal berbeda dari OOP. FP menekankan bahwa dengan cara ini kode tetap sederhana. Anda ingin melakukan sesuatu, menulis fungsi untuk tujuan ini - itu saja.
Masalah dimulai ketika beberapa fungsi harus bergantung pada yang lain. Ketika fungsi A memanggil fungsi B, dan fungsi B memanggil lima hingga enam fungsi lainnya, dan pada akhirnya
fungsi pengisian nol ditemukan yang dapat merusak - ini adalah di mana Anda tidak akan iri.
Kebanyakan programmer yang menganggap diri mereka pendukung FP suka FP karena kesederhanaannya dan tidak menganggap masalah seperti itu serius. Ini cukup jujur jika tugas Anda adalah hanya lulus kode dan tidak pernah memikirkannya lagi. Jika Anda ingin membangun basis kode yang nyaman dalam dukungan, lebih baik mematuhi prinsip-prinsip
kode murni , khususnya, menerapkan
inversi ketergantungan , di mana FI dalam praktiknya juga menjadi jauh lebih rumit.
OOP atau FP?
OOP dan FI adalah
alat . Pada akhirnya, tidak masalah paradigma pemrograman mana yang Anda gunakan. Masalah yang dijelaskan dalam sebagian besar artikel tentang topik ini berkaitan dengan organisasi kode.
Menurut pendapat saya, struktur makro aplikasi jauh lebih penting. Apa saja modul di dalamnya? Bagaimana mereka saling bertukar informasi? Struktur data apa yang paling umum dengan Anda? Bagaimana mereka didokumentasikan? Objek mana yang paling penting dalam hal logika bisnis?
Semua masalah ini sama sekali tidak terhubung dengan paradigma pemrograman yang digunakan, pada tingkat paradigma seperti itu mereka bahkan tidak dapat diselesaikan. Seorang programmer yang baik mempelajari paradigma untuk menguasai alat yang ditawarkannya, dan kemudian memilih mana yang paling cocok untuk menyelesaikan tugas.