`by` ให้ forwarder ฟรี แล้วเก็บกับดักไว้ในที่ที่มองไม่เห็น
บทที่ 4 ทำให้ orderctl คิดยอดได้ บทนี้ทำให้มันรับกฎส่วนลดที่ยังไม่มีใครเขียน
Kotlin ให้เครื่องมือสองอย่างที่ Java ไม่มีเลย คือ by กับ extension function
· ทั้งสองอย่างดูเหมือนลัดทางได้ และทั้งสองอย่างมีที่ที่มันเงียบสนิท
build.gradle.kts กับ settings.gradle.kts ที่บทที่ 3 เขียนไว้ ยังใช้ตัวเดิมทั้งหมด
· บทนี้ไม่เพิ่ม dependency สักบรรทัด
ห้องทดลองของบทนี้อยู่ใน src/main/kotlin/orderctl/RulesDemo.kt · ชี้บรรทัด mainClass
ใน build.gradle.kts ไปที่ orderctl.RulesDemoKt แบบเดียวกับบทที่ 4
ทุกผลลัพธ์ในบทนี้มาจาก gradle -q run · ธง -q ตัดรายงานของ Gradle ออก
เหลือเฉพาะสิ่งที่โปรแกรมพิมพ์เอง
กฎส่วนลดตัวแรกของ orderctl
หัวข้อที่มีชื่อว่า “กฎส่วนลดตัวแรกของ orderctl”interface ตัวเดียวถือของสี่อย่าง สองอย่างที่ผู้ implement ต้องเขียน และสองอย่างที่ได้มาฟรี
DiscountRule รับยอดหนึ่งก้อนแล้วคืนยอดใหม่ หน่วยเป็นสตางค์ตามที่บทที่ 2 ตกลงไว้
package orderctl
interface DiscountRule { fun name(): String
fun applySatang(subtotalSatang: Long): Long
fun describe(): String = "กฎ " + name()
fun applyTwice(subtotalSatang: Long): Long = applySatang(applySatang(subtotalSatang))}describe() กับ applyTwice() มีเนื้อความติดมาด้วย ผู้ implement จึงไม่ต้องเขียนสองตัวนี้
· Kotlin ไม่ต้องมีคำว่า default นำหน้า เพราะ interface ของ Kotlin มีเนื้อความได้ตั้งแต่วันแรก
ส่วน Java ต้องพิมพ์คำว่า default กำกับ และความสามารถข้อนั้นเพิ่งมาที่ Java 8
· บทที่ 5 ของคอร์ส Java
ไล่เส้นเวลาของมันไว้สามข้อ
ผู้ implement สองตัวแรกเขียนความผูกพันไว้ที่ตัวเองตรงๆ ด้วยเครื่องหมายทวิภาค
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี DiscountRule อยู่ในโปรเจกต์ด้วยpackage orderctl
class PercentOff(private val pct: Int) : DiscountRule {
init { require(pct in 0..100) { "pct ต้องอยู่ใน 0..100 แต่ได้ $pct" } }
override fun name() = "percent-$pct"
override fun applySatang(subtotalSatang: Long) = subtotalSatang - subtotalSatang * pct / 100}
class FlatOff(private val offSatang: Long) : DiscountRule {
override fun name() = "flat-$offSatang"
override fun applySatang(subtotalSatang: Long) = maxOf(0L, subtotalSatang - offSatang)}override ของ Kotlin เป็นคำในภาษา ไม่ใช่ annotation ที่เติมหรือไม่เติมก็ได้แบบ @Override
ของ Java ซึ่งบทที่ 5 ของคอร์สนั้นเขียนเองว่า “ไม่ได้บังคับ”
ลืมพิมพ์แล้วการแปลแดงทันทีด้วย 'name' hides member of supertype 'DiscountRule' and needs an 'override' modifier.
PercentOff ข้างบนไม่มีคำว่า open การสืบทอดจึงทำไม่ได้เลย
// ❌ แปลไม่ผ่านpackage orderctl
class CappedPercentOff(pct: Int) : PercentOff(pct)e: file:///…/src/main/kotlin/orderctl/Probe.kt:4:36 This type is final, so it cannot be extended.Java กลับด้านกับข้อนี้ — class Base เปล่าๆ ที่ไม่มีคำว่า final ถูกสืบทอดได้เลย
javac ของ JDK 25 คืนรหัสออก 0 · Kotlin เลือกให้การเปิดเป็นเรื่องที่ต้องประกาศ
ผลคือเครื่องมือที่เหลือให้ต่อของเข้ากับกฎเดิมมีสองอย่าง คือ interface กับการห่อ
extension function เติม method ให้ชนิดของคนอื่นได้ แต่พาเข้า interface ไม่ได้
หัวข้อที่มีชื่อว่า “extension function เติม method ให้ชนิดของคนอื่นได้ แต่พาเข้า interface ไม่ได้”ชนิดที่มี method ครบทุกตัวตามรูปร่าง ยังไม่ใช่สมาชิกของ interface ถ้าไม่ได้ประกาศไว้
สมมติว่า Coupon มาจาก library ของทีมอื่น เราแก้ file ต้นทางของมันไม่ได้
extension functionextension functionfunction ที่เรียกเหมือนเป็นสมาชิกของชนิดที่เราไม่ได้เป็นเจ้าของ · compiler เลือกตัวไหนตัดสินตอนแปลจากชนิดที่ประกาศ ไม่ใช่ตอนรันจากชนิดจริง สมาชิกใน `List<T>` จึงได้ตัวของ `T` เสมอ เติม name() กับ applySatang() เข้าไปได้ทั้งสองตัว
แล้วเรียกได้เหมือนเป็นสมาชิกจริง · แต่พอเอาไปวางในที่ที่ต้องการ DiscountRule มันแดง
// ❌ แปลไม่ผ่าน · src/main/kotlin/orderctl/Coupon.ktpackage orderctl
class Coupon(val code: String, val offSatang: Long)
fun Coupon.name(): String = "coupon-" + code
fun Coupon.applySatang(subtotalSatang: Long): Long = maxOf(0L, subtotalSatang - offSatang)
val songkran: DiscountRule = Coupon("SONGKRAN", 3_000L)e: file:///…/src/main/kotlin/orderctl/Coupon.kt:10:28 Initializer type mismatch: expected 'DiscountRule', actual 'Coupon'.นี่คือ nominal typingnominal typingการเข้ากันของชนิดตัดสินด้วยชื่อที่ประกาศไว้ ไม่ใช่ด้วยรูปร่าง · Kotlin อยู่ฝั่งเดียวกับ Java ไม่ใช่ฝั่ง Go และ extension function ก็ไม่ช่วย compiler ตอบว่า `initializer type mismatch` · การเข้ากันของชนิดตัดสินจากชื่อที่ประกาศไว้ ไม่ใช่จากรูปร่างของ method ที่มีอยู่
Kotlin ยืนฝั่งเดียวกับ Java ไม่ใช่ฝั่ง Go
เหตุผลอยู่ในตัวคำว่า extension เอง — มันไม่ได้เป็นสมาชิกของชนิด มันเป็นฟังก์ชันที่ compiler
เลือกให้ตอนแปล แล้ววางเนื้อความไว้ที่จุดเรียก · ตาราง method ของ Coupon ไม่เคยเปลี่ยน
จุดเทียบสามภาษาอยู่ที่บรรทัดเดียวกันนี้
| ภาษา | ชนิดที่มี method ครบแต่ไม่ได้ประกาศความผูกพัน |
|---|---|
| Go | เข้ากับ interface ได้ทันที ไม่มีที่ให้เขียนความผูกพันด้วยซ้ำ |
| Java | ไม่เข้า ต้องพิมพ์ implements |
| Kotlin | ไม่เข้า ต้องพิมพ์เครื่องหมายทวิภาค และ extension function ไม่ช่วย |
บทที่ 5 ของคอร์ส Go ตั้งชื่อหัวข้อแรกไว้ว่า “ไม่มีที่ให้ประกาศว่า implement” ส่วนคอร์ส Java ตั้งว่า “ไม่มีการเข้ากันโดยบังเอิญ” · บทนี้อยู่ข้างหลัง
Kotlin มี fun interface ที่ทำให้ lambda กลายเป็นชนิดนั้นได้เลยผ่าน SAM conversion
· แต่มันมีเงื่อนไขข้อเดียวที่ DiscountRule ผ่านไม่ได้
// ❌ แปลไม่ผ่านpackage orderctl
fun interface Rule2 { fun name(): String
fun applySatang(subtotalSatang: Long): Long}e: file:///…/src/main/kotlin/orderctl/Probe.kt:4:1 Functional interface must have exactly one abstract function.DiscountRule มี method ที่ยังไม่มีเนื้อความสองตัว ประตูนี้จึงปิดตั้งแต่ตอนประกาศ
ทางที่ผ่านคือ adapter
หัวข้อที่มีชื่อว่า “ทางที่ผ่านคือ adapter”เมื่อ extension function พาชนิดเข้า interface ไม่ได้ ทางที่เหลือคือเขียนชนิดใหม่มาห่อมัน
CouponRule ถือ Coupon ไว้เป็น property แล้วประกาศความผูกพันที่ตัวมันเอง
CouponRule.kt ข้างล่างประกาศ Coupon ไว้ด้วย เพื่อให้คัดลอกไปแปลได้ทันที
· ในโปรเจกต์จริงบรรทัดนั้นมาจาก jar ของทีมอื่น และไม่ได้อยู่ในมือเรา
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี DiscountRule อยู่ในโปรเจกต์ด้วยpackage orderctl
class Coupon(val code: String, val offSatang: Long)
class CouponRule(private val coupon: Coupon) : DiscountRule {
override fun name() = "coupon-" + coupon.code
override fun applySatang(subtotalSatang: Long) = maxOf(0L, subtotalSatang - coupon.offSatang)}ราคาที่จ่ายคือชนิดเพิ่มมาหนึ่งตัวและ object เพิ่มมาหนึ่งใบต่อ coupon หนึ่งใบ
· ที่ได้กลับมาคือ CouponRule เข้ากล่อง List<DiscountRule> ได้เหมือนกฎตัวอื่นทุกประการ
extension function ตัดสินตอนแปล ไม่ใช่ตอนรัน
หัวข้อที่มีชื่อว่า “extension function ตัดสินตอนแปล ไม่ใช่ตอนรัน”ตัวไหนถูกเรียกขึ้นกับชนิดที่เขียนไว้ในการประกาศ ไม่ใช่ชนิดของ object ที่อยู่ในนั้นจริง
ประกาศ extension ชื่อเดียวกันสองตัว ตัวหนึ่งบน interface อีกตัวบน class ที่แคบกว่า
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · Labels.kt ต้องมี DiscountRule กับ PercentOff อยู่ข้างกันpackage orderctl
fun DiscountRule.label(): String = "rule:" + name()
fun PercentOff.label(): String = "percent:" + name()// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · เนื้อของ fun main ใน RulesDemo.ktval p = PercentOff(10)println(p.label())
val asRule: DiscountRule = pprintln(asRule.label())
val all: List<DiscountRule> = listOf(p, FlatOff(5_000L))for (r in all) println(r.label())$ gradle -q runpercent:percent-10rule:percent-10rule:percent-10rule:flat-5000บรรทัดแรกกับบรรทัดที่สองถือ object ใบเดียวกัน แล้วให้คำตอบคนละคำ · ต่างกันแค่ชนิดที่เขียนไว้ตอนประกาศตัวแปร
พอค่าเข้าไปอยู่ในกล่อง List<DiscountRule> ชนิดที่ compiler มองเห็นของทุกสมาชิก
คือ DiscountRule เท่านั้น
Rules.chain ที่เราจะเขียนในหัวข้อถัดๆ ไป รับ List<DiscountRule> พอดี
· extension ที่เขียนไว้ให้ PercentOff จึงไม่มีวันถูกเรียกจากในนั้น
และไม่มีคำเตือนสักบรรทัด · -Werror -Wextra ก็ไม่จับ เพราะไม่มีอะไรผิดในสายตาของภาษา
member function ไม่มีปัญหานี้ · name() ที่อยู่ใน interface กระจายแบบ virtual ตามปกติ
ทุกสมาชิกใน list จึงได้ตัวของชนิดจริงเสมอ
by เขียน forwarder ให้ทุกตัวที่เราไม่ได้เขียนทับ
หัวข้อที่มีชื่อว่า “by เขียน forwarder ให้ทุกตัวที่เราไม่ได้เขียนทับ”ประกาศครั้งเดียวแล้ว compiler สร้าง method ส่งต่อให้ครบทั้ง interface
CountingRule ต้องการนับว่ากฎที่มันห่ออยู่ถูกเรียกกี่ครั้ง และเปลี่ยนชื่อให้เห็นว่ามีตัวห่อ
· ส่วนที่เหลือของ interface ไม่ต้องการเปลี่ยนอะไรเลย
นี่คืองานของ class delegationclass delegation`class A(b: B) : B by b` สั่งให้ compiler สร้าง method ส่งต่อทุกตัวที่ไม่ได้เขียนทับ · แต่ตัวที่เขียนทับไม่ถูกเรียกจาก default method ที่ทำงานอยู่บนตัวถูกมอบหมาย เพราะมันไม่เคยรู้จักตัวห่อ
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี DiscountRule อยู่ในโปรเจกต์ด้วยpackage orderctl
class CountingRule(private val inner: DiscountRule) : DiscountRule by inner {
var seen: Int = 0 private set
override fun name() = "counting(" + inner.name() + ")"
override fun applySatang(subtotalSatang: Long): Long { seen++ return inner.applySatang(subtotalSatang) }}: DiscountRule by inner อ่านว่า “เป็น DiscountRule โดยให้ inner ทำแทน”
· method ที่เราไม่ได้เขียนทับ compiler สร้างตัวส่งต่อให้เอง
Go มีของที่ให้ผลคล้ายกันชื่อ embedding แต่มันฝังทั้งชนิดเข้ามาเป็น field ไม่มีชื่อ
· ส่วน by ของ Kotlin ผูกกับ interface ตัวที่ประกาศไว้ตรงบรรทัดนั้นเท่านั้น
รันแล้วเงินถูก แต่ตัวนับค้างที่ 1
หัวข้อที่มีชื่อว่า “รันแล้วเงินถูก แต่ตัวนับค้างที่ 1”เรียก
applySatangหนึ่งครั้งแล้วapplyTwiceอีกหนึ่งครั้ง ตัวนับควรเป็น 3 แต่ไม่ใช่
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · เนื้อของ fun main ใน RulesDemo.ktval counting = CountingRule(PercentOff(10))
println(counting.applySatang(20_000L))println(counting.seen)println(counting.applyTwice(20_000L))println(counting.seen)
println("name(): " + counting.name())println("describe(): " + counting.describe())$ gradle -q run180001162001name(): counting(percent-10)describe(): กฎ percent-10ตัวเลขเงินถูกทั้งสองตัว · 20,000 สตางค์ลบสิบเปอร์เซ็นต์ได้ 18,000 และลบสองรอบได้ 16,200
ผิดอยู่สองที่ และทั้งสองที่เป็น method ที่มีเนื้อความติดมากับ interface
seenค้างที่ 1 ทั้งที่applyTwiceเรียกapplySatangไปอีกสองครั้งdescribe()ตอบกฎ percent-10ทั้งที่name()ของตัวเดียวกันตอบcounting(percent-10)
บรรทัดสองบรรทัดสุดท้ายคือหลักฐานที่ชัดที่สุด · object ใบเดียวกัน ถามสองคำถามที่พึ่ง name()
เหมือนกัน แล้วได้ชื่อคนละชื่อ
forwarder ถือ reference ไปหาตัวข้างใน ไม่ใช่ไปหาตัวเอง
หัวข้อที่มีชื่อว่า “forwarder ถือ reference ไปหาตัวข้างใน ไม่ใช่ไปหาตัวเอง”
describe()ของCountingRuleมีอยู่จริงในรูป bytecode และบรรทัดแรกของมันคือการหยิบinner
javap -c ของ CountingRule ที่เขียน by inner ให้เนื้อของ describe() แบบนี้
public java.lang.String describe(); Code: 0: aload_0 1: getfield #23 // Field inner:Lorderctl/DiscountRule; 4: invokeinterface #56, 1 // InterfaceMethod orderctl/DiscountRule.describe:()Ljava/lang/String; 9: areturnลบคำว่า by inner ทิ้งแล้วแปลใหม่ เนื้อของ method ชื่อเดียวกันเปลี่ยนไปทั้งก้อน
public java.lang.String describe(); Code: 0: aload_0 1: invokespecial #56 // InterfaceMethod orderctl/DiscountRule.describe:()Ljava/lang/String; 4: areturnรูปแรกมีบรรทัดที่รูปหลังไม่มี คือ getfield inner แล้วมันเรียกบนตัวที่หยิบออกมานั้นด้วย
invokeinterface · รูปหลังไม่หยิบอะไรเลย มันเรียกบน this ตรงๆ ด้วย invokespecial
พอเนื้อความปริยายทำงานอยู่บน inner การเรียก name() ข้างในมันจึงไปถึง PercentOff
· inner ไม่เคยรู้ว่ามีใครห่อมันอยู่
flowchart LR CALL["counting.applyTwice(20000)"] --> FWD["CountingRule.applyTwice<br/>forwarder ที่ by สร้าง"] FWD -->|"getfield inner"| DEL["PercentOff.applyTwice<br/>เนื้อความปริยายของ interface"] DEL --> IN["PercentOff.applySatang<br/>คืน 16200"] OVR["CountingRule.applySatang<br/>seen++"] -.-> DEAD["ไม่มีเส้นไหนวิ่งมาถึง<br/>seen ค้างที่ 1"]
คำบรรยายภาพ: เส้นทางจริงเลี้ยวออกจากตัวห่อตั้งแต่ก้าวแรก · ตัวที่เราเขียนทับยืนอยู่นอกเส้นทางทั้งเส้น
ประโยคเดียวที่ควรจำจากบทนี้ — by ไม่สร้างสายการสืบทอด มันสร้าง method ส่งต่อ
บทที่ 5 ของคอร์ส Go สาธิตข้อเดียวกันด้วย
Printer กับ Fancy ที่พิมพ์ <p:base> ไม่ใช่ <p:fancy> · Go เรียกมันว่า embedding
Kotlin เรียกมันว่า by แต่กติกาข้อนี้ตรงกัน
Java ไม่มีของแบบนี้เลย · บทที่ 5 ของคอร์ส Java
ต้องเขียน apply ส่งต่อออกมาเอง ซึ่งแลกความยาวกับการที่ไม่มีอะไรถูกสร้างลับหลัง
สองทางแก้ และแต่ละทางจ่ายคนละอย่าง
หัวข้อที่มีชื่อว่า “สองทางแก้ และแต่ละทางจ่ายคนละอย่าง”ทางแรกเลิกใช้
byทางที่สองย้ายเนื้อความปริยายออกจาก interface
ทางแรก — ลบ by inner ทิ้ง แล้วรับเนื้อความปริยายมาตามปกติ
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · ต้องมี DiscountRule อยู่ในโปรเจกต์ด้วย// แทนที่ฉบับก่อนหน้าในบทนี้package orderctl
class CountingRule(private val inner: DiscountRule) : DiscountRule {
var seen: Int = 0 private set
override fun name() = "counting(" + inner.name() + ")"
override fun applySatang(subtotalSatang: Long): Long { seen++ return inner.applySatang(subtotalSatang) }}$ gradle -q run180001162003name(): counting(percent-10)describe(): กฎ counting(percent-10)ตัวนับเป็น 3 และ describe() ตอบชื่อของตัวห่อแล้ว
· ที่นี่ by ไม่ได้ประหยัดอะไรให้เลย เพราะ method ที่มันสร้างให้คือสองตัวที่มีเนื้อความอยู่แล้ว
ทางที่สอง — ย้าย describe กับ applyTwice ออกไปเป็น extension function
// แทนที่ฉบับก่อนหน้าในบทนี้package orderctl
interface DiscountRule { fun name(): String
fun applySatang(subtotalSatang: Long): Long}
fun DiscountRule.describe(): String = "กฎ " + name()
fun DiscountRule.applyTwice(subtotalSatang: Long): Long = applySatang(applySatang(subtotalSatang))รูปนี้ให้ผลชุดเดียวกับทางแรก คือ 3 กับ กฎ counting(percent-10) ทั้งที่ by inner ยังอยู่ครบ
เหตุผลย้อนกลับไปที่หัวข้อเรื่อง extension · การเลือกว่า extension ตัวไหนถูกเรียก
ตัดสินตอนแปล แต่ name() ที่อยู่ ข้างใน เนื้อความของมัน เป็นการเรียก member ตามปกติ
ซึ่งกระจายตอนรัน
ราคาของทางที่สองคือไม่มีใครเขียนทับ describe() ได้อีกเลย · extension เขียนทับไม่ได้ตามนิยาม
ใช้ by เมื่อสิ่งที่คุณเขียนทับ ไม่มี method ตัวอื่นใน interface เรียกใช้อยู่
ถ้ามีเนื้อความปริยายใน interface ที่พึ่ง method ซึ่งตัวห่อจะเขียนทับ ให้เขียน forwarder เอง · ยาวกว่า แต่เส้นทางที่ได้คือเส้นทางที่คุณอ่านเห็นด้วยตา
List ของ Kotlin ยอมให้ส่งชนิดที่แคบกว่าเข้ามา
หัวข้อที่มีชื่อว่า “List ของ Kotlin ยอมให้ส่งชนิดที่แคบกว่าเข้ามา”
List<PercentOff>ส่งเข้าที่ที่ต้องการList<DiscountRule>ได้เลย ไม่ต้องเขียนอะไรที่ลายเซ็น
Rules เป็นที่รวมของการต่อกฎ · chain รับ list แล้วพับเข้าด้วยกันเป็นกฎตัวเดียว
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · Rules.kt ต้องมี DiscountRule อยู่ในโปรเจกต์ด้วยpackage orderctl
private class Chained( private val first: DiscountRule, private val second: DiscountRule,) : DiscountRule {
override fun name() = first.name() + "+" + second.name()
override fun applySatang(subtotalSatang: Long) = second.applySatang(first.applySatang(subtotalSatang))}
object Rules {
val none: DiscountRule = object : DiscountRule { override fun name() = "none" override fun applySatang(subtotalSatang: Long) = subtotalSatang }
fun chain(rules: List<DiscountRule>): DiscountRule = if (rules.isEmpty()) none else rules.reduce { acc, r -> Chained(acc, r) }}List ของ Kotlin ประกาศตัวชนิดไว้เป็น out การส่ง List<PercentOff> เข้า chain จึงผ่าน
โดยไม่ต้องแตะลายเซ็น · Java ต้องเขียน List<? extends DiscountRule> และ Go ต้องวนคัดลอกทีละตัว
| ภาษา | ส่ง list ของชนิดลูกเข้าที่ที่ต้องการ list ของชนิดแม่ |
|---|---|
| Go | ทำไม่ได้ ต้องสร้าง slice ใหม่แล้วคัดลอกทีละตัว |
| Java | ได้ ถ้าผู้เขียนลายเซ็นใส่ ? extends ไว้ล่วงหน้า |
| Kotlin | ได้ทันที เพราะ List<out E> ประกาศไว้ที่ตัว interface เอง |
ราคาที่จ่ายอยู่ที่อีกฝั่ง · MutableList เขียนได้จึงเป็น invariant และปิดประตูเดียวกันนี้
// ❌ แปลไม่ผ่านpackage orderctl
fun total(rules: MutableList<DiscountRule>): Long = Rules.chain(rules).applySatang(20_000L)
val offers: MutableList<PercentOff> = mutableListOf(PercentOff(10))
val sum: Long = total(offers)e: file:///…/src/main/kotlin/orderctl/Probe.kt:8:23 Argument type mismatch: actual type is 'MutableList<PercentOff>', but 'MutableList<DiscountRule>' was expected.เส้นแบ่งจึงไม่ใช่ “Kotlin ใจกว้างกว่า” แต่เป็น “ชนิดที่อ่านอย่างเดียวยอมได้ ชนิดที่เขียนได้ยอมไม่ได้”
ประกอบร่างแล้วรันจริง
หัวข้อที่มีชื่อว่า “ประกอบร่างแล้วรันจริง”กฎสามตัวจากสามชนิด ต่อเป็นสายเดียว แล้วห่อด้วยตัวนับที่แก้แล้ว
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์ · RulesDemo.kt ต้องมีกฎทุกตัวของบทนี้อยู่ในโปรเจกต์ด้วยpackage orderctl
fun main() { val offers: List<PercentOff> = listOf(PercentOff(10), PercentOff(5)) println(Rules.chain(offers).applySatang(20_000L))
val mixed = listOf(PercentOff(10), FlatOff(2_000L), CouponRule(Coupon("SONGKRAN", 3_000L))) val counting = CountingRule(Rules.chain(mixed))
println(counting.name()) println(counting.applySatang(20_000L)) println(counting.applyTwice(20_000L)) println(counting.seen) println(counting.describe())}$ gradle -q run17100counting(percent-10+flat-2000+coupon-SONGKRAN)1300067003กฎ counting(percent-10+flat-2000+coupon-SONGKRAN)บรรทัดแรกมาจาก List<PercentOff> ที่ส่งเข้า chain ตรงๆ · 20,000 ลบสิบเปอร์เซ็นต์แล้วลบห้า
เปอร์เซ็นต์ได้ 17,100
บรรทัดที่สามคือกฎสามตัวเรียงกัน · ลบสิบเปอร์เซ็นต์เหลือ 18,000 ลบ 2,000 เหลือ 16,000 ลบอีก 3,000 เหลือ 13,000
บรรทัดที่ห้าเป็นตัวเลขที่บอกว่าเราแก้ถูกแล้ว · seen เป็น 3 เพราะเรียก applySatang หนึ่งครั้ง
และ applyTwice พาไปอีกสองครั้ง
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”
DiscountRuleต่อกฎเพิ่มได้โดยไม่แตะของเดิม และคุณเห็นแล้วว่าbyซื้ออะไรและขายอะไร
สามข้อที่บทนี้พิสูจน์ด้วยการรัน
- extension function พาชนิดเข้า interface ไม่ได้ compiler ตอบ
Initializer type mismatch - extension function ตัดสินจากชนิดที่ประกาศ สมาชิกใน
List<DiscountRule>จึงได้ตัวของ interface เสมอ byสร้าง forwarder ที่ทำงานอยู่บนตัวข้างใน ตัวที่เขียนทับจึงอยู่นอกเส้นทางของเนื้อความปริยาย
บทหน้าเป็นเรื่องความผิดพลาด · จุดที่คนมาจาก Java สะดุดที่สุดคือ Kotlin ไม่มี checked exception เลยสักตัว
ตรงที่ javac ตอบ unreported exception IOException; must be caught or declared to be thrown
kotlinc เงียบสนิท แม้เปิด -Werror -Wextra
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Kotlin — Delegation (เข้าถึง 2026-08-28) —
byและข้อจำกัดว่าสมาชิกที่เขียนทับไม่ถูกเรียกจากสมาชิกของ delegate - Kotlin — Extensions (เข้าถึง 2026-08-28) — การเลือก extension ตัดสินตอนแปลจากชนิดที่ประกาศ
- Kotlin — Fun interfaces (SAM) (เข้าถึง 2026-08-28) — เงื่อนไข abstract function ตัวเดียว
- Kotlin — Generics: variance (เข้าถึง 2026-08-28) — กติกาของ
outและinที่ประกาศไว้ที่ตัว interface - Kotlin —
kotlin.collections.List(เข้าถึง 2026-08-28) — ลายเซ็นinterface List<out E> : Collection<E>ที่หัวข้อสุดท้ายอ้าง - ผลลัพธ์ทุกบรรทัดในบทนี้รันเองเมื่อ 2026-08-28 บน Kotlin 2.4.10 กับ JDK 25 และ Gradle 9.7.1
- ข้อความของ compiler ทุกบรรทัดยกจาก
gradle -q runของโปรเจกต์เดียวกัน · จุดสามจุดในfile:///…คือ path เต็มของเครื่องที่รัน
เช็กความเข้าใจ — บทที่ 5
ข้อ 1 / 3`CountingRule(PercentOff(10))` ที่เขียน `: DiscountRule by inner` แล้วเขียนทับ `applySatang` ไว้ · เรียก `applyTwice(20000)` แล้วทำไม `seen` ถึงไม่ขยับ