Ketika saya membagi metode besar (atau prosedur, atau fungsi - pertanyaan ini tidak spesifik untuk OOP, tetapi karena saya bekerja dalam bahasa OOP 99% dari waktu, itu adalah terminologi yang paling nyaman bagi saya) menjadi banyak yang kecil , Saya sering merasa tidak senang dengan hasilnya. Menjadi lebih sulit untuk menalar tentang metode-metode kecil ini daripada ketika mereka hanya berupa blok kode dalam metode besar, karena ketika saya mengekstraknya, saya kehilangan banyak asumsi mendasar yang berasal dari konteks pemanggil.
Kemudian, ketika saya melihat kode ini dan melihat metode individual, saya tidak langsung tahu dari mana mereka dipanggil, dan menganggapnya sebagai metode pribadi biasa yang dapat dipanggil dari mana saja dalam file. Misalnya, bayangkan metode inisialisasi (konstruktor atau lainnya) dibagi menjadi serangkaian yang kecil: dalam konteks metode itu sendiri, Anda dengan jelas tahu bahwa keadaan objek masih tidak valid, tetapi dalam metode pribadi biasa Anda mungkin beralih dari asumsi objek itu sudah diinisialisasi dan dalam keadaan valid.
Satu-satunya solusi yang saya lihat untuk ini adalah where
klausa di Haskell, yang memungkinkan Anda untuk mendefinisikan fungsi-fungsi kecil yang hanya digunakan dalam fungsi "induk". Pada dasarnya, ini terlihat seperti ini:
len x y = sqrt $ (sq x) + (sq y)
where sq a = a * a
Tetapi bahasa lain yang saya gunakan tidak memiliki hal seperti ini - hal terdekat adalah mendefinisikan lambda dalam lingkup lokal, yang mungkin bahkan lebih membingungkan.
Jadi, pertanyaan saya adalah - apakah Anda menemukan ini, dan apakah Anda bahkan melihat ini adalah masalah? Jika Anda melakukannya, bagaimana Anda biasanya menyelesaikannya, khususnya dalam bahasa OOP "arus utama", seperti Java / C # / C ++?
Edit tentang duplikat: Seperti yang diketahui orang lain, sudah ada pertanyaan yang membahas metode pemisahan dan pertanyaan kecil yang bersifat satu baris. Saya membacanya, dan mereka tidak membahas masalah asumsi mendasar yang dapat diturunkan dari konteks penelepon (misalnya di atas, objek diinisialisasi). Itulah inti dari pertanyaan saya, dan itulah mengapa pertanyaan saya berbeda.
Pembaruan: Jika Anda mengikuti pertanyaan dan diskusi di bawah ini, Anda dapat menikmati artikel ini oleh John Carmack tentang masalah ini , khususnya:
Selain kesadaran akan kode aktual yang dieksekusi, fungsi inlining juga bermanfaat karena tidak memungkinkan untuk memanggil fungsi dari tempat lain. Itu kedengarannya konyol, tapi ada benarnya. Sebagai basis kode tumbuh dari tahun ke tahun penggunaan, akan ada banyak peluang untuk mengambil jalan pintas dan hanya memanggil fungsi yang hanya melakukan pekerjaan yang menurut Anda perlu dilakukan. Mungkin ada fungsi FullUpdate () yang memanggil PartialUpdateA (), dan PartialUpdateB (), tetapi dalam beberapa kasus tertentu Anda mungkin menyadari (atau berpikir) bahwa Anda hanya perlu melakukan PartialUpdateB (), dan Anda menjadi efisien dengan menghindari yang lain kerja. Banyak dan banyak bug berasal dari ini. Sebagian besar bug adalah hasil dari kondisi eksekusi yang tidak sesuai dengan apa yang Anda pikirkan.
Jawaban:
Kekhawatiran Anda cukup beralasan. Ada solusi lain.
Ambil langkah mundur. Apa yang secara mendasar merupakan tujuan dari suatu metode? Metode hanya melakukan satu dari dua hal:
Atau, sayangnya, keduanya. Saya mencoba menghindari metode yang melakukan keduanya, tetapi banyak yang melakukannya. Katakanlah efek yang dihasilkan atau nilai yang dihasilkan adalah "hasil" dari metode ini.
Anda perhatikan bahwa metode dipanggil dalam "konteks". Apa konteksnya?
Pada dasarnya apa yang Anda tunjukkan adalah: kebenaran hasil dari metode tergantung pada konteks di mana ia dipanggil .
Kita sebut kondisi yang diperlukan sebelum tubuh metode dimulai untuk metode untuk menghasilkan hasil yang benar nya prasyarat , dan kita sebut kondisi yang akan diproduksi setelah metode tubuh mengembalikan nya postconditions .
Jadi pada dasarnya apa yang Anda tunjukkan adalah: ketika saya mengekstrak blok kode ke dalam metodenya sendiri, saya kehilangan informasi kontekstual tentang prasyarat dan postkondisi .
Solusi untuk masalah ini adalah membuat prasyarat dan postkondisi eksplisit dalam program . Dalam C #, misalnya, Anda dapat menggunakan
Debug.Assert
atau Kode Kontrak untuk mengekspresikan prasyarat dan postkondisi.Sebagai contoh: Saya dulu bekerja pada kompiler yang bergerak melalui beberapa "tahap" kompilasi. Pertama kodenya akan diexex, lalu diurai, lalu jenisnya akan diselesaikan, lalu hierarki pewarisan akan diperiksa untuk siklus, dan seterusnya. Setiap bit kode sangat sensitif terhadap konteksnya; akan menjadi bencana, misalnya, untuk bertanya "apakah jenis ini dapat dikonversi ke jenis itu?" jika grafik tipe dasar belum diketahui asiklik! Jadi karena itu setiap bit kode dengan jelas mendokumentasikan prasyaratnya. Kita akan
assert
dalam metode yang memeriksa konvertibilitas jenis yang telah kita lewati cek "tipe dasar acylic", dan kemudian menjadi jelas bagi pembaca di mana metode itu bisa dipanggil dan di mana itu tidak bisa dipanggil.Tentu saja ada banyak cara di mana perancangan metode yang baik mengurangi masalah yang telah Anda identifikasi:
sumber
string
dan menyimpannya ke database, Anda berisiko mengalami injeksi SQL jika Anda lupa membersihkannya. Jika, di sisi lain, fungsi Anda membutuhkanSanitisedString
, dan satu-satunya cara untuk mendapatkannyaSantisiedString
adalah dengan memanggilSanitise
, maka Anda telah mengesampingkan bug injeksi SQL dengan konstruksi. Saya semakin menemukan diri saya mencari cara untuk membuat kompiler menolak kode yang salah.Saya sering melihat ini, dan setuju bahwa itu masalah. Biasanya saya menyelesaikannya dengan membuat objek metode : kelas khusus baru yang anggotanya adalah variabel lokal dari metode asli, terlalu besar.
Kelas yang baru cenderung memiliki nama seperti 'Eksportir' atau 'Tabulasi', dan akan diberikan informasi apa pun yang diperlukan untuk melakukan tugas tertentu dari konteks yang lebih besar. Maka, bebas menentukan cuplikan kode pembantu yang lebih kecil yang tidak berbahaya digunakan untuk apa pun selain tabulasi atau ekspor.
sumber
Banyak bahasa membiarkan Anda membuat fungsi sarang seperti Haskell. Java / C # / C ++ sebenarnya outlier relatif dalam hal itu. Sayangnya, mereka begitu populer bahwa orang-orang datang untuk berpikir, "Ini memiliki menjadi ide yang buruk, jika tidak 'utama' bahasa favorit saya akan mengizinkannya."
Java / C # / C ++ pada dasarnya berpikir kelas harus menjadi satu-satunya pengelompokan metode yang Anda butuhkan. Jika Anda memiliki begitu banyak metode yang tidak dapat Anda tentukan konteksnya, ada dua pendekatan umum yang harus diambil: urutkan berdasarkan konteks, atau bagi berdasarkan konteks.
Menyortir berdasarkan konteks adalah salah satu rekomendasi yang dibuat dalam Kode Bersih , di mana penulis menggambarkan pola "ke paragraf." Ini pada dasarnya menempatkan fungsi penolong Anda segera setelah fungsi yang memanggilnya, sehingga Anda dapat membacanya seperti paragraf di artikel surat kabar, mendapatkan detail lebih lanjut saat Anda membaca. Saya pikir dalam videonya dia bahkan membuat indentasi.
Pendekatan lain adalah dengan membagi kelas Anda. Ini tidak dapat diambil terlalu jauh, karena kebutuhan yang mengganggu untuk membuat instance objek sebelum Anda dapat memanggil metode apa pun pada mereka, dan masalah yang melekat dengan memutuskan mana dari beberapa kelas kecil harus memiliki masing-masing bagian data. Namun, jika Anda sudah mengidentifikasi beberapa metode yang benar-benar hanya cocok dalam satu konteks, mereka mungkin merupakan kandidat yang baik untuk mempertimbangkan untuk memasukkan ke dalam kelas mereka sendiri. Misalnya, inisialisasi yang kompleks dapat dilakukan dalam pola penciptaan seperti pembangun.
sumber
Saya pikir jawabannya dalam kebanyakan kasus adalah konteks. Sebagai kode penulisan pengembang, Anda harus menganggap kode Anda akan diubah di masa mendatang. Kelas mungkin diintegrasikan dengan kelas lain, mungkin menggantikannya algoritma internal, atau mungkin dipecah menjadi beberapa kelas untuk membuat abstraksi. Itu adalah hal-hal yang biasanya tidak dipertimbangkan oleh pengembang pemula, menyebabkan kebutuhan untuk penyelesaian yang berantakan atau perbaikan total di kemudian hari.
Metode ekstraksi itu baik, tetapi sampai tingkat tertentu. Saya selalu mencoba bertanya pada diri sendiri pertanyaan-pertanyaan ini ketika memeriksa atau sebelum menulis kode:
Bagaimanapun, selalu pikirkan tanggung jawab tunggal. Kelas harus memiliki satu tanggung jawab, fungsinya harus melayani satu layanan konstan tunggal, dan jika mereka melakukan sejumlah tindakan, tindakan tersebut harus memiliki fungsi sendiri, sehingga mudah untuk membedakan atau mengubahnya nanti.
sumber
Saya tidak menyadari seberapa besar masalah ini sampai saya mengadopsi ECS yang mendorong fungsi sistem yang lebih besar (dengan sistem menjadi satu-satunya yang memiliki fungsi) dan ketergantungan mengalir ke data mentah , bukan abstraksi.
Itu, yang mengejutkan saya, menghasilkan basis kode jauh lebih mudah untuk dipikirkan dan dipelihara dibandingkan dengan basis kode yang saya kerjakan di masa lalu di mana, selama debugging, Anda harus melacak semua jenis fungsi kecil yang mungil, seringkali melalui fungsi abstrak yang dipanggil melalui antarmuka murni mengarah ke siapa yang tahu di mana sampai Anda melacak ke dalamnya, hanya untuk menelurkan beberapa kaskade peristiwa yang mengarah ke tempat-tempat Anda tidak pernah berpikir kode harus pernah mengarah.
Tidak seperti John Carmack, masalah terbesar saya dengan basis kode itu bukan kinerja karena saya tidak pernah memiliki permintaan laten yang sangat ketat terhadap mesin game AAA dan sebagian besar masalah kinerja kami lebih terkait dengan throughput. Tentu saja Anda juga dapat mulai membuatnya lebih dan lebih sulit untuk mengoptimalkan hotspot ketika Anda bekerja dalam batas yang lebih sempit dan lebih sempit dari fungsi dan kelas remaja dan tanpa struktur yang menghalangi (mengharuskan Anda untuk menggabungkan semua potongan kecil ini kembali) untuk sesuatu yang lebih besar sebelum Anda bahkan dapat mulai menanganinya secara efektif).
Namun masalah terbesar bagi saya adalah tidak dapat dengan yakin alasan tentang kebenaran sistem secara keseluruhan terlepas dari semua tes yang lulus. Ada terlalu banyak untuk dimasukkan ke dalam otak saya dan dipahami karena jenis sistem itu tidak membiarkan Anda berpikir tentang hal itu tanpa memperhitungkan semua detail kecil ini dan interaksi tanpa akhir antara fungsi kecil dan objek yang terjadi di mana-mana. Ada terlalu banyak "bagaimana jika?", Terlalu banyak hal yang perlu dipanggil pada waktu yang tepat, terlalu banyak pertanyaan tentang apa yang akan terjadi jika mereka disebut waktu yang salah (yang mulai naik ke titik paranoia ketika Anda memiliki satu peristiwa yang memicu peristiwa lain memicu peristiwa lainnya yang mengarahkan Anda ke semua jenis tempat yang tidak dapat diprediksi), dll.
Sekarang saya suka fungsi 80-line pantat besar saya di sana-sini, selama mereka masih melakukan tanggung jawab tunggal dan jelas dan tidak memiliki seperti 8 tingkat blok bersarang. Mereka menimbulkan perasaan bahwa ada sedikit hal dalam sistem untuk diuji dan dipahami, bahkan jika versi yang lebih kecil, yang dipotong dadu dari fungsi yang lebih besar ini hanyalah detail implementasi pribadi yang tidak dapat dipanggil oleh orang lain ... masih, entah bagaimana, ia cenderung merasa ada sedikit interaksi yang terjadi di seluruh sistem. Saya bahkan suka beberapa duplikasi kode yang sangat sederhana, selama itu bukan logika yang kompleks (katakan saja 2-3 baris kode), jika itu berarti kurang fungsi. Saya suka alasan Carmack di sana tentang inlining membuat fungsionalitas itu tidak mungkin untuk memanggil tempat lain di file sumber. Sana'
Kesederhanaan tidak selalu mengurangi kompleksitas pada tingkat gambaran besar jika pilihannya adalah antara satu fungsi gemuk vs 12 fungsi sederhana yang memanggil satu sama lain dengan grafik dependensi yang kompleks. Pada akhirnya, Anda harus sering berpikir tentang apa yang terjadi di luar fungsi, harus berpikir tentang apa yang ditambahkan fungsi-fungsi ini pada akhirnya, dan mungkin akan lebih sulit untuk melihat gambaran besar itu jika Anda harus menyimpulkannya dari potongan puzzle terkecil.
Tentu saja kode jenis pustaka dengan tujuan umum yang telah teruji dengan baik dapat dikecualikan dari aturan ini, karena kode tujuan umum seperti itu sering berfungsi dan berdiri dengan baik sendiri. Juga cenderung lebih kecil dibandingkan dengan kode yang sedikit lebih dekat dengan domain aplikasi Anda (ribuan baris kode, bukan jutaan), dan sangat berlaku sehingga mulai menjadi bagian dari kosakata harian. Tetapi dengan sesuatu yang lebih spesifik untuk aplikasi Anda di mana invarian seluruh sistem yang harus Anda pertahankan jauh melampaui fungsi tunggal atau kelas, saya cenderung merasa terbantu memiliki fungsi yang lebih kecil untuk alasan apa pun. Saya merasa jauh lebih mudah bekerja dengan potongan puzzle yang lebih besar dalam mencoba mencari tahu apa yang terjadi dengan gambaran besar.
sumber
Saya tidak berpikir itu masalah besar , tapi saya setuju itu menyusahkan. Biasanya saya hanya menempatkan helper segera setelah penerima dan menambahkan akhiran "Helper". Itu ditambah
private
akses specifier harus membuat perannya jelas. Jika ada beberapa invarian yang tidak berlaku ketika helper dipanggil, saya menambahkan komentar di helper.Solusi ini memang memiliki kekurangan yang disayangkan karena tidak menangkap lingkup fungsi yang membantu. Idealnya fungsi Anda kecil sehingga mudah-mudahan ini tidak menghasilkan terlalu banyak parameter. Biasanya Anda akan menyelesaikan ini dengan mendefinisikan struct atau kelas baru untuk menggabungkan parameter, tetapi jumlah pelat ketel yang diperlukan untuk itu dapat dengan mudah lebih lama daripada helper itu sendiri, dan kemudian Anda kembali ke tempat Anda memulai tanpa ada cara yang jelas untuk menghubungkan struct dengan fungsi.
Anda sudah menyebutkan solusi lain - tentukan helper di dalam fungsi utama. Ini mungkin idiom yang agak tidak umum dalam beberapa bahasa, tapi saya pikir itu tidak membingungkan (kecuali teman-teman Anda bingung oleh lambda secara umum). Ini hanya berfungsi jika Anda dapat mendefinisikan fungsi atau objek seperti fungsi dengan mudah. Saya tidak akan mencoba ini di Java 7, misalnya, karena kelas anonim membutuhkan memperkenalkan 2 tingkat bersarang bahkan untuk "fungsi" terkecil. Ini sedekat
let
atau denganwhere
klausa yang bisa Anda dapatkan; Anda dapat merujuk ke variabel lokal sebelum definisi dan helper tidak dapat digunakan di luar cakupan itu.sumber