Scala vaka sınıflarını bildirmenin dezavantajları nelerdir?


105

Çok sayıda güzel, değişmez veri yapısı kullanan bir kod yazıyorsanız, vaka sınıfları bir nimet gibi görünür ve size aşağıdakilerin tümünü tek bir anahtar kelimeyle ücretsiz olarak verir:

  • Varsayılan olarak her şey değişmez
  • Getiriciler otomatik olarak tanımlanır
  • İyi toString () uygulaması
  • Uyumlu eşittir () ve hashCode ()
  • Eşleştirme için unapply () yöntemine sahip tamamlayıcı nesne

Fakat değişmez bir veri yapısını bir vaka sınıfı olarak tanımlamanın dezavantajları nelerdir?

Sınıfa veya müşterilerine ne gibi kısıtlamalar getiriyor?

Vakasız bir sınıfı tercih etmeniz gereken durumlar var mı?


Şu ilgili soruya bakın: stackoverflow.com/q/4635765/156410
David

18
Bu neden yapıcı değil? Bu sitedeki modlar çok katı. Bunun sınırlı sayıda olası gerçek yanıtı vardır.
Eloff

5
Eloff ile anlaşın. Bu benim de cevap almak istediğim bir soru ve verilen cevaplar çok yararlı ve sübjektif görünmüyor. Daha fazla tartışma ve fikir ortaya çıkaran birçok 'kod pasajımı nasıl düzeltirim' sorusunu gördüm.
Herc

Yanıtlar:


51

Büyük bir dezavantaj: vaka sınıfları vaka sınıfını genişletemez. Kısıtlama budur.

Kaçırdığınız diğer avantajlar, eksiksizlik için listelenir: uyumlu serileştirme / seriyi kaldırma, oluşturmak için "yeni" anahtar sözcüğü kullanmaya gerek yoktur.

Değişken durumu olan, özel durumu olan veya durumu olmayan nesneler için büyük / küçük harf olmayan sınıfları tercih ederim (örneğin çoğu tek bileşenli bileşen). Hemen hemen her şey için vaka sınıfları.


48
Bir vaka sınıfını alt sınıflara ayırabilirsiniz. Alt sınıf da bir durum sınıfı olamaz - bu kısıtlamadır.
Seth Tisue

99

Önce iyi kısımlar:

Varsayılan olarak her şey değişmez

Evet ve varihtiyacınız olursa geçersiz kılınabilir (kullanarak )

Getiriciler otomatik olarak tanımlanır

Herhangi bir sınıfta, parametrelerin önüne val

İyi toString()uygulama

Evet, çok kullanışlı, ancak gerekirse herhangi bir sınıfta elle yapılabilir

Uyumlu equals()vehashCode()

Kolay kalıp eşleştirmeyle birleştirildiğinde, insanların vaka sınıflarını kullanmasının ana nedeni budur

unapply()Eşleştirme yöntemine sahip tamamlayıcı nesne

Ekstraktör kullanarak herhangi bir sınıfta elle yapmak da mümkündür

Bu liste aynı zamanda Scala 2.8'e gelebilecek en iyi şeylerden biri olan son derece güçlü kopyalama yöntemini de içermelidir.


O zaman kötü, vaka sınıflarında yalnızca bir avuç gerçek kısıtlama var:

applyCompanion nesnesinde, derleyici tarafından oluşturulan yöntemle aynı imzayı kullanarak tanımlayamazsınız

Pratikte bu nadiren bir sorundur. Oluşturulan uygulama yönteminin değişen davranışının kullanıcıları şaşırtması garanti edilir ve kesinlikle önerilmemelidir, bunu yapmanın tek gerekçesi girdi parametrelerini doğrulamaktır - bu, en iyi ana yapıcı gövdesinde yapılan bir görevdir (aynı zamanda kullanım sırasında doğrulamayı da kullanılabilir kılar copy)

Alt sınıf yapamazsınız

Doğru, yine de bir vaka sınıfının kendi soyundan gelmesi mümkün. Yaygın bir model, ağacın yaprak düğümleri olarak vaka sınıflarını kullanarak bir özellik sınıf hiyerarşisi oluşturmaktır.

sealedDeğiştiriciye de dikkat çekmeye değer . Bu değiştirici ile bir özelliğin alt sınıflarının gerekir aynı dosya içinde bildirilebilir. Özelliğin örnekleriyle örüntü eşleştirirken, derleyici tüm olası somut alt sınıfları kontrol etmediyseniz sizi uyarabilir. Vaka sınıflarıyla birleştirildiğinde, bu, kodunuz uyarı vermeden derlenirse, size çok yüksek düzeyde güven sağlayabilir.

Ürünün bir alt sınıfı olarak, vaka sınıfları 22'den fazla parametreye sahip olamaz

Bu kadar çok parametreyle sınıfları kötüye kullanmayı durdurmak dışında gerçek bir çözüm yok :)

Ayrıca...

Bazen belirtilen diğer bir kısıtlama, Scala'nın (şu anda) tembel parametreleri ( lazy vals gibi , ancak parametreler olarak) desteklememesidir. Bunun geçici çözümü, bir ad-ad parametresi kullanmak ve bunu yapıcıdaki tembel bir değere atamaktır. Ne yazık ki, takma ad parametreleri desen eşleştirmeyle karışmaz, bu da tekniğin derleyici tarafından üretilen çıkarıcıyı bozarken vaka sınıflarıyla kullanılmasını engeller.

Bu, son derece işlevsel tembel veri yapıları uygulamak istiyorsanız önemlidir ve umarız Scala'nın gelecekteki bir sürümüne tembel parametrelerin eklenmesiyle çözülür.


1
Kapsamlı cevap için teşekkürler. Sanırım her şey istisnası "Alt sınıf yapamazsın" muhtemelen yakın zamanda beni aşamalı hale getirmeyecek.
Graham Lea

15
Bir vaka sınıfını alt sınıflara ayırabilirsiniz. Alt sınıf da bir durum sınıfı olamaz - bu kısıtlamadır.
Seth Tisue

5
Vaka sınıfları için 22 parametreli sınır Scala 2.11'de kaldırılmıştır. konular.scala-lang.org/browse/SI-7296
Jonathan Crosmer

"Companion nesnesinde uygulamayı, derleyici tarafından oluşturulan yöntemle aynı imzayı kullanarak tanımlayamazsınız" şeklinde bir iddia yanlıştır. Bunu yapmak için bazı çemberlerin üzerinden atlamayı gerektirse de (eskiden scala derleyicisi tarafından görünmez bir şekilde oluşturulan işlevselliği korumayı düşünüyorsanız), kesinlikle başarılabilir: stackoverflow.com/a/25538287/501113
chaotic3quilibrium

Scala vaka sınıflarını kapsamlı bir şekilde kullanıyorum ve yukarıda tanımlanan bazı sorunlara yardımcı olan bir "vaka sınıfı modeli" (sonunda bir Scala makrosu olarak sona erecek) buldum
chaotic3quilibrium

10

Sanırım TDD ilkesi burada geçerli: aşırı tasarım yapmayın. Bir şeyi a case classolarak ilan ettiğinizde, birçok işlevsellik beyan etmiş olursunuz. Bu, gelecekte sınıfı değiştirirken sahip olduğunuz esnekliği azaltacaktır.

Örneğin , yapıcı parametreleri üzerinde case classbir equalsyöntemi vardır . Sınıfınızı ilk yazdığınızda bunu önemsemeyebilirsiniz, ancak daha sonra, eşitliğin bu parametrelerin bazılarını görmezden gelmesini veya biraz farklı bir şey yapmasını istediğinize karar verebilirsiniz. Ancak, müşteri kodu case classeşitliğe bağlı olan ortalama sürede yazılabilir .


4
İstemci kodunun 'eşittir' kelimesinin tam anlamına bağlı olması gerektiğini düşünmüyorum; "Eşittir" in onun için ne anlama geldiğine karar vermek sınıfa bağlıdır. Sınıf yazarı, "eşittir" ifadesinin uygulanmasını değiştirmekte özgür olmalıdır.
pkaeding

8
@pkaeding Herhangi bir özel yönteme bağlı müşteri koduna sahip olmamakta özgürsünüz. Herkese açık olan her şey, kabul ettiğiniz bir sözleşmedir.
Daniel C. Sobral

3
@ DanielC.Sobral True, ancak equals () 'ın (temel aldığı alanlar) tam olarak uygulanması sözleşmede zorunlu değildir. En azından sınıfı ilk yazdığınızda onu sözleşmeden açıkça hariç tutabilirsiniz.
herman

2
@ DanielC.Sobral Kendinizle çelişiyorsunuz: İnsanların varsayılan eşittir uygulamasına (nesne kimliğini karşılaştıran) bile güveneceğini söylüyorsunuz. Bu doğruysa ve daha sonra farklı bir eşittir uygulaması yazarsanız, kodları da kırılır. Her neyse, ön / son koşulları ve değişmezleri belirtirseniz ve insanlar bunları görmezden gelirse, bu onların sorunu.
herman

2
@herman Söylediklerimde hiçbir çelişki yok. "Onların sorunu" na gelince, tabii ki, senin sorunun olmadıkça . Örneğin, girişiminizin büyük bir müşterisi oldukları için ya da yöneticileri üst yönetimi değiştirmenin kendileri için çok maliyetli olduğuna ikna ettiği için, değişikliklerinizi geri almanız ya da değişikliğin milyonlarca dolara neden olması nedeniyle diyelim. hata verir ve geri döner vb. Ancak hobi için kod yazıyorsanız ve kullanıcıları umursamıyorsanız, devam edin.
Daniel C. Sobral

7

Vakasız bir sınıfı tercih etmeniz gereken durumlar var mı?

Martin Odersky, Scala'daki İşlevsel Programlama İlkeleri (Ders 4.6 - Örüntü Eşleştirme) dersinde, sınıf ve durum sınıfı arasında seçim yapmamız gerektiğinde kullanabileceğimiz iyi bir başlangıç ​​noktası veriyor . Scala By Example'un 7. bölümü aynı örneği içermektedir.

Diyelim ki aritmetik ifadeler için bir yorumlayıcı yazmak istiyoruz. Başlangıçta işleri basit tutmak için kendimizi sadece sayılar ve + işlemlerle sınırlandırıyoruz. Bu tür ifadeler, kök olarak bir soyut temel sınıf İfade ve iki alt sınıf Sayı ve Toplam ile bir sınıf hiyerarşisi olarak temsil edilebilir. Daha sonra 1 + (3 + 7) ifadesi şu şekilde temsil edilir:

yeni Toplam (yeni Sayı (1), yeni Toplam (yeni Sayı (3), yeni Sayı (7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

Ayrıca, yeni bir Prod sınıfı eklemek, mevcut kodda herhangi bir değişiklik gerektirmez:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

Buna karşılık, yeni bir yöntem eklemek, mevcut tüm sınıfların değiştirilmesini gerektirir.

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

Aynı problem vaka sınıfları ile çözüldü.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

Yeni bir yöntem eklemek yerel bir değişikliktir.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

Yeni bir Prod sınıfı eklemek, potansiyel olarak tüm desen eşleşmelerinin değiştirilmesini gerektirir.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

Videolecture 4.6 Örüntü Eşleştirme'den transkript

Bu tasarımların her ikisi de mükemmeldir ve aralarında seçim yapmak bazen bir stil meselesidir, ancak yine de önemli olan bazı kriterler vardır.

Kriterlerden biri, daha sık yeni ifade alt sınıfları mı oluşturuyorsunuz yoksa daha sık yeni yöntemler mi oluşturuyorsunuz? Yani bu, gelecekteki genişletilebilirliğe ve sisteminizin olası genişletme geçişine bakan bir kriterdir.

Yaptığınız şey çoğunlukla yeni alt sınıflar oluşturmaksa, aslında nesneye yönelik ayrıştırma çözümünün üstünlüğü vardır. Bunun nedeni, bir eval yöntemiyle yeni bir alt sınıf oluşturmanın çok kolay ve çok yerel bir değişiklik olmasıdır ; burada işlevsel çözümde olduğu gibi, eval yönteminin içindeki kodu değiştirip yeni bir durum eklemeniz gerekir. ona.

Öte yandan, yaptığınız şey çok sayıda yeni yöntem yaratmaksa, ancak sınıf hiyerarşisinin kendisi nispeten sabit tutulacaksa, model eşleştirme aslında avantajlıdır. Çünkü, yine, desen eşleştirme çözümündeki her yeni yöntem , ister temel sınıfa, ister sınıf hiyerarşisinin dışına bile koysanız , yalnızca yerel bir değişikliktir . Nesne yönelimli ayrıştırmada göster gibi yeni bir yöntem, her alt sınıf için yeni bir artış gerektirecektir. Yani dokunmanız gereken daha fazla parça olacaktır.

Dolayısıyla, bir hiyerarşiye yeni sınıflar eklemek isteyebileceğiniz veya yeni yöntemler veya belki her ikisini birden eklemek isteyebileceğiniz iki boyutta bu genişletilebilirliğin problematiği ifade problemi olarak adlandırılmıştır .

Unutmayın: Bunu bir başlangıç ​​noktası olarak kullanmalıyız ve tek kriter gibi değil.

görüntü açıklamasını buraya girin


0

Ben bu quoting Scala cookbooktarafından Alvin Alexanderbölüm 6: objects.

Bu, bu kitapta ilginç bulduğum pek çok şeyden biri.

Bir vaka sınıfı için birden çok kurucu sağlamak için, vaka sınıfı bildiriminin gerçekte ne yaptığını bilmek önemlidir.

case class Person (var name: String)

Scala derleyicisinin vaka sınıfı örneği için ürettiği koda bakarsanız, bunun Person $ .class ve Person.class olmak üzere iki çıktı dosyası oluşturduğunu görürsünüz. Kişi $ .class'ı javap komutuyla parçalara ayırırsanız, bunun bir uygulama yöntemi ve diğer pek çok yöntem içerdiğini görürsünüz:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

Ne içerdiğini görmek için Person.class'ı da parçalarına ayırabilirsiniz. Bunun gibi basit bir sınıf için ek 20 yöntem içerir; bu gizli şişkinlik, bazı geliştiricilerin vaka sınıflarını sevmemesinin bir nedenidir.

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.