การสืบทอดคลาสเคส Scala


91

ฉันมีแอปพลิเคชันที่ใช้ Squeryl ฉันกำหนดโมเดลของฉันเป็นคลาสเคสส่วนใหญ่เนื่องจากฉันสะดวกที่จะมีวิธีการคัดลอก

ฉันมีสองรุ่นที่เกี่ยวข้องกันอย่างเคร่งครัด ฟิลด์เหมือนกันการดำเนินการหลายอย่างเหมือนกันและจะถูกเก็บไว้ในตาราง DB เดียวกัน แต่มีพฤติกรรมบางอย่างที่สมเหตุสมผลในหนึ่งในสองกรณีเท่านั้นหรือที่สมเหตุสมผลในทั้งสองกรณี แต่แตกต่างกัน

จนถึงตอนนี้ฉันใช้คลาสเคสเดียวเท่านั้นโดยมีแฟล็กที่แยกแยะประเภทของโมเดลและเมธอดทั้งหมดที่แตกต่างกันตามประเภทของโมเดลเริ่มต้นด้วย if นี่เป็นเรื่องที่น่ารำคาญและไม่ค่อยปลอดภัย

สิ่งที่ฉันต้องการจะทำคือแยกพฤติกรรมทั่วไปและฟิลด์ในคลาสเคสของบรรพบุรุษและให้โมเดลจริงทั้งสองสืบทอดมาจากโมเดลนั้น แต่เท่าที่ฉันเข้าใจการสืบทอดจากคลาสเคสนั้นขมวดอยู่ใน Scala และเป็นสิ่งต้องห้ามด้วยซ้ำหากคลาสย่อยนั้นเป็นคลาสเคส (ไม่ใช่เคสของฉัน)

อะไรคือปัญหาและข้อผิดพลาดที่ฉันควรทราบในการสืบทอดจากคลาสเคส มันสมเหตุสมผลไหมในกรณีของฉันที่จะทำเช่นนั้น


1
คุณไม่สามารถสืบทอดจากคลาสที่ไม่ใช่เคสหรือขยายลักษณะทั่วไปได้หรือไม่?
Eduardo

ฉันไม่แน่ใจ. ฟิลด์ถูกกำหนดไว้ในบรรพบุรุษ ฉันต้องการรับวิธีการคัดลอกความเท่าเทียมกันและอื่น ๆ ตามช่องเหล่านั้น ถ้าฉันประกาศพาเรนต์เป็นคลาสนามธรรมและเด็ก ๆ เป็นคลาสเคสจะใช้พารามิเตอร์บัญชีที่กำหนดบนพาเรนต์หรือไม่
Andrea

ฉันคิดว่าไม่คุณต้องกำหนดอุปกรณ์ประกอบฉากทั้งในพาเรนต์นามธรรม (หรือลักษณะ) และคลาสเคสเป้าหมาย ในท้ายที่สุดแล้วแผ่นสำเร็จรูปของล็อต แต่พิมพ์อย่างปลอดภัยอย่างน้อยที่สุด
Virtualeyes

คำตอบ:


121

วิธีที่ฉันต้องการในการหลีกเลี่ยงการสืบทอดคลาสเคสโดยไม่มีการทำซ้ำโค้ดค่อนข้างชัดเจน: สร้างคลาสพื้นฐานทั่วไป (นามธรรม):

abstract class Person {
  def name: String
  def age: Int
  // address and other properties
  // methods (ideally only accessors since it is a case class)
}

case class Employer(val name: String, val age: Int, val taxno: Int)
    extends Person

case class Employee(val name: String, val age: Int, val salary: Int)
    extends Person


หากคุณต้องการละเอียดมากขึ้นให้จัดกลุ่มคุณสมบัติเป็นลักษณะเฉพาะ:

trait Identifiable { def name: String }
trait Locatable { def address: String }
// trait Ages { def age: Int }

case class Employer(val name: String, val address: String, val taxno: Int)
    extends Identifiable
    with    Locatable

case class Employee(val name: String, val address: String, val salary: Int)
    extends Identifiable
    with    Locatable

84
"โดยไม่ต้องทำรหัสซ้ำ" ที่คุณพูดถึงอยู่ที่ไหน ใช่สัญญาถูกกำหนดระหว่างคลาสเคสและพาเรนต์ แต่คุณยังคงพิมพ์อุปกรณ์ประกอบฉาก X2 อยู่
Virtualeyes

6
@virtualeyes ทรูยังต้องย้ำสรรพคุณอีก คุณไม่จำเป็นต้องทำซ้ำวิธีการซึ่งโดยปกติจะมีจำนวนรหัสมากกว่าคุณสมบัติ
Malte Schwerhoff

1
ใช่เพียงหวังว่าจะหลีกเลี่ยงการซ้ำซ้อนของคุณสมบัติ - คำตอบอีกคำตอบที่คลาสประเภทเป็นวิธีแก้ปัญหาที่เป็นไปได้ อย่างไรก็ตามไม่แน่ใจว่าดูเหมือนว่าจะมุ่งเน้นไปที่การผสมผสานในพฤติกรรมเช่นลักษณะนิสัย แต่มีความยืดหยุ่นมากกว่า เพียงต้นแบบ Re: เรียนกรณีที่สามารถมีชีวิตอยู่กับมันจะสวยอย่างไม่น่าเชื่อว่ามันเป็นอย่างอื่นจริงๆอาจตัดออกจากการเพาะปลูกที่ดีของคำนิยามคุณสมบัติ
virtualeyes

1
@virtualeyes ฉันเห็นด้วยอย่างยิ่งว่ามันจะดีมากหากสามารถหลีกเลี่ยงการซ้ำซ้อนของทรัพย์สินได้ด้วยวิธีง่ายๆ ปลั๊กอินคอมไพเลอร์สามารถทำเคล็ดลับได้อย่างแน่นอน แต่ฉันจะไม่เรียกว่าเป็นวิธีที่ง่าย
Malte Schwerhoff

13
@virtualeyes ฉันคิดว่าการหลีกเลี่ยงการทำโค้ดซ้ำไม่ใช่แค่การเขียนน้อยลงเท่านั้น สำหรับฉันมันเป็นมากกว่าการไม่มีโค้ดชิ้นเดียวกันในส่วนต่างๆของแอปพลิเคชันของคุณโดยไม่มีการเชื่อมต่อระหว่างกัน ด้วยโซลูชันนี้คลาสย่อยทั้งหมดจะเชื่อมโยงกับสัญญาดังนั้นหากคลาสพาเรนต์เปลี่ยนไป IDE จะสามารถช่วยคุณระบุส่วนของโค้ดที่คุณต้องการแก้ไขได้
Daniel

40

เนื่องจากนี่เป็นหัวข้อที่น่าสนใจสำหรับหลาย ๆ คนผมขออธิบายตรงนี้

คุณสามารถใช้แนวทางต่อไปนี้:

// You can mark it as 'sealed'. Explained later.
sealed trait Person {
  def name: String
}

case class Employee(
  override val name: String,
  salary: Int
) extends Person

case class Tourist(
  override val name: String,
  bored: Boolean
) extends Person

ใช่คุณต้องทำซ้ำช่อง ถ้าคุณทำไม่ได้มันก็จะเป็นไปไม่ได้ที่จะใช้ความเสมอภาคที่ถูกต้องของปัญหาอื่น

อย่างไรก็ตามคุณไม่จำเป็นต้องทำซ้ำวิธีการ / ฟังก์ชัน

หากการทำซ้ำคุณสมบัติบางอย่างมีความสำคัญกับคุณมากให้ใช้คลาสปกติ แต่อย่าลืมว่าคุณสมบัติเหล่านี้ไม่เหมาะกับ FP

หรือคุณสามารถใช้องค์ประกอบแทนการสืบทอด:

case class Employee(
  person: Person,
  salary: Int
)

// In code:
val employee = ...
println(employee.person.name)

การจัดองค์ประกอบเป็นกลยุทธ์ที่ถูกต้องและเป็นกลยุทธ์ที่ดีที่คุณควรพิจารณาเช่นกัน

และในกรณีที่คุณสงสัยว่าลักษณะการปิดผนึกหมายถึงอะไร - เป็นสิ่งที่สามารถขยายได้เฉพาะในไฟล์เดียวกัน นั่นคือคลาสเคสสองคลาสด้านบนจะต้องอยู่ในไฟล์เดียวกัน สิ่งนี้ช่วยให้สามารถตรวจสอบคอมไพเลอร์โดยละเอียด:

val x = Employee(name = "Jack", salary = 50000)

x match {
  case Employee(name) => println(s"I'm $name!")
}

ให้ข้อผิดพลาด:

warning: match is not exhaustive!
missing combination            Tourist

ซึ่งมีประโยชน์จริงๆ. ตอนนี้คุณจะไม่ลืมที่จะจัดการกับประเภทอื่น ๆ ของPersons (คน) นี่คือสิ่งที่Optionชั้นเรียนใน Scala ทำ

หากนั่นไม่สำคัญสำหรับคุณคุณสามารถทำให้มันไม่ปิดผนึกและโยนคลาสเคสลงในไฟล์ของพวกมัน และอาจจะไปกับองค์ประกอบ


1
ผมคิดว่าในลักษณะที่จะต้องมีdef name val nameคอมไพเลอร์ของฉันกำลังให้คำเตือนรหัสที่ไม่สามารถเข้าถึงได้กับอดีต
BAR

13

คลาสเคสเหมาะสำหรับออบเจ็กต์ค่าเช่นอ็อบเจ็กต์ที่ไม่เปลี่ยนแปลงคุณสมบัติใด ๆ และสามารถเปรียบเทียบกับเท่ากับ

แต่การใช้งานเท่ากับต่อหน้ามรดกนั้นค่อนข้างซับซ้อน พิจารณาสองคลาส:

class Point(x : Int, y : Int)

และ

class ColoredPoint( x : Int, y : Int, c : Color) extends Point

ดังนั้นตามคำจำกัดความ ColorPoint (1,4, สีแดง) ควรจะเท่ากับ Point (1,4) ซึ่งเป็นจุดเดียวกัน ดังนั้น ColorPoint (1,4, สีน้ำเงิน) ก็ควรจะเท่ากับ Point (1,4) ด้วยใช่ไหม? แต่แน่นอนว่า ColorPoint (1,4, สีแดง) ไม่ควรเท่ากับ ColorPoint (1,4, สีน้ำเงิน) เนื่องจากมีสีต่างกัน เอาล่ะคุณสมบัติพื้นฐานอย่างหนึ่งของความสัมพันธ์ความเท่าเทียมกันเสีย

อัพเดต

คุณสามารถใช้การสืบทอดจากลักษณะการแก้ปัญหามากมายดังที่อธิบายไว้ในคำตอบอื่น ทางเลือกที่ยืดหยุ่นยิ่งขึ้นคือการใช้คลาสประเภท ดูประเภทคลาสใน Scala มีประโยชน์สำหรับอะไร? หรือhttp://www.youtube.com/watch?v=sVMES4RZF-8


ฉันเข้าใจและเห็นด้วยกับสิ่งนั้น ดังนั้นสิ่งที่คุณแนะนำควรทำเมื่อคุณมีแอปพลิเคชันที่เกี่ยวข้องกับนายจ้างและลูกจ้าง สมมติว่าพวกเขาแชร์ฟิลด์ทั้งหมด (ชื่อที่อยู่และอื่น ๆ ) ความแตกต่างเพียงอย่างเดียวในบางวิธี - ตัวอย่างเช่นอาจต้องการกำหนดEmployer.fire(e: Emplooyee)แต่ไม่ใช่ในทางกลับกัน ฉันต้องการสร้างคลาสสองคลาสที่แตกต่างกันเนื่องจากมันเป็นตัวแทนของวัตถุที่แตกต่างกัน แต่ฉันก็ไม่ชอบการทำซ้ำที่เกิดขึ้น
Andrea

มีตัวอย่างวิธีการเรียนประเภทพร้อมคำถามที่นี่? เช่นในชั้นเรียนกรณี
Virtualeyes

@virtualeyes หนึ่งอาจมีประเภทที่เป็นอิสระอย่างสมบูรณ์สำหรับเอนทิตีประเภทต่างๆและจัดเตรียมคลาสประเภทเพื่อให้พฤติกรรม คลาส Type เหล่านี้สามารถใช้การสืบทอดได้มากพอ ๆ กับที่มีประโยชน์เนื่องจากไม่ถูกผูกมัดโดยสัญญาทางความหมายของคลาสเคส คำถามนี้จะมีประโยชน์หรือไม่? ไม่รู้คำถามไม่เฉพาะเจาะจงพอที่จะบอกได้
Jens Schauder

@JensSchauder ดูเหมือนว่าลักษณะจะให้สิ่งเดียวกันในแง่ของพฤติกรรมเพียง แต่มีความยืดหยุ่นน้อยกว่าคลาสประเภท ฉันได้รับคุณสมบัติที่ไม่ซ้ำซ้อนของคลาสเคสบางอย่างที่ลักษณะหรือคลาสนามธรรมมักจะช่วยให้เราหลีกเลี่ยงได้
Virtualeyes

7

ในสถานการณ์เหล่านี้ฉันมักจะใช้การจัดองค์ประกอบแทนการสืบทอดเช่น

sealed trait IVehicle // tagging trait

case class Vehicle(color: String) extends IVehicle

case class Car(vehicle: Vehicle, doors: Int) extends IVehicle

val vehicle: IVehicle = ...

vehicle match {
  case Car(Vehicle(color), doors) => println(s"$color car with $doors doors")
  case Vehicle(color) => println(s"$color vehicle")
}

เห็นได้ชัดว่าคุณสามารถใช้ลำดับชั้นและการจับคู่ที่ซับซ้อนมากขึ้นได้ แต่หวังว่านี่จะช่วยให้คุณได้แนวคิด กุญแจสำคัญคือการใช้ประโยชน์จากตัวแยกที่ซ้อนกันซึ่งคลาสเคสมีให้


3
ดูเหมือนจะเป็นคำตอบเดียวที่นี่ซึ่งไม่มีช่องที่ซ้ำกันอย่างแท้จริง
Alan Thomas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.