มีสถานการณ์ที่คุณควรเลือกคลาสที่ไม่ใช่เคสหรือไม่?
Martin Odersky ทำให้เรามีจุดเริ่มต้นที่ดีในหลักสูตรFunctional Programming Principles ใน Scala (Lecture 4.6 - Pattern Matching) ที่เราสามารถใช้เมื่อต้องเลือกระหว่างคลาสและคลาสเคส บทที่ 7 ของScala By Exampleมีตัวอย่างเดียวกัน
สมมติว่าเราต้องการเขียนล่ามสำหรับนิพจน์เลขคณิต เพื่อให้สิ่งต่างๆง่ายขึ้นในตอนแรกเรา จำกัด ตัวเองไว้ที่ตัวเลขและการดำเนินการ + เท่านั้น expres- sions ดังกล่าวสามารถแสดงเป็นลำดับชั้นของคลาสโดยมี Expr คลาสพื้นฐานที่เป็นนามธรรมเป็นรูทและสองคลาสย่อย Number และ Sum จากนั้นนิพจน์ 1 + (3 + 7) จะแสดงเป็น
ผลรวมใหม่ (หมายเลขใหม่ (1) ผลรวมใหม่ (หมายเลขใหม่ (3) หมายเลขใหม่ (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
}
นอกจากนี้การเพิ่มคลาส Prod ใหม่ไม่ได้ทำให้เกิดการเปลี่ยนแปลงใด ๆ กับโค้ดที่มีอยู่:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
ในทางตรงกันข้ามการเพิ่มวิธีการใหม่ต้องมีการปรับเปลี่ยนคลาสที่มีอยู่ทั้งหมด
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(")")
}
}
ปัญหาเดียวกันได้รับการแก้ไขด้วยคลาสเคส
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
การเพิ่มวิธีการใหม่เป็นการเปลี่ยนแปลงเฉพาะที่
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(")")
}
}
}
การเพิ่มคลาส Prod ใหม่อาจต้องเปลี่ยนการจับคู่รูปแบบทั้งหมด
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 Pattern Matching
การออกแบบทั้งสองแบบนี้ทำได้ดีอย่างสมบูรณ์แบบและบางครั้งการเลือกระหว่างพวกเขาก็เป็นเรื่องของสไตล์ แต่ถึงกระนั้นก็มีเกณฑ์บางอย่างที่สำคัญ
เกณฑ์หนึ่งอาจเป็นได้คุณสร้างนิพจน์ย่อยใหม่บ่อยขึ้นหรือคุณสร้างเมธอดใหม่บ่อยขึ้น? ดังนั้นจึงเป็นเกณฑ์ที่พิจารณาถึงความสามารถในการขยายในอนาคตและส่วนขยายที่เป็นไปได้ของระบบของคุณ
หากสิ่งที่คุณทำส่วนใหญ่เป็นการสร้างคลาสย่อยใหม่จริง ๆ แล้วโซลูชันการสลายตัวที่มุ่งเน้นวัตถุจะเป็นมือบน เหตุผลก็คือมันง่ายมากและเป็นการเปลี่ยนแปลงในท้องถิ่นเพียงแค่สร้างคลาสย่อยใหม่ด้วยวิธีการ evalโดยที่ในโซลูชันการทำงานคุณจะต้องย้อนกลับและเปลี่ยนรหัสภายในวิธีการ eval และเพิ่มกรณีใหม่ ไปเลย
ในทางกลับกันหากสิ่งที่คุณทำจะเป็นการสร้างวิธีการใหม่ ๆ มากมาย แต่ลำดับชั้นของคลาสนั้นจะยังคงค่อนข้างคงที่ดังนั้นการจับคู่รูปแบบจึงเป็นประโยชน์อย่างแท้จริง เพราะอีกครั้งวิธีการใหม่แต่ละวิธีในโซลูชันการจับคู่รูปแบบเป็นเพียงการเปลี่ยนแปลงเฉพาะที่ไม่ว่าคุณจะวางไว้ในคลาสฐานหรืออาจจะอยู่นอกลำดับชั้นของคลาส ในขณะที่วิธีการใหม่เช่นการแสดงในการสลายตัวเชิงวัตถุจะต้องมีการเพิ่มขึ้นใหม่คือแต่ละคลาสย่อย ดังนั้นจะมีหลายส่วนที่คุณต้องสัมผัส
ดังนั้นปัญหาของการขยายนี้ในสองมิติที่คุณอาจต้องการที่จะเพิ่มชั้นเรียนใหม่ในการลำดับชั้นหรือคุณอาจต้องการที่จะเพิ่มวิธีการใหม่หรืออาจจะทั้งสองได้รับการตั้งชื่อปัญหาการแสดงออก
โปรดจำไว้ว่าเราต้องใช้สิ่งนี้เป็นจุดเริ่มต้นและไม่เหมือนกับเกณฑ์เดียว