Pemisahan kekhawatiran: Kapan pemisahan itu "terlalu banyak"?


9

Saya sangat suka kode bersih dan saya selalu ingin kode kode saya dengan cara terbaik. Tapi selalu ada satu hal, saya tidak begitu mengerti:

Kapan terlalu banyak "pemisahan masalah" tentang metode?

Katakanlah kita memiliki metode berikut:

def get_last_appearance_of_keyword(file, keyword):
    with open(file, 'r') as file:
        line_number = 0
        for line in file:
            if keyword in line:
                line_number = line
        return line_number

Saya pikir metode ini baik-baik saja. Ini sederhana, mudah dibaca dan jelas, sesuai dengan namanya. Tetapi: Ini tidak benar-benar melakukan "hanya satu hal". Ini benar-benar membuka file, dan kemudian menemukannya. Itu berarti saya dapat membaginya lebih jauh (Juga mempertimbangkan "Prinsip Tanggung Jawab Tunggal"):

Variasi B (Ya, ini masuk akal. Dengan cara ini kita dapat dengan mudah menggunakan kembali algoritma untuk menemukan tampilan kata kunci terakhir dalam sebuah teks, namun sepertinya "terlalu banyak". Saya tidak bisa menjelaskan mengapa, tapi saya hanya "merasa" "seperti itu):

def get_last_appearance_of_keyword(file, keyword):
    with open(file, 'r') as text_from_file:
        line_number = find_last_appearance_of_keyword(text_from_file, keyword) 
    return line_number

def find_last_appearance_of_keyword(text, keyword):
    line_number = 0
    for line in text:
        if keyword in line:
            line_number = line
    return line_number

Variasi C (Ini hanya tidak masuk akal menurut saya. Kami pada dasarnya merangkum satu-liner ke metode lain hanya dengan satu baris dua kali. Tetapi orang dapat berargumen, bahwa cara membuka sesuatu dapat berubah di masa depan, karena beberapa permintaan fitur , dan karena kami tidak ingin mengubahnya berkali-kali, tetapi sekali saja, kami hanya merangkumnya dan memisahkan fungsi utama kami lebih jauh):

def get_last_appearance_of_keyword(file, keyword):
    text_from_file = get_text_from_file(file)
    line_number = find_keyword_in_text(text_from_file, keyword)
    return line_number 

def get_text_from_file(file):
    with open(file, 'r') as text:
        return text

def find_last_appearance_of_keyword(text, keyword):
    line_number = 0
    for line in text:
        if check_if_keyword_in_string(line, keyword):
            line_number = line         
    return line_number

def check_if_keyword_in_string(text, keyword):
    if keyword in string:
        return true
    return false

Jadi pertanyaan saya sekarang: Apa cara yang benar dalam menulis kode ini dan mengapa pendekatan lain benar atau salah? Saya selalu belajar: Pemisahan, tetapi tidak pernah ketika itu terlalu banyak. Dan bagaimana saya bisa yakin di masa depan, bahwa itu "tepat" dan bahwa tidak perlu lebih banyak pemisahan ketika saya coding lagi?



2
Selain itu: apakah Anda berniat mengembalikan string atau angka? line_number = 0adalah default numerik, dan line_number = linememberikan nilai string (yang merupakan isi baris bukan posisinya )
Caleth

3
Dalam contoh terakhir Anda menerapkan kembali dua fungsi yang ada: opendan in. Menerapkan kembali fungsi yang ada tidak meningkatkan pemisahan kekhawatiran, kekhawatiran sudah ditangani dalam fungsi yang ada!
MikeFHay

Jawaban:


10

Berbagai contoh pemecahan masalah Anda menjadi fungsi-fungsi terpisah semuanya mengalami masalah yang sama: Anda masih menyandikan file ke dalamnya get_last_appearance_of_keyword. Ini membuat fungsi itu sulit untuk diuji karena sekarang harus membalas pada file yang ada di sistem file ketika tes dijalankan. Ini mengarah pada tes rapuh.

Jadi saya cukup mengubah fungsi asli Anda menjadi:

def get_last_appearance_of_keyword(text, keyword):
    line_number = 0
    for line in text:
        if keyword in line:
            line_number = line
    return line_number

Sekarang Anda memiliki fungsi yang hanya memiliki satu tanggung jawab: cari kemunculan kata kunci terakhir dalam beberapa teks. Jika teks itu berasal dari file, itu menjadi tanggung jawab penelepon untuk menangani. Saat menguji, Anda kemudian hanya bisa meneruskan blok teks. Saat menggunakannya dengan kode runtime, pertama file dibaca, lalu fungsi ini dipanggil. Itu adalah pemisahan nyata dari keprihatinan.


2
Pikirkan tentang pencarian case-insensitive. Pikirkan tentang melewatkan garis komentar. Pemisahan keprihatinan dapat menjadi berbeda. Juga, line_number = linejelas merupakan kesalahan.
9000

2
juga contoh terakhir melakukan hal ini
Ewan

1

Prinsip tanggung jawab tunggal menyatakan bahwa suatu kelas harus menangani satu fungsi saja, dan fungsi ini harus dirangkum dengan tepat di dalamnya.

Apa sebenarnya yang dilakukan metode Anda? Ia mendapat tampilan kata kunci terakhir. Setiap baris di dalam metode bekerja ke arah ini dan tidak terkait dengan hal lain, dan hasil akhirnya hanya satu dan satu saja. Dengan kata lain: Anda tidak perlu membagi metode ini menjadi yang lain.

Gagasan utama di balik prinsip ini adalah bahwa Anda tidak boleh melakukan lebih dari satu hal pada akhirnya. Mungkin Anda membuka file dan membiarkannya begitu sehingga metode lain dapat menggunakannya, Anda akan melakukan dua hal. Atau jika Anda tetap menggunakan data yang terkait dengan metode ini, sekali lagi, dua hal.

Sekarang, Anda bisa mengekstrak baris "file terbuka" dan membuat metode menerima objek file untuk dikerjakan, tapi itu lebih merupakan refactoring teknis daripada mencoba mematuhi SRP.

Ini adalah contoh yang baik dari over engineering. Jangan terlalu banyak berpikir atau Anda akan berakhir dengan sekelompok metode satu baris.


2
Sama sekali tidak ada yang salah dengan fungsi satu baris. Bahkan, beberapa fungsi yang paling berguna hanya satu baris kode.
Joshua Jones

2
@JoshuaJones Pada dasarnya tidak ada yang salah dengan fungsi satu-baris, tetapi mereka mungkin menjadi penghalang jika mereka tidak mengabstraksikan sesuatu yang berguna. Fungsi satu garis untuk mengembalikan jarak kartesius antara dua titik sangat berguna, tetapi jika Anda memiliki satu garis return keyword in text, itu hanya menambahkan lapisan yang tidak perlu di atas konstruksi bahasa bawaan.
cariehl

@cariehl Mengapa akan return keyword in textmenjadi lapisan yang tidak perlu? Jika Anda menemukan diri Anda secara konsisten menggunakan kode itu dalam lambda sebagai parameter dalam fungsi tingkat tinggi, mengapa tidak membungkusnya dalam suatu fungsi?
Joshua Jones

1
@JoshuaJones Dalam konteks itu, Anda abstrak sesuatu yang berguna. Dalam konteks contoh asli, tidak ada alasan yang baik untuk fungsi seperti itu ada. inadalah kata kunci Python umum, itu mencapai tujuan, dan itu ekspresif sendiri. Menulis fungsi pembungkus di sekitarnya hanya demi memiliki fungsi pembungkus mengaburkan kode, membuatnya kurang intuitif segera.
cariehl

0

Pendapat saya: Tergantung :-)

Menurut pendapat saya, kode harus memenuhi sasaran ini, dipesan berdasarkan prioritas:

  1. Memenuhi semua persyaratan (yaitu dengan benar melakukan apa yang seharusnya)
  2. Mudah dibaca dan mudah diikuti / dimengerti
  3. Mudah refactor
  4. Ikuti praktik / prinsip pengkodean yang baik

Bagi saya contoh asli Anda melewati semua tujuan ini (kecuali mungkin kebenaran karena line_number = linehal yang telah disebutkan dalam komentar , tapi bukan itu intinya di sini).

Masalahnya, SRP bukan satu-satunya prinsip yang harus diikuti. Ada juga Kamu Tidak Akan Membutuhkannya (YAGNI) (di antara banyak lainnya). Ketika prinsip-prinsip bertabrakan, Anda perlu menyeimbangkannya.

Contoh pertama Anda mudah dibaca, mudah untuk refactor ketika Anda perlu, tetapi mungkin tidak mengikuti SRP sebanyak mungkin.

Setiap metode dalam contoh ketiga Anda juga dapat dibaca dengan sempurna tetapi semuanya tidak lagi mudah dipahami karena Anda harus menyatukan semua bagian dalam pikiran Anda. Itu mengikuti SRP sekalipun.

Karena Anda tidak mendapatkan apa-apa sekarang dari pemisahan metode Anda, jangan lakukan itu, karena Anda memiliki alternatif yang lebih mudah dimengerti.

Ketika kebutuhan Anda berubah, Anda dapat memperbaiki metode tersebut sesuai kebutuhan. Sebenarnya "semua dalam satu hal" mungkin lebih mudah untuk direvisi: Bayangkan Anda ingin menemukan baris terakhir yang cocok dengan beberapa kriteria sewenang-wenang. Sekarang Anda hanya perlu melewati beberapa fungsi predikat lambda untuk mengevaluasi apakah garis cocok dengan kriteria atau tidak.

def get_last_match(file, predicate):
    with open(file, 'r') as file:
        line_number = 0
        for line in file:
            if predicate matches line:
                line_number = line
        return line_number

Dalam contoh terakhir Anda, Anda harus melewati predikat 3 level, yaitu memodifikasi 3 metode hanya untuk memodifikasi perilaku yang terakhir.

Perhatikan bahwa bahkan membagi pembacaan file (refactoring yang biasanya banyak berguna, termasuk saya) mungkin memiliki konsekuensi yang tak terduga: Anda perlu membaca seluruh file ke memori untuk meneruskannya sebagai string ke metode Anda. Jika file berukuran besar, mungkin itu bukan yang Anda inginkan.

Intinya: Prinsip-prinsip tidak boleh diikuti dengan ekstrem tanpa mengambil langkah mundur dan mempertimbangkan semua faktor lainnya.

Mungkin "pemisahan metode dini" dapat dianggap sebagai kasus khusus pengoptimalan prematur ? ;-)


0

Ini seperti pertanyaan keseimbangan dalam pikiran saya tanpa jawaban benar dan salah yang mudah. Saya hanya akan pergi dengan pendekatan berbagi pengalaman pribadi saya di sini termasuk kecenderungan dan kesalahan saya sendiri sepanjang karir saya. YMMV jauh.

Sebagai peringatan saya bekerja di daerah yang melibatkan beberapa basis kode skala sangat besar (jutaan LOC, kadang-kadang warisan selama beberapa dekade). Saya juga bekerja di bidang khusus di mana tidak ada jumlah komentar atau kejelasan kode yang dapat diterjemahkan ke pengembang yang kompeten untuk dapat memahami apa yang dilakukan oleh implementasi (kami tidak dapat serta merta mengambil pengembang yang layak dan membuatnya memahami implementasi suatu negara. Implementasi dinamika fluida canggih berdasarkan pada makalah yang diterbitkan 6 bulan lalu tanpa dia menghabiskan banyak waktu dari kode dalam spesialisasi bidang ini). Ini umumnya berarti bahwa hanya beberapa pengembang teratas yang dapat secara efektif memahami dan memelihara bagian tertentu dari basis kode.

Mengingat pengalaman khusus saya dan mungkin dikombinasikan dengan sifat khas industri ini, saya tidak lagi merasa produktif untuk menggunakan SoC, DRY, membuat implementasi fungsi dapat dibaca semudah mungkin, bahkan dapat digunakan kembali hingga batas maksimalnya demi YAGNI, decoupling, testability, tes tertulis, dokumentasi antarmuka (jadi kami setidaknya tahu cara menggunakan antarmuka bahkan jika implementasinya membutuhkan terlalu banyak pengetahuan khusus) dan akhirnya mengirim perangkat lunak.

Blok Lego

Saya sebenarnya cenderung untuk pergi ke arah yang berlawanan total awalnya di beberapa titik sebelumnya dalam karir saya. Saya sangat senang dengan pemrograman fungsional dan desain kelas kebijakan dalam Desain C ++ Modern dan metaprogramming template dan sebagainya. Khususnya saya senang dengan desain yang paling ringkas dan ortogonal di mana Anda memiliki semua fungsionalitas kecil ini (seperti "atom") yang dapat Anda gabungkan bersama (untuk membentuk "molekul") dengan cara yang tampaknya tak terbatas untuk mendapatkan hasil yang diinginkan. Itu membuat saya ingin menulis hampir semuanya sebagai fungsi yang terdiri dari beberapa baris kode, dan tidak ada yang secara inheren salah dengan fungsi sesingkat itu (masih bisa sangat luas dalam penerapan dan memperjelas kode), kecuali saya mulai berpikir dogmatis bahwa kode saya memiliki sesuatu yang salah jika ada fungsi yang membentang lebih dari beberapa baris. Dan saya mendapatkan beberapa mainan yang sangat rapi dan bahkan beberapa kode produksi dari jenis kode itu tetapi saya mengabaikan jam: jam dan hari dan minggu berlalu.

Khususnya ketika saya mengagumi kesederhanaan dari setiap "blok lego" kecil yang saya buat yang dapat saya gabungkan secara tak terbatas, saya mengabaikan jumlah waktu dan kekuatan otak yang saya gunakan untuk menyatukan semua blok ini bersama-sama untuk membentuk "alat" yang rumit. Terlebih lagi dalam kasus yang jarang tetapi menyakitkan di mana terjadi kesalahan dengan alat rumit, saya sengaja mengabaikan waktu yang saya habiskan untuk mencoba mencari tahu apa yang salah dengan menelusuri serangkaian panggilan fungsi yang tampaknya tak berujung menganalisis setiap bagian lego yang terdesentralisasi dan himpunan bagian dari kombinasi mereka ketika semuanya mungkin jauh lebih sederhana jika tidak dibuat dari "lego" ini, jika Anda mau, dan hanya ditulis sebagai segelintir fungsi yang lebih kecil atau kelas menengah.

Namun saya terus berputar-putar dan ketika tenggat waktu memaksa saya untuk menjadi lebih sadar akan waktu, saya mulai menyadari bahwa usaha saya mengajar saya lebih banyak tentang apa yang saya lakukan salah daripada apa yang saya lakukan dengan benar . Saya mulai sekali lagi menghargai fungsi dan objek / komponen meatier di sana-sini, bahwa ada lebih banyak cara pragmatis untuk mencapai tingkat yang wajar dari SoC seperti yang David Arnoditunjukkan dengan memisahkan input file dari pemrosesan string tanpa harus mendekomposisi pemrosesan string ke bawah. tingkat granular yang bisa dibayangkan.

Fungsi Meatier

Dan terlebih lagi saya mulai baik-baik saja dengan bahkan beberapa duplikasi kode, bahkan beberapa duplikasi logis (saya tidak mengatakan copy dan paste coding, semua yang saya bicarakan adalah menemukan "keseimbangan"), asalkan fungsi tidak rawan untuk membuat perubahan berulang dan didokumentasikan dalam hal penggunaannya dan yang paling penting diuji untuk memastikan fungsinya sesuai dengan apa yang didokumentasikan untuk dilakukan dan tetap seperti itu. Saya mulai menyadari bahwa penggunaan kembali sebagian besar terkait dengan keandalan .

Saya telah menyadari bahwa bahkan fungsi terkecil yang masih cukup tunggal dalam keprihatinan untuk tidak terlalu sempit diterapkan dan terlalu kikuk untuk digunakan dan diuji, bahkan jika itu menduplikasi beberapa logika dalam beberapa fungsi yang jauh di tempat lain dalam basis kode, dan asalkan itu teruji dengan baik dan dapat diandalkan dan tes-tes tersebut secara wajar memastikan tetap seperti itu, masih lebih disukai daripada kombinasi fungsi yang paling terurai dan fleksibel yang tidak memiliki kualitas ini. Jadi saya menyukai beberapa hal yang lebih baik belakangan ini jika itu dapat diandalkan .

Hal ini juga tampaknya saya bahwa sebagian besar waktu, lebih murah untuk menyadari bahwa Anda Apakah akan membutuhkan sesuatu di belakang dan menambahkannya, disediakan kode Anda setidaknya menerima tambahan baru tanpa Cascading api neraka, daripada kode segala macam hal ketika Anda aren 't akan membutuhkannya dan kemudian menghadapi godaan menghapus itu semua ketika itu mulai menjadi PITA nyata untuk mempertahankan.

Jadi itulah yang saya pelajari, itu adalah pelajaran yang saya anggap paling penting bagi saya untuk dipelajari secara pribadi dalam konteks ini, dan sebagai peringatan itu harus diambil dengan sebutir garam. YMMV. Namun semoga itu bermanfaat bagi Anda dalam membantu Anda menemukan jenis keseimbangan yang tepat untuk mengirimkan produk yang membuat pengguna Anda senang dalam jumlah waktu yang wajar dan menjaganya secara efektif.


-1

Masalah yang Anda temui, adalah Anda tidak memfaktorkan fungsi Anda ke bentuk yang paling berkurang. Lihatlah yang berikut ini: (Saya bukan programmer python, jadi potong sedikit saya)

def lines_from_file(file):
    with open(file, 'r') as text:
        line_number = 1
        lines = []
        for line in text:
            lines.append((line_number, line.strip()))
            line_number += 1
    return lines

def filter(l, func):
    new_l = []
    for x in l:
        if func(x):
            new_l.append(x)
    return new_l

def contains(needle):
    return lambda haystack: needle in haystack

def last(l):
    length = len(l)
    if length > 0:
        return l[length - 1]
    else:
        return None

Masing-masing fungsi di atas melakukan sesuatu yang sama sekali berbeda, dan saya yakin Anda akan mengalami kesulitan memfaktorkan fungsi-fungsi itu lebih jauh. Kita dapat menggabungkan fungsi-fungsi itu untuk menyelesaikan tugas yang ada.

lines = lines_from_file('./test_file')
filtered = filter(lines, lambda x : contains('some value')(x[1]))
line = last(filtered)
if line is not None:
    print(line[0])

Baris kode di atas dapat dengan mudah disatukan menjadi satu fungsi untuk melakukan apa yang Anda ingin lakukan. Cara untuk benar-benar memisahkan masalah adalah dengan memecah operasi kompleks menjadi bentuk yang paling diperhitungkan. Setelah Anda memiliki sekelompok fungsi yang difaktorkan dengan baik, Anda dapat mulai menyatukannya untuk menyelesaikan masalah yang lebih kompleks. Satu hal yang menyenangkan tentang fungsi-fungsi yang diperhitungkan dengan baik, adalah bahwa mereka sering dapat digunakan kembali di luar konteks masalah saat ini.


-2

Saya akan mengatakan bahwa memang tidak pernah ada terlalu banyak pemisahan kekhawatiran. Tetapi mungkin ada fungsi yang hanya Anda gunakan sekali, dan bahkan tidak menguji secara terpisah. Mereka dapat dengan aman diuraikan, menjaga pemisahan dari merembes ke namespace luar.

Contoh Anda benar-benar tidak perlu check_if_keyword_in_string, karena kelas string sudah menyediakan implementasi: keyword in linecukup. Tetapi Anda mungkin berencana untuk menukar implementasi, misalnya menggunakan pencarian Boyer-Moore, atau mengizinkan pencarian malas di generator; maka itu masuk akal.

Anda find_last_appearance_of_keywordbisa lebih umum, dan temukan tampilan terakhir item secara berurutan. Untuk itu, Anda dapat menggunakan implementasi yang ada, atau membuat implementasi yang dapat digunakan kembali. Juga dapat mengambil filter yang berbeda , sehingga Anda dapat mencari regex, atau untuk pertandingan yang tidak peka terhadap huruf besar-kecil, dll.

Biasanya segala sesuatu yang berhubungan dengan I / O layak untuk fungsi yang terpisah, jadi get_text_from_filemungkin ide yang baik jika Anda ingin menangani berbagai kasus khusus secara langsung. Mungkin tidak jika Anda mengandalkan IOErrorpenangan luar untuk itu.

Bahkan penghitungan garis dapat menjadi perhatian tersendiri jika di masa depan Anda mungkin perlu mendukung mis garis lanjutan (misalnya dengan \) dan akan membutuhkan nomor baris logis. Atau Anda mungkin perlu mengabaikan baris komentar, tanpa melanggar penomoran baris.

Mempertimbangkan:

def get_last_appearance_of_keyword(filename, keyword):
    with open(filename) as f:  # File-opening concern.
        numbered_lines = enumerate(f, start=1)  # Line-numbering concern.
        last_line = None  # Also a concern! Some e.g. prefer -1.
        for line_number, line in numbered_lines:  # The searching concern.
            if keyword in line: # The matching concern, applied.
                last_line = line_number
    # Here the file closes; an I/O concern again.
    return last_line

Lihat bagaimana Anda mungkin ingin memecah kode Anda ketika Anda mempertimbangkan beberapa masalah yang mungkin berubah di masa depan, atau hanya karena Anda memperhatikan bagaimana kode yang sama dapat digunakan kembali di tempat lain.

Ini adalah sesuatu yang harus diperhatikan ketika Anda menulis fungsi pendek dan manis yang asli. Sekalipun Anda belum membutuhkan masalah yang dipisahkan sebagai fungsi, pisahkan masalah tersebut sama praktisnya. Ini tidak hanya membantu untuk mengembangkan kode nanti, tetapi juga membantu untuk lebih memahami kode dengan segera, dan membuat lebih sedikit kesalahan.


-4

Kapan pemisahan "terlalu banyak"? Tidak pernah. Anda tidak dapat memiliki terlalu banyak pemisahan.

Contoh terakhir Anda cukup bagus, tetapi Anda mungkin bisa menyederhanakan perulangan for dengan text.GetLines(i=>i.containsKeyword)sesuatu.

* Versi praktis: Berhenti saat berfungsi. Pisahkan lebih banyak ketika rusak.


5
"Kamu tidak bisa memiliki terlalu banyak pemisahan." Saya pikir ini tidak benar. Contoh ketiga OP hanyalah penulisan ulang konstruksi python umum menjadi fungsi terpisah. Apakah saya benar-benar membutuhkan fungsi yang sama sekali baru hanya untuk melakukan 'jika x in y'?
cariehl

@cariehl Anda harus menambahkan jawaban dengan alasan kasus itu. Saya pikir Anda akan menemukan bahwa untuk benar-benar berfungsi, Anda akan memerlukan sedikit lebih banyak logika dalam fungsi-fungsi tersebut
Ewan

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.