Eğer fonksiyonum karmaşıksa ve çok fazla değişken varsa, bir sınıf oluşturmalı mıyım?


40

Nesne yönelimli programlama (OOP), örneğin, birinci sınıf işlevlere sahip olmayan Java'da Python'da olduğundan farklı olduğundan, bu soru biraz agnostiktir, ancak tamamen değildir .

Başka bir deyişle, Java gibi bir dilde gereksiz sınıflar oluşturduğum için kendimi daha az suçlu hissediyorum, ancak Python gibi daha az dil bilen dillerde daha iyi bir yol olabileceğini düşünüyorum.

Programımın birkaç kez nispeten karmaşık bir işlem yapması gerekiyor. Bu işlem çok fazla "defter tutma" gerektirir, bazı geçici dosyalar yaratmalı ve silmeli.

Bu yüzden aynı zamanda bir çok başka “alt işlem” olarak da adlandırılması gerekiyor - her şeyi büyük bir yönteme koymak çok hoş, modüler, okunabilir, vb. Değil.

Şimdi bunlar aklıma gelen yaklaşımlar:

1. Yalnızca bir genel metoda sahip olan ve alt işlemler için gereken iç durumu örnek değişkenlerinde tutan bir sınıf yapın.

Bu gibi bir şey olurdu:

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.

Bu yaklaşımla gördüğüm sorun, bu sınıfın durumunun yalnızca içsel olarak anlam ifade etmesi ve örneklerini tek kamuya açık yöntemini çağırmak dışında hiçbir şey yapamamasıdır.

# 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. Sınıfsız bir sürü fonksiyon yapın ve içsel olarak ihtiyaç duyulan çeşitli değişkenleri aralarında (argümanlar olarak) iletin.

Bununla ilgili gördüğüm sorun, fonksiyonlar arasında çok fazla değişken geçirmem gerekiyor. Ayrıca, işlevler birbirleriyle yakından ilişkili olacak, ancak birlikte gruplandırılmayacaktı.

3. Gibi 2. ancak durum değişkenlerini geçmek yerine küresel hale getirin.

İşlemi farklı girişlerle bir kereden fazla yapmak zorunda olduğum için bu hiç iyi olmaz.

Dördüncü, daha iyi bir yaklaşım var mı? Olmazsa, bu yaklaşımlardan hangisi daha iyi olurdu ve neden? Kaçırdığım bir şey mi var?


9
“Bu yaklaşımla gördüğüm sorun, bu sınıfın durumunun yalnızca içsel olarak anlam ifade etmesi ve kendi örnekleriyle tek genel yöntemini çağırmak dışında örnekleri ile hiçbir şey yapamamasıdır.” Bunu bir problem olarak dile getirdiniz, ama neden düşündüğünüzü değil.
Patrick Maupin

@Patrick Maupin - Haklısın. Ve gerçekten bilmiyorum, sorun bu. Sadece başka bir şeyin kullanılması gereken bir şey için sınıfları kullanıyorum ve Python'da henüz keşfedilmediğim birçok şey varmış gibi hissediyorum, bu yüzden belki de birisinin daha uygun bir şeyler önereceğini düşündüm.
iCanLearn 12:15

Belki ne yapmaya çalıştığım konusunda net olmakla ilgilidir. Mesela, Java'daki enums yerine normal sınıfları kullanmakla ilgili yanlış bir şey görmüyorum, fakat yine de enums için daha doğal olan şeyler var. Yani bu soru gerçekten yapmaya çalıştığım şeye daha doğal bir yaklaşım olup olmadığına ilişkin.
iCanLearn 15


1
Mevcut kodunuzu sadece yöntemlere ve örnek değişkenlere dağıtır ve yapısıyla ilgili hiçbir şeyi değiştirmezseniz netliği kaybetmekten başka bir şey kazanmazsınız. (1) kötü bir fikirdir.
usr

Yanıtlar:


47
  1. Yalnızca bir genel yönteme sahip bir sınıf yapın ve alt işlemler için gereken iç durumu örnek değişkenlerinde tutar.

Bu yaklaşımla gördüğüm sorun, bu sınıfın durumunun yalnızca içsel olarak anlam ifade etmesi ve örnekleriyle kendi ortak yöntemini çağırmak dışında hiçbir şey yapamamasıdır.

Seçenek 1, doğru kullanılan iyi bir kapsülleme örneğidir . Sen istiyorsun iç devlet dış koddan görünmemesini sağlar.

Bu, sınıfınızın yalnızca bir genel yöntemi olduğu anlamına gelirse, o zaman öyle olsun. Bakımı o kadar kolay olacak.

OOP'de, tam olarak 1 şeyi yapan, küçük bir kamusal yüzeye sahip ve tüm içsel durumunu gizli tutan bir sınıfınız varsa, o zaman (Charlie Sheen'ın dediği gibi) KAZANMAKtasınız .

  1. Sınıfsız bir sürü fonksiyon yapın ve içsel olarak ihtiyaç duyulan çeşitli değişkenleri aralarında (argümanlar olarak) iletin.

Bununla ilgili gördüğüm sorun, fonksiyonlar arasında çok fazla değişken geçirmem gerekiyor. Ayrıca, işlevler birbirleriyle yakından ilişkili olacaktır, ancak birlikte gruplandırılmayacaktır.

Seçenek 2 düşük uyumdan muzdariptir . Bu bakım daha zor hale getirecek.

  1. 2. gibi ama durum değişkenlerini geçmek yerine küresel hale getirin.

Seçenek 3, seçenek 2 gibi, düşük uyumdan muzdarip, ancak çok daha ciddi!

Tarihçe, küresel değişkenlerin kolaylığının getirdiği acımasız bakım maliyetinden ağır basıldığını göstermiştir. Bu yüzden benim gibi yaşlı osurukların enkapsülasyonu sürekli duyduğunu duyuyorsunuz.


Kazanan seçenek 1'dir .


6
# 1, yine de oldukça çirkin bir API. Bunun için bir sınıf yapmaya karar verirsem, muhtemelen sınıfa delege atan ve tüm sınıfı özel yapan tek bir kamu işlevi yapardım. ThingDoer(var1, var2).do_it()vs do_thing(var1, var2).
user2357112

7
Seçenek 1 açık bir kazanan olsa da, bir adım daha ileri gideceğim. Dahili nesne durumunu kullanmak yerine, yerel değişkenleri ve yöntem parametrelerini kullanın. Bu, kamu işlevini daha güvenli olan reentrant yapar.

4
Seçenek 1'in genişletilmesinde yapabileceğiniz ek bir şey: ortaya çıkan sınıfı korumalı hale getirin (adın önüne bir alt çizgi ekleyerek) ve ardından def do_thing(var1, var2): return _ThingDoer(var1, var2).run()harici API'yi biraz daha güzel hale getirmek için modül düzeyinde bir işlev tanımlayın .
Sjoerd Job Postmus, 12:15

4
Nedenini 1 olarak takip etmiyorum. İç durum zaten gizli. Bir sınıf eklemek bunu değiştirmez. Bu yüzden neden önerdiğini anlamıyorum (1). Aslında, sınıfın varlığını hiçbir şekilde ortaya koymak gerekli değildir.
usr

3
Örneklemeniz ve ardından hemen tek bir yöntem çağırıp tekrar kullanmamanız gereken herhangi bir sınıf, benim için büyük bir kod kokusudur. Basit bir fonksiyona izomorfiktir, sadece gizlenmiş veri akışı ile (uygulamanın parçalara ayrılması, değişken parametreler yerine atma örneğine değişkenler atayarak iletişim kurar). Dahili işlevleriniz aramaların karmaşık ve hantal olmasını sağlayacak kadar çok parametre alırsa, bu veri akışını gizlediğinizde kod daha az karmaşık olmaz !
Ben

23

Bence # 1 aslında kötü bir seçenek.

İşlevinizi düşünelim:

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()

Alt işlem hangi veri parçalarını kullanır? Alt işlem tarafından kullanılan verileri toplar mı2? Verileri kendi üzerine kaydederek etrafa aktardığınızda, işlevsellik parçalarının nasıl ilişkili olduğunu söyleyemem. Kendime baktığımda, niteliklerin bir kısmı yapıcı, bir kısmı da public_method'a, bazıları ise başka yerlere rasgele eklenmiş. Bence bu bir karışıklık.

Peki ya 2 numara? Öncelikle, ikinci probleme bakalım:

Ayrıca, işlevler birbirleriyle yakından ilişkili olacaktır, ancak birlikte gruplandırılmayacaktır.

Birlikte bir modülde olacaklar, böylece tamamen birlikte gruplandırılacaklardı.

Bununla ilgili gördüğüm sorun, fonksiyonlar arasında çok fazla değişken geçirmem gerekiyor.

Bence bu iyi. Bu, algoritmanızdaki veri bağımlılıklarını açıkça ortaya koyar. Onları küresel değişkenlerde ya da bir kişide saklayarak, bağımlılıkları gizler ve daha az kötü görünmelerini sağlarsınız, ancak hala oradalar.

Genellikle, bu durum ortaya çıktığında, bu sorununuzu çözmek için doğru yolu bulamadığınız anlamına gelir. Birden çok işleve ayrılmanın zor olduğunu görüyorsunuz çünkü yanlış yoldan ayırmaya çalışıyorsunuz.

Tabii ki, gerçek işlevinizi görmeden iyi bir öneri olacağını tahmin etmek zor. Ama burada neyle uğraştığınızla ilgili bir ipucu veriyorsunuz:

Programımın birkaç kez nispeten karmaşık bir işlem yapması gerekiyor. Bu işlem çok fazla "defter tutma" gerektirir, bazı geçici dosyalar yaratmalı ve silmeli.

Sorta tanımınıza uyan bir şey örneği seçmeme izin verin, bir yükleyici. Bir yükleyicinin bir sürü dosyayı kopyalaması gerekir, ancak bir kısmını iptal ederseniz, değiştirdiğiniz dosyaları geri koymak da dahil olmak üzere tüm işlemi geri sarmanız gerekir. Bunun için algoritma gibi görünüyor:

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)

Şimdi, kayıt defteri ayarlarını, program simgelerini vb. Yapmak zorunda kaldığınız için bu mantığı çarpın.

1 numaralı çözümünüzün şöyle olduğunu düşünüyorum:

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

Bu, genel algoritmayı daha açık hale getirir, ancak farklı parçaların iletişim şeklini gizler.

Yaklaşım # 2 şuna benzer:

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)

Şimdi, verilerin parçalarının fonksiyonlar arasında nasıl hareket ettiğini açıkça ifade ediyor, ancak bu çok garip.

Peki bu fonksiyon nasıl yazılmalıdır?

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

FileTransactionLog nesnesi bir içerik yöneticisidir. Copy_new_files bir dosyayı kopyaladığında, geçici kopyalamayı yapan ve hangi dosyaların kopyalandığını takip eden FileTransactionLog üzerinden yapar. İstisna durumunda, orijinal dosyaları geri kopyalar ve başarı durumunda geçici kopyaları siler.

Bu işe yarıyor, çünkü görevin daha doğal bir şekilde ayrışmasını bulduk. Önceden, iptal edilen bir kurulumun nasıl kurtarılacağıyla ilgili uygulamanın mantıkla nasıl kurulacağı hakkındaki mantığı karıştırıyorduk. Şimdi işlem günlüğü geçici dosyalar ve defter tutma ile ilgili tüm detayları ele alıyor ve fonksiyon temel algoritmaya odaklanabiliyor.

Davanızın aynı teknede olduğundan şüpheleniyorum. Kayıt tutma öğelerini bir tür nesneye çıkartmanız gerekir, böylece karmaşık göreviniz daha basit ve zarif bir şekilde ifade edilebilir.


9

Metod 1'in tek belirgin dezavantajı en düşük kullanım şekli olduğu için, en iyi çözümün enkapsülasyonu bir adım daha ileri götürmek olduğunu düşünüyorum: Bir sınıf kullanın, ancak aynı zamanda sadece nesneyi inşa eden, metodu çağıran ve döndüren serbest duran bir fonksiyon sağlayan :

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

Bununla, kodunuz için mümkün olan en küçük arayüze sahipsiniz ve dahili olarak kullandığınız sınıf gerçekten genel işlevinizin bir uygulama detayı haline geliyor. Arama kodunu hiçbir zaman dahili sınıfınız hakkında bilmek gerekmez.


4

Bir yandan, soru bir şekilde dilden agnostik; ancak diğer yandan, uygulama dile ve paradigmalarına bağlıdır. Bu durumda, çoklu paradigmaları destekleyen Python'dur.

Çözümlerinizin yanı sıra, işlemlerin daha işlevsel bir şekilde vatansız olarak yapılması da mümkündür.

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

Her şey aşağı iniyor

  • okunabilirliği
  • tutarlılık
  • maintainability

Ancak, kod tabanınız OOP ise, bazı kısımlar (daha fazla) işlevsel bir tarzda yazıldığında aniden tutarlılığı ihlal eder.


4

Neden Kodlama Yapabildiğimde Tasarım?

Okuduğum cevaplara çelişkili bir görüş sunuyorum. Bu açıdan tüm cevaplar ve hatta sorunun kendisi bile kodlama mekaniğine odaklanıyor. Ancak bu bir tasarım konusudur.

Eğer fonksiyonum karmaşıksa ve çok fazla değişken varsa, bir sınıf oluşturmalı mıyım?

Evet, tasarımınız için mantıklı olduğu gibi. Kendi başına bir sınıf veya başka bir sınıfın bir parçası olabilir veya davranışı sınıflar arasında dağıtılabilir.


Nesneye Dayalı Tasarım Karmaşıklık Hakkında

OO'nun amacı, kodu sistemin kendisi açısından içine alarak büyük, karmaşık sistemleri başarıyla oluşturmak ve sürdürmektir. “Uygun” tasarım her şeyin bir sınıfta olduğunu söylüyor.

OO tasarımı doğal olarak karmaşıklığı öncelikle tek bir sorumluluk ilkesine bağlı odaklanmış sınıflarla yönetir. Bu sınıf, sistemin nefesi ve derinliği boyunca yapı ve işlevsellik sağlar, bu boyutları birbiriyle etkileşime geçirir ve birleştirir.

Buna bakıldığında, sık sık sistem hakkında sarkan fonksiyonların - hepsi çok yaygın olan genel fayda sınıfı - yetersiz tasarım öneren bir kod kokusu olduğu söylenir. Katılmaya eğilimliyim.


OO tasarımı doğal olarak karmaşıklığı yönetir OOP doğal olarak gereksiz karmaşıklığı da beraberinde getirir.
Miles Rout,

“Gereksiz karmaşıklık” bana “ah, bu çok fazla sınıf” gibi iş arkadaşlarının paha biçilmez yorumlarını hatırlatıyor. Tecrübe bana suyu sıkmaya değdiğini söylüyor. Neredeyse en iyi yöntemle 1-3 satır uzunluğundaki metodlarla neredeyse bütün sınıfları görüyorum ve her sınıfın belirli bir yöntemin parçalı karmaşıklığını en aza indirgiyorum: İki koleksiyonu karşılaştıran ve kopyaları iade eden tek bir kısa LOC - elbette bunun arkasında kod var. "Çok fazla kod" u karmaşık kodla karıştırmayın. Her durumda, hiçbir şey ücretsiz değildir.
radarbob

1
Karmaşık bir şekilde etkileşime giren "basit" kodların çoğu, az miktarda karmaşık koddan daha kötüdür.
Miles Rout,

1
Az miktarda karmaşık kod karmaşıklık içeriyordu . Karmaşıklık var, ama sadece orada. Sızıntı yapmaz. Gerçekten karmaşık ve anlaşılması zor bir şekilde birlikte çalışan çok sayıda ayrı ayrı basit parçaya sahip olduğunuzda, daha karmaşık bir sınır yoktur veya duvarlar yoktur.
Miles Rout

3

Neden ihtiyaçlarınızı standart Python kütüphanesinde bulunan bir şeyle kıyaslamıyorsunuz ve bunun nasıl uygulandığını görüyorsunuz?

Bir nesneye ihtiyacınız yoksa, yine de işlevler içinde işlevleri tanımlayabileceğinizi unutmayın. Python 3 ile nonlocalebeveyn fonksiyonunuzdaki değişkenleri değiştirmenize izin veren yeni bir bildirim var .

Soyutlamaları ve toplama işlemlerini uygulamak için fonksiyonunuzda bazı basit özel sınıfları bulundurmanız yine de yararlı olabilir.


Teşekkürler. Ve standart kütüphanede, benim sorum olana benzeyen bir şey biliyor musunuz?
iCanLearn 12:15

İç içe geçmiş işlevler kullanarak bir şeyler alıntı yapmak zor olurdu, aslında nonlocalşu anda yüklü python kütüphanelerimden hiçbir yerde bulamıyorum . Belki textwrap.pybir TextWrappersınıfa sahip olan kalbi alabilir , aynı zamanda def wrap(text)basit bir TextWrapper örnek oluşturan , .wrap()üzerinde bulunan metodu çağıran ve geri dönen bir işlev de alabilirsiniz . Yani bir sınıf kullanın, ancak bazı kolaylık işlevleri ekleyin.
meuh
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.