Haruskah saya membuat kelas jika fungsi saya kompleks dan memiliki banyak variabel?

40

Pertanyaan ini agak agnostik bahasa, tetapi tidak sepenuhnya, karena Object Oriented Programming (OOP) berbeda di, misalnya, Java , yang tidak memiliki fungsi kelas satu, daripada di Python .

Dengan kata lain, saya merasa kurang bersalah karena membuat kelas yang tidak perlu dalam bahasa seperti Java, tapi saya merasa mungkin ada cara yang lebih baik dalam bahasa yang kurang boilerplate-y seperti Python.

Program saya perlu melakukan operasi yang relatif kompleks beberapa kali. Operasi itu membutuhkan banyak "pembukuan", harus membuat dan menghapus beberapa file sementara, dll.

Itu sebabnya ia juga perlu memanggil banyak "sub-operasi" lainnya - memasukkan semuanya ke dalam satu metode besar tidak terlalu bagus, modular, mudah dibaca, dll.

Sekarang ini adalah pendekatan yang muncul di pikiran saya:

1. Buat kelas yang hanya memiliki satu metode publik dan pertahankan keadaan internal yang diperlukan untuk suboperasi dalam variabel instan.

Akan terlihat seperti ini:

class Thing:

    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2
        self.var3 = []

    def the_public_method(self, param1, param2):
        self.var4 = param1
        self.var5 = param2
        self.var6 = param1 + param2 * self.var1
        self.__suboperation1()
        self.__suboperation2()
        self.__suboperation3()


    def __suboperation1(self):
        # Do something with self.var1, self.var2, self.var6
        # Do something with the result and self.var3
        # self.var7 = something
        # ...
        self.__suboperation4()
        self.__suboperation5()
        # ...

    def suboperation2(self):
        # Uses self.var1 and self.var3

#    ...
#    etc.

Masalah yang saya lihat dengan pendekatan ini adalah bahwa keadaan kelas ini hanya masuk akal secara internal, dan tidak dapat melakukan apa pun dengan instance-nya kecuali memanggil satu-satunya metode publik mereka.

# Make a thing object
thing = Thing(1,2)

# Call the only method you can call
thing.the_public_method(3,4)

# You don't need thing anymore

2. Membuat banyak fungsi tanpa kelas dan meneruskan berbagai variabel yang diperlukan secara internal di antara mereka (sebagai argumen).

Masalah yang saya lihat dengan ini adalah saya harus melewati banyak variabel antar fungsi. Juga, fungsi-fungsi itu akan terkait erat satu sama lain, tetapi mereka tidak akan dikelompokkan bersama.

3. Suka 2. tetapi buat variabel keadaan global alih-alih melewatinya.

Ini tidak akan bagus sama sekali, karena saya harus melakukan operasi lebih dari sekali, dengan input berbeda.

Apakah ada pendekatan keempat yang lebih baik? Jika tidak, pendekatan mana yang lebih baik, dan mengapa? Apakah ada sesuatu yang saya lewatkan?

Saya bisa belajar
sumber
9
"Masalah yang saya lihat dengan pendekatan ini adalah bahwa keadaan kelas ini hanya masuk akal secara internal, dan tidak dapat melakukan apa pun dengan instance-nya kecuali memanggil satu-satunya metode publik mereka." Anda telah mengartikulasikan ini sebagai masalah, tetapi tidak mengapa Anda berpikir demikian.
Patrick Maupin
@ Patrick Maupin - Anda benar. Dan saya tidak begitu tahu, itulah masalahnya. Rasanya seperti saya menggunakan kelas untuk hal yang harus digunakan sesuatu yang lain, dan ada banyak hal di Python yang belum saya jelajahi, jadi saya pikir mungkin seseorang akan menyarankan sesuatu yang lebih cocok.
iCanLearn
Mungkin ini tentang menjadi jelas tentang apa yang saya coba lakukan. Seperti, saya tidak melihat ada yang salah dengan menggunakan kelas biasa sebagai pengganti enum di Jawa, tetapi masih ada hal-hal yang enum lebih alami. Jadi pertanyaan ini sebenarnya tentang apakah ada pendekatan yang lebih alami untuk apa yang saya coba lakukan.
iCanLearn
3
kemungkinan rangkap dari Apa tanggung jawab nyata suatu kelas?
nyamuk
1
Jika Anda hanya membubarkan kode yang ada ke dalam metode dan variabel instan dan tidak mengubah apa pun tentang strukturnya, maka Anda tidak mendapatkan apa-apa selain kehilangan kejelasan. (1) adalah ide yang buruk.
usr

Jawaban:

47
  1. Buat kelas yang hanya memiliki satu metode publik dan pertahankan keadaan internal yang diperlukan untuk suboperasi dalam variabel instan.

Masalah yang saya lihat dengan pendekatan ini adalah bahwa keadaan kelas ini hanya masuk akal secara internal, dan tidak dapat melakukan apa pun dengan instance-nya kecuali memanggil satu-satunya metode publik mereka.

Opsi 1 adalah contoh enkapsulasi yang digunakan dengan benar. Anda ingin status internal disembunyikan dari kode luar.

Jika itu berarti kelas Anda hanya memiliki satu metode publik, maka biarlah. Akan jauh lebih mudah untuk dirawat.

Dalam OOP jika Anda memiliki kelas yang melakukan tepat 1 hal, memiliki permukaan publik kecil, dan menjaga semua keadaan internalnya tersembunyi, maka Anda (seperti yang dikatakan Charlie Sheen) WINNING .

  1. Buatlah banyak fungsi tanpa kelas dan berikan berbagai variabel yang diperlukan secara internal di antara mereka (sebagai argumen).

Masalah yang saya lihat dengan ini adalah saya harus melewati banyak variabel antar fungsi. Juga, fungsi-fungsi tersebut akan terkait erat satu sama lain, tetapi tidak akan dikelompokkan bersama.

Opsi 2 menderita kohesi rendah . Ini akan membuat perawatan lebih sulit.

  1. Suka 2. tetapi buat variabel keadaan global alih-alih meneruskannya.

Opsi 3, seperti opsi 2, menderita kohesi rendah, tetapi jauh lebih parah!

Sejarah telah menunjukkan bahwa kenyamanan variabel global tidak sebanding dengan biaya pemeliharaan brutal yang ditimbulkannya. Itu sebabnya Anda mendengar kentut tua seperti saya mengomentari enkapsulasi sepanjang waktu.


Pilihan yang menang adalah # 1 .

MetaFight
sumber
6
# 1 adalah API yang cukup jelek. Jika saya memutuskan untuk membuat kelas untuk ini, saya mungkin akan membuat satu fungsi publik yang mendelegasikan ke kelas dan membuat seluruh kelas menjadi pribadi. ThingDoer(var1, var2).do_it()vs. do_thing(var1, var2).
user2357112 mendukung Monica
7
Sementara opsi 1 adalah pemenang yang jelas, saya akan melangkah lebih jauh. Alih-alih menggunakan keadaan objek internal, gunakan variabel lokal dan parameter metode. Ini membuat fungsi publik kembali, yang lebih aman.
4
Satu hal tambahan yang dapat Anda lakukan dalam ekstensi opsi 1: buat kelas yang dihasilkan terlindungi (menambahkan garis bawah di depan nama), dan kemudian tentukan fungsi tingkat modul def do_thing(var1, var2): return _ThingDoer(var1, var2).run(), untuk membuat API eksternal sedikit lebih cantik.
Sjoerd Job Postmus
4
Saya tidak mengikuti alasan Anda untuk 1. Keadaan internal sudah tersembunyi. Menambahkan kelas tidak mengubah itu. Karena itu saya tidak mengerti mengapa Anda merekomendasikan (1). Bahkan tidak perlu mengekspos keberadaan kelas sama sekali.
usr
3
Setiap kelas yang Anda instantiate dan kemudian segera memanggil satu metode dan kemudian tidak pernah menggunakan lagi adalah bau kode utama, bagi saya. Ini isomorfik untuk fungsi sederhana, hanya dengan aliran data yang dikaburkan (sebagai bagian dari implementasi berkomunikasi dengan menetapkan variabel pada instance throwaway alih-alih melewati parameter). Jika fungsi internal Anda mengambil begitu banyak parameter sehingga panggilan menjadi rumit dan sulit, kode tidak menjadi lebih rumit ketika Anda menyembunyikan aliran data itu!
Ben
23

Saya pikir # 1 sebenarnya adalah pilihan yang buruk.

Mari pertimbangkan fungsi Anda:

def the_public_method(self, param1, param2):
    self.var4 = param1
    self.var5 = param2 
    self.var6 = param1 + param2 * self.var1
    self.__suboperation1()
    self.__suboperation2()
    self.__suboperation3()

Bagian mana dari data yang digunakan suboperation1? Apakah ia mengumpulkan data yang digunakan oleh suboperation2? Saat Anda meneruskan data dengan menyimpannya sendiri, saya tidak bisa memastikan bagaimana fungsionalitas terkait. Ketika saya melihat diri sendiri, beberapa atribut berasal dari konstruktor, beberapa dari panggilan ke the_public_method, dan beberapa ditambahkan secara acak di tempat lain. Menurut pendapat saya, ini berantakan.

Bagaimana dengan nomor # 2? Pertama, mari kita lihat masalah kedua:

Juga, fungsi-fungsi tersebut akan terkait erat satu sama lain, tetapi tidak akan dikelompokkan bersama.

Mereka akan berada dalam modul bersama, sehingga mereka benar-benar akan dikelompokkan bersama.

Masalah yang saya lihat dengan ini adalah saya harus melewati banyak variabel antar fungsi.

Menurut saya, ini bagus. Ini membuat eksplisit ketergantungan data dalam algoritme Anda. Dengan menyimpannya baik dalam variabel global atau pada diri, Anda menyembunyikan dependensi dan membuatnya tampak tidak terlalu buruk, tetapi masih ada.

Biasanya, ketika situasi ini muncul, itu berarti Anda belum menemukan cara yang tepat untuk menguraikan masalah Anda. Anda merasa aneh untuk memecah menjadi beberapa fungsi karena Anda mencoba membaginya dengan cara yang salah.

Tentu saja, tanpa melihat fungsi Anda yang sebenarnya, sulit menebak apa saran yang baik. Tapi Anda memberi sedikit petunjuk tentang apa yang Anda hadapi di sini:

Program saya perlu melakukan operasi yang relatif kompleks beberapa kali. Operasi itu membutuhkan banyak "pembukuan", harus membuat dan menghapus beberapa file sementara dll.

Biarkan saya memilih contoh sesuatu yang agak sesuai dengan deskripsi Anda, sebuah installer. Pemasang harus menyalin banyak file, tetapi jika Anda membatalkannya sebagian, Anda harus memundurkan seluruh proses termasuk mengembalikan semua file yang Anda ganti. Algoritma untuk ini terlihat seperti:

def install_program():
    copied_files = []
    try:
        for filename in FILES_TO_COPY:
           temporary_file = create_temporary_file()
           copy(target_filename(filename), temporary_file)
           copied_files = [target_filename(filename), temporary_file)
           copy(source_filename(filename), target_filename(filename))
     except CancelledException:
        for source_file, temp_file in copied_files:
            copy(temp_file, source_file)
     else:
        for source_file, temp_file in copied_files:
            delete(temp_file)

Sekarang gandakan logika itu karena harus melakukan pengaturan registri, ikon program, dll, dan Anda punya cukup banyak kekacauan.

Saya pikir solusi # 1 Anda terlihat seperti:

class Installer:
    def install(self):
        try:
            self.copy_new_files()
        except CancellationError:
            self.restore_original_files()
        else:
            self.remove_temp_files()

Ini memang membuat keseluruhan algoritma lebih jelas, tetapi menyembunyikan cara berbagai bagian berkomunikasi.

Pendekatan # 2 terlihat seperti:

def install_program():
    try:
       temp_files = copy_new_files()
    except CancellationError as error:
       restore_old_files(error.files_that_were_copied)
    else:
       remove_temp_files(temp_files)

Sekarang secara eksplisit bagaimana potongan-potongan data berpindah antar fungsi, tetapi sangat canggung.

Jadi bagaimana seharusnya fungsi ini ditulis?

def install_program():
    with FileTransactionLog() as file_transaction_log:
         copy_new_files(file_transaction_log)

Objek FileTransactionLog adalah manajer konteks. Ketika copy_new_files menyalin file, ia melakukannya melalui FileTransactionLog yang menangani pembuatan salinan sementara dan melacak file mana yang telah disalin. Dalam kasus pengecualian itu menyalin file asli kembali, dan jika berhasil, itu akan menghapus salinan sementara.

Ini berhasil karena kami telah menemukan dekomposisi tugas yang lebih alami. Sebelumnya, kami menggabungkan logika tentang cara menginstal aplikasi dengan logika tentang cara memulihkan instalasi yang dibatalkan. Sekarang log transaksi menangani semua detail tentang file sementara dan pembukuan, dan fungsinya dapat fokus pada algoritma dasar.

Saya menduga bahwa kasus Anda berada di kapal yang sama. Anda perlu mengekstraksi elemen pembukuan menjadi semacam objek sehingga tugas kompleks Anda dapat diekspresikan lebih sederhana dan elegan.

Winston Ewert
sumber
9

Karena satu-satunya kelemahan jelas dari metode 1 adalah pola penggunaannya yang kurang optimal, saya pikir solusi terbaik adalah mendorong enkapsulasi satu langkah lebih jauh: Gunakan kelas, tetapi juga menyediakan fungsi berdiri bebas, yang hanya membangun objek, memanggil metode dan mengembalikan :

def publicFunction(var1, var2, param1, param2)
    thing = Thing(var1, var2)
    thing.theMethod(param1, param2)

Dengan itu, Anda memiliki antarmuka sekecil mungkin untuk kode Anda, dan kelas yang Anda gunakan secara internal benar-benar hanya menjadi detail implementasi dari fungsi publik Anda. Kode panggilan tidak perlu tahu tentang kelas internal Anda.

cmaster
sumber
4

Di satu sisi, pertanyaannya entah bagaimana agnostik bahasa; tetapi di sisi lain, implementasinya tergantung pada bahasa dan paradigmanya. Dalam hal ini adalah Python, yang mendukung banyak paradigma.

Selain solusi Anda, ada juga kemungkinan, untuk melakukan operasi melengkapi stateless dengan cara yang lebih fungsional, misalnya

def outer(param1, param2):
    def inner1(param1, param2, param3):
        pass
    def inner2(param1, param2):
        pass
    return inner2(inner1(param1),param2,param3)

Semuanya bermuara pada

  • keterbacaan
  • konsistensi
  • rawatan

Tetapi -Jika basis kode Anda adalah OOP, itu melanggar konsistensi jika tiba-tiba beberapa bagian ditulis dalam gaya fungsional (lebih).

Thomas Junk
sumber
4

Mengapa Mendesain saat saya bisa menjadi Pengodean?

Saya menawarkan pandangan yang bertentangan dengan jawaban yang saya baca. Dari perspektif itu semua jawaban dan terus terang bahkan pertanyaan itu sendiri fokus pada mekanisme pengkodean. Tapi ini masalah desain.

Haruskah saya membuat kelas jika fungsi saya kompleks dan memiliki banyak variabel?

Ya, karena masuk akal untuk desain Anda. Ini mungkin kelas tersendiri atau bagian dari kelas lain atau perilakunya dapat didistribusikan di antara kelas-kelas.


Desain Berorientasi Objek adalah tentang Kompleksitas

Maksud dari OO adalah untuk berhasil membangun dan memelihara sistem yang besar dan kompleks dengan merangkum kode dalam hal sistem itu sendiri. "Tepat" desain mengatakan semuanya ada di beberapa kelas.

Desain OO secara alami mengelola kompleksitas terutama melalui kelas-kelas terfokus yang mematuhi prinsip tanggung jawab tunggal. Kelas-kelas ini memberikan struktur dan fungsionalitas sepanjang napas dan kedalaman sistem, berinteraksi dan mengintegrasikan dimensi-dimensi ini.

Mengingat hal itu, sering dikatakan bahwa fungsi yang tergantung pada sistem - kelas utilitas umum yang terlalu umum - adalah bau kode yang menunjukkan desain yang tidak memadai. Saya cenderung setuju.

radarbob
sumber
Desain OO secara alami mengelola kompleksitas OOP secara alami memperkenalkan kompleksitas yang tidak perlu.
Miles Rout
"Kompleksitas yang tidak perlu" mengingatkan saya pada komentar tak ternilai dari rekan kerja seperti "oh, itu terlalu banyak kelas." Pengalaman memberi tahu saya bahwa jus itu layak diperas. Paling-paling saya melihat hampir seluruh kelas dengan metode 1-3 baris panjang dan dengan setiap kelas melakukan bagian kompleksitas itu dari setiap metode yang diberikan diminimalkan: LOC pendek tunggal membandingkan dua koleksi dan mengembalikan duplikat - tentu saja ada kode di balik itu. Jangan bingung "banyak kode" dengan kode kompleks. Bagaimanapun, tidak ada yang gratis.
radarbob
1
Banyak kode "sederhana" yang berinteraksi dengan cara yang rumit jauh lebih buruk daripada sejumlah kecil kode kompleks.
Rute Mil
1
Sejumlah kecil kode kompleks mengandung kompleksitas . Ada kerumitan, tetapi hanya di sana. Itu tidak bocor. Ketika Anda memiliki banyak sekali potongan-potongan sederhana individual yang bekerja bersama dalam cara yang benar-benar kompleks dan sulit dipahami itu jauh lebih membingungkan karena tidak ada batas atau dinding untuk kompleksitas.
Miles Rout
3

Mengapa tidak membandingkan kebutuhan Anda dengan sesuatu yang ada di pustaka Python standar, dan kemudian lihat bagaimana itu diterapkan?

Catatan, jika Anda tidak membutuhkan objek, Anda masih bisa mendefinisikan fungsi di dalam fungsi. Dengan Python 3 ada nonlocaldeklarasi baru untuk memungkinkan Anda mengubah variabel di fungsi induk Anda.

Anda mungkin masih merasa berguna untuk memiliki beberapa kelas privat sederhana di dalam fungsi Anda untuk mengimplementasikan abstraksi dan merapikan operasi.

meuh
sumber
Terima kasih. Dan apakah Anda tahu sesuatu di perpustakaan standar yang terdengar seperti apa yang saya miliki di pertanyaan?
iCanLearn
Saya akan kesulitan mengutip sesuatu menggunakan fungsi bersarang, memang saya tidak menemukan nonlocaldi mana pun di libs python saya yang terinstal. Mungkin Anda dapat mengambil hati dari textwrap.pyyang memiliki TextWrapperkelas, tetapi juga def wrap(text)fungsi yang hanya membuat TextWrapper contoh, memanggil .wrap()metode di atasnya dan kembali. Jadi gunakan kelas, tetapi tambahkan beberapa fungsi kenyamanan.
meuh