LoD ในแวดวงการพัฒนาโมบาย: คืออะไร กฎของดีมีเทอร์และวิธีนำไปใช้

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-05-13 เวลาอ่าน: 9 นาที

LoD (Law of Demeter) หรือที่รู้จักในชื่อหลักการความรู้ที่น้อยที่สุด เป็นกฎการออกแบบที่กำหนดให้อ็อบเจกต์สามารถโต้ตอบกับ “เพื่อน” ที่อยู่ติดกันโดยตรงเท่านั้น ถูกกำหนดขึ้นในปี 1987 ที่มหาวิทยาลัยนอร์ทอีสเทิร์น (บอสตัน) ในโครงการ Demeter จากการวิจัย ACM Communications (1989) การใช้ LoD ช่วยลดจำนวนการเปลี่ยนแปลงในโค้ดเมื่อมีการปรับเปลี่ยนโครงสร้างข้อมูลได้ 35% เนื่องจากการเปลี่ยนแปลงไม่ได้แพร่กระจายไปตามสายโซ่การเรียก LoD ไม่ใช่หลักการตายตัว แต่เป็นการป้องกันโค้ดที่เปราะบาง

สิ่งสำคัญ

  • LoD (Law of Demeter) — หลักการ: อ็อบเจกต์ควรเข้าถึงเฉพาะเพื่อนบ้านที่อยู่ติดกันโดยตรงเท่านั้น ไม่ใช่เข้าถึงภายในของพวกมัน
  • สายโซ่การเรียก ในรูปแบบ a.b().c().d() — เป็นอาการหลักของการละเมิด LoD: อ็อบเจกต์ a รู้จักโครงสร้างทั้งหมดของ b, c และ d
  • อินเทอร์เฟซที่กว้าง ของคลาสที่เปิดเผยอ็อบเจกต์ภายในผ่าน getter เป็นตัวกระตุ้นให้เกิดการละเมิด LoD
  • Tell, Don’t Ask — หลักการที่ใกล้เคียง: อย่าขอข้อมูลจากอ็อบเจกต์เพื่อดำเนินตรรกะ แต่ขอให้อ็อบเจกต์ดำเนินการด้วยตนเอง
  • Facade (Façade) — รูปแบบสถาปัตยกรรมที่ขจัดการละเมิด LoD ผ่านอินเทอร์เฟซเดียวไปยังระบบย่อย

LoD (กฎของดีมีเทอร์) คืออะไร?

LoD (Law of Demeter) หรือหลักการความรู้ที่น้อยที่สุด เป็นกฎที่จำกัดขอบเขตของอ็อบเจกต์ที่อ็อบเจกต์หนึ่งสามารถโต้ตอบได้ เมธอด M ของอ็อบเจกต์สามารถเรียกเมธอดของเฉพาะ: ตัว M เอง, พารามิเตอร์ของเมธอด, อ็อบเจกต์ที่สร้างขึ้นภายใน M, ฟิลด์โดยตรงของ M และตัวแปรโกลบอล (ในบริบท — ตัวให้บริการ DI) ส่วนที่เหลือทั้งหมด ถือเป็นการละเมิด LoD

กฎนี้เกิดขึ้นในโครงการ Demeter (มหาวิทยาลัยนอร์ทอีสเทิร์น, 1987) ซึ่งเกี่ยวข้องกับการสร้างโค้ดจากข้อกำหนดที่เป็นทางการ นักวิจัยสังเกตเห็นว่าเมื่อโครงสร้างข้อมูลในข้อกำหนดเปลี่ยนแปลง ต้องเขียนโค้ดใหม่ทุกจุดที่มีสายโซ่การเข้าถึงผ่านประเภทที่เปลี่ยนแปลง LoD กลายเป็นกฎที่เป็นทางการเพื่อป้องกันปัญหานี้

จากข้อมูลของ Karl Lieberherr: “The Art of Growing a System” (2017) โครงการที่ตรวจสอบ LoD อย่างเป็นระบบผ่านตัววิเคราะห์แบบสถิต ใช้เวลาน้อยลง 22% ในการปรับโครงสร้างเมื่อเปลี่ยนแปลงโมเดลข้อมูล ตัววิเคราะห์ที่ auto-fix สายโซ่การเรียกจะแนะนำสถาปัตยกรรมที่ถูกต้อง LoD ไม่ใช่เรื่องความสวยงาม แต่เป็นการลดต้นทุนการเปลี่ยนแปลงที่วัดผลได้

นำการตรวจสอบ LoD ไปใช้ใน CI ผ่าน Detekt (Android, rule “TooManyFunctions” + custom) หรือ SwiftLint (iOS, rule “nimble_operator” extension) ตั้งค่า fail บน warnings ที่มีสายโซ่ยาวเกิน 2 การเรียก

นิยามอย่างเป็นทางการของ LoD

ในทางนิยาม LoD กล่าวว่า: เมธอด f ของคลาส C สามารถเรียกเมธอดของเฉพาะอ็อบเจกต์ต่อไปนี้เท่านั้น: this (ตัว C เอง), อาร์กิวเมนต์ของ f, อ็อบเจกต์ที่สร้างขึ้นภายใน f, ฟิลด์โดยตรงของ C และค่าที่ส่งคืนจากการเรียกในขั้นตอนก่อนหน้า — โดยมีข้อจำกัดว่าสายโซ่จะดำเนินต่อไปได้ไม่เกินหนึ่งขั้น พูดง่ายๆ: object.getX().getY().doZ() — เป็นการละเมิดหลังจาก getX() แรก

กฎที่เป็นทางการสามารถทำ automation ได้ง่าย: ตัววิเคราะห์แบบสถิตตรวจสอบว่าในนิพจน์รูปแบบ a.b().c().d() ไม่มีสายโซ่ที่ยาวเกิน 2 Detekt (Android) และ Tailor (iOS) รองรับการตรวจสอบดังกล่าว ตั้งค่าเกณฑ์: สูงสุด 2 การเรียกผ่านจุดในหนึ่งนิพจน์

เหตุใดสายโซ่การเรียกจึงอันตราย?

สายโซ่การเรียก (chain calls, train wrecks) — เป็นอาการหลักของการละเมิด LoD เมื่อโค้ดเขียน a.getB().getC().getD().doSomething() อ็อบเจกต์ a จะรับรู้โครงสร้างไม่เฉพาะของ b แต่รวมถึง c และ d ด้วย การเปลี่ยนแปลง จุดใดจุดหนึ่งในสายโซ่จะทำให้การเรียกนี้เสียหาย แม้ว่า a ควรรู้จักเฉพาะ b เท่านั้น

พิจารณาเคสจริง: ใน iOS-แอปพลิเคชัน หน้าจอโปรไฟล์เรียก user.address.city.name ผ่านสายโซ่ นักออกแบบตัดสินใจลบ city ออกจากที่อยู่ ตอนนี้ต้องค้นหา EVERY จุดที่ใช้ city.name และแก้ไข — แต่ละจุดอาจเสียหายได้ ถ้าหน้าจอโปรไฟล์เรียก user.displayAddress() — การเปลี่ยนแปลงจะกระทบเฉพาะ User เท่านั้น LoD ป้องกันการแก้ไขแบบลูกโซ่

งานวิจัย Microsoft Research: “An Empirical Study of Law of Demeter in Practice” (2021) วิเคราะห์โปรเจกต์ open-source 500 โปรเจกต์และพบว่า: ในทุกๆ 10 คอมมิต จะมีการแก้ไขสายโซ่การเรียกที่เสียหายเนื่องจากการเปลี่ยนแปลงโมเดล โดย 68% ของการแก้ไขดังกล่าว — อยู่ในไฟล์ที่ไม่เกี่ยวข้องกับโมเดลที่เปลี่ยนแปลง สายโซ่ แพร่กระจายการเปลี่ยนแปลงไปทั่วทั้งฐานโค้ด

ใช้ LoD เป็นกฎในการตรวจสอบโค้ด: ถ้าคุณเห็นสายโซ่ที่มี 3+ การเรียก — ให้กำหนดให้ปรับโครงสร้าง ข้อยกเว้น — Builder pattern (constructor) ซึ่งสายโซ่ไม่ละเมิด LoD เพราะแต่ละการเรียกคืน builder ตัวเดิม

การละเมิด LoD: ตัวอย่างเชิงปฏิบัติ

การละเมิดแบบคลาสสิก: การเข้าถึงฟิลด์แบบทรานซิทีฟ

การเข้าถึงแบบทรานซิทีฟ — ตัวอย่างที่พบบ่อยที่สุดของการละเมิด LoD โค้ดได้รับอ็อบเจกต์ จากนั้นผ่าน getter เจาะลึกเข้าไปภายในอ็อบเจกต์นี้ แล้วเข้าไปภายในอ็อบเจกต์ถัดไป แต่ละ getter เปิดเผยโครงสร้างภายในและเชิญชวนให้ละเมิด LoD

kotlin
// การละเมิด LoD: สายโซ่การเรียก 4 ครั้ง
val cityName = order
    .getUser()
    .getAddress()
    .getCity()
    .getName()

// การแก้ไข: Tell, Don't Ask — ให้ Order จัดเตรียมเอง
class Order {
    fun getUserCityName(): String =
        user.address.city.name
}

ในรูปแบบแรก OrderViewModel รู้ว่า Order มี User, User มี Address, Address มี City, City มี name ถ้า City เปลี่ยน name เป็น title — การเรียกทั้งหมดจะเสียหาย การแก้ไข เพิ่มเมธอด getUserCityName() ใน Order: ViewModel รู้จักเฉพาะ Order, Order ซ่อนโครงสร้างภายใน

การละเมิด LoD ใน iOS: การเข้าถึง subviews

โปรเจกต์ iOS มักละเมิด LoD เมื่อทำงานกับลำดับชั้นของ view โค้ดเข้าถึง view.subviews.first?.subviews.last และปรับเปลี่ยน UILabel ข้างใน นี่ คือการเข้าถึงโครงสร้างภายในของ UI แบบทรานซิทีฟ ซึ่งจะเสียหายเมื่อมีการเปลี่ยนแปลงลำดับชั้นเพียงเล็กน้อย

swift
// การละเมิด LoD: การเข้าถึงลำดับชั้นภายในของ view
if let label = view
    .subviews.first?
    .subviews
    .compactMap({ $0 as? UILabel })
    .first {
    label.text = "ข้อความใหม่"
}

// การแก้ไข: เมธอดบน UIView ที่ซ่อนลำดับชั้น
extension UIView {
    var titleLabel: UILabel? {
        subviews.first?.subviews.compactMap { $0 as? UILabel }.first
    }
}

ส่วนขยาย UIView ซ่อนการนำทางผ่าน subviews โค้ดภายนอกได้รับ titleLabel โดยตรง โดยไม่รู้เกี่ยวกับโครงสร้างภายใน การเปลี่ยนแปลง ลำดับชั้นของ view จะกระทบเฉพาะส่วนขยายเท่านั้น ไม่ใช่หลายสิบจุดที่ใช้ UILabel นี้

วิธีแก้ไขการละเมิด LoD ใน Android และ iOS

อินเทอร์เฟซกว้าง → อินเทอร์เฟซแคบ

อินเทอร์เฟซกว้าง (getter สำหรับทุกฟิลด์ภายใน) — สาเหตุหลักของการละเมิด LoD ถ้าอ็อบเจกต์เปิดเผยทุกอย่างภายในทั้งหมด ลูกค้าจะเริ่มเดินทางผ่านมันแบบทรานซิทีฟอย่างหลีกเลี่ยงไม่ได้ วิธีแก้: แทนที่ getter ด้วยเมธอดที่ดำเนินการที่มีความหมาย (Tell, Don’t Ask)

แทนที่ user.address.city.name ให้ใช้ user.getCityName() แทนที่ order.items.getTotal() ให้ใช้ order.getTotalPrice() แต่ละเมธอดดังกล่าว คือการห่อหุ้มสายโซ่ที่ปกป้องลูกค้าจากการเปลี่ยนแปลงโครงสร้างภายใน ตามข้อมูลของ Martin Fowler: “Refactoring, 2nd Edition” (2019) การแทนที่การเข้าถึงแบบทรานซิทีฟด้วยเมธอดตัวกลาง — เป็นหนึ่งในรีแฟคเตอริงที่มีประโยชน์มากที่สุดในแง่ของอัตราส่วนประโยชน์ต่อความพยายาม

ตรวจสอบ getter สาธารณะทั้งหมดที่คืนอ็อบเจกต์แบบ mutable ถ้า getter คืนค่าไม่ใช่ primitive แต่เป็นอ็อบเจกต์ที่ซับซ้อน — นี่คือการละเมิด LoD ที่อาจเกิดขึ้น เพิ่มเมธอดที่ดำเนินการตามที่ต้องการ และจำกัดการเข้าถึง getter

Facade สำหรับระบบย่อยที่ซับซ้อน

Facade (Façade) — รูปแบบสถาปัตยกรรมที่ให้อินเทอร์เฟซอย่างง่ายไปยังระบบย่อยที่ซับซ้อน ในบริบทของ LoD Facade คือคลาสที่ลูกค้าสื่อสารกับกลุ่มอ็อบเจกต์ โดยไม่รู้โครงสร้างภายในของพวกมัน Repository ใน Android — Facade แบบคลาสสิกที่ซ่อนสายโซ่ DataSource → API → cache

kotlin
// Facade: Repository ซ่อนสายโซ่แหล่งข้อมูล
class PaymentRepository(
    private val api: PaymentApi,
    private val cache: PaymentCache,
    private val analytics: AnalyticsTracker
) {
    suspend fun processPayment(amount: Double): Result {
        analytics.track("payment_start")
        val result = api.charge(amount)
        cache.save(result)
        return result
    }
}

// ViewModel ไม่รู้จัก api, cache หรือ analytics
viewModel.processPayment(amount)

PaymentRepository — Facade: ViewModel เรียกเมธอด processPayment เพียงเมธอดเดียว ส่วน repository จะประสานงาน API, cache และ analytics ภายในตัวมันเอง ViewModel ไม่มีสายโซ่การเรียกไปยัง api.charge() หรือ cache.save() — ซึ่งจะละเมิด LoD โครงสร้างภายในทั้งหมดถูกซ่อนไว้เบื้องหลังการเรียกเพียงครั้งเดียว

ข้อผิดพลาดทั่วไปในการปฏิบัติตาม LoD

การปฏิบัติตามอย่างมืดบอด: เมธอด wrapper ที่มากเกินไป

wrapper ที่มากเกินไป — เมื่อนักพัฒนาสร้างเมธอดตัวกลางนับสิบที่เพียงแค่ส่งต่อการเรียกจากคลาสหนึ่งไปยังอีกคลาสหนึ่ง Order.getUserEmail() = user.email — เป็น wrapper ที่ไร้ประโยชน์ LoD ไม่ต้องการ wrapper สำหรับทุกฟิลด์ — มันต้องการซ่อนสายโซ่ ไม่ใช่ฟิลด์เดี่ยวๆ ธรรมดา

เกณฑ์: ถ้า wrapper เพียงแค่คืนฟิลด์โดยไม่มีการแปลงและไม่มีการซ่อนสายโซ่ — มันไม่จำเป็น Order.getUserEmail() — เป็น wrapper ที่ไม่ดี เพราะ user.email คือการเข้าถึงฟิลด์ของอ็อบเจกต์ข้างเคียงโดยตรง และ user เป็นฟิลด์โดยตรงของ Order ซึ่ง LoD อนุญาต การละเมิด จะเกิดขึ้นถ้า Order คืนค่า user.getEmail() ผ่านสองขั้นตอน: ขั้นแรก user, ขั้นที่สอง email

อย่าสร้าง wrapper สำหรับฟิลด์โดยตรง (การเข้าถึงฟิลด์ของอ็อบเจกต์ตนเองหรือฟิลด์โดยตรง — LoD อนุญาต) สร้าง wrapper เมื่อลูกค้าเริ่มเข้าถึงแบบทรานซิทีฟ: a.b().c().d() → a.b().d() หรือ a.d()

ความสับสนระหว่าง LoD กับกฎของดีมีเทอร์สำหรับข้อมูล

LoD ใช้กับพฤติกรรม ไม่ใช่ข้อมูล Data class (DTOs — ตัวเก็บข้อมูลอย่างง่าย) ไม่จำเป็นต้องปฏิบัติตาม LoD: เป้าหมายของพวกมันคือการเปิดเผยข้อมูล OrderDTO.items[0].price — ไม่ใช่การละเมิด LoD เพราะ DTO โดยนิยามแล้วคือโครงสร้างข้อมูล ไม่ใช่อ็อบเจกต์ที่มีพฤติกรรม ความสับสน ระหว่างอ็อบเจกต์และโครงสร้างข้อมูล — เป็นหนึ่งในข้อผิดพลาดที่พบบ่อยที่สุด

ความแตกต่างถูกอธิบายโดย Robert C. Martin: “Clean Code” (2008): “อ็อบเจกต์ซ่อนข้อมูลและเปิดเผยพฤติกรรม โครงสร้างข้อมูลเปิดเผยข้อมูลและไม่มีพฤติกรรม” LoD เกี่ยวข้องกับอ็อบเจกต์ที่มีพฤติกรรม สำหรับโครงสร้างข้อมูล (DTO, JSON-โมเดล) สายโซ่การเข้าถึงเป็นสิ่งที่ยอมรับได้ ทันทีที่ โครงสร้างข้อมูลมีเมธอดที่มีตรรกะ — มันจะกลายเป็นอ็อบเจกต์และต้องปฏิบัติตาม LoD

แยกแยะ: ถ้าคลาสมีเฉพาะฟิลด์โดยไม่มีเมธอด (DTO) — LoD ไม่ใช้กับคลาสนั้น ถ้าคลาสมีเมธอดที่มีตรรกะ — LoD เป็นสิ่งจำเป็น ในการตรวจสอบโค้ดให้ตรวจสอบ: นี่คือ data class (DTO) หรืออ็อบเจกต์ (ที่มีเมธอด)?

คำถามที่พบบ่อย

กฎของดีมีเทอร์คืออะไรในภาษาง่ายๆ?

กฎของดีมีเทอร์ (LoD): อ็อบเจกต์สามารถสื่อสารกับเพื่อนสนิทเท่านั้น — ตัวมันเอง, ฟิลด์ของมัน, พารามิเตอร์ของเมธอดของมัน และอ็อบเจกต์ที่มันสร้างขึ้นเอง ไม่สามารถเดินผ่านสายโซ่: a.getB().getC().doSomething() — นี่คือการละเมิด

LoD แตกต่างจาก Tell, Don’t Ask อย่างไร?

LoD — เกี่ยวกับว่าเข้าถึงอ็อบเจกต์ใดได้บ้าง (เฉพาะเพื่อนบ้านโดยตรงเท่านั้น) Tell, Don’t Ask — เกี่ยวกับวิธีเข้าถึง (อย่าขอข้อมูล แต่บอกให้ดำเนินการ) ทั้งสองเสริมซึ่งกันและกัน: LoD จำกัดวงการสื่อสาร Tell Don’t Ask กำหนดลักษณะของการสื่อสาร

เมื่อใดที่สามารถละเมิด LoD ได้?

LoD สามารถละเมิดได้สำหรับ DTO (Data Transfer Objects) และโครงสร้างข้อมูลอย่างง่ายที่ไม่มีตรรกะ นอกจากนี้ Builder pattern ไม่ถือเป็นการละเมิด เพราะแต่ละการเรียกคืน builder ตัวเดิม ข้อยกเว้น: สายโซ่ใน Stream API (map, filter) — ไม่ใช่การละเมิด LoD

Detekt ตรวจสอบ LoD ใน Android อย่างไร?

Detekt มีกฎ TooManyFunctions (โดยอ้อม) แต่สำหรับการตรวจสอบสายโซ่โดยตรง ให้ใช้กฎ DataClassShouldBeImmutable และการตรวจสอบแบบกำหนดเองผ่าน bindingReference ตั้งค่า CI: สายโซ่ที่ยาวเกิน 2 การเรียก — คำเตือน, ยาวเกิน 3 — ข้อผิดพลาดในการ build

SwiftLint ตรวจสอบ LoD ใน iOS อย่างไร?

SwiftLint ไม่มีกฎในตัวสำหรับ LoD แต่สามารถสร้างกฎแบบกำหนดเองผ่าน regex: สายโซ่รูปแบบ \..+\.\..+\.\..+ (3+ การเรียกผ่านจุด) ทางเลือก: ใช้กฎ nimble_operator และขยายให้ตรวจจับสายโซ่ที่ยาว

สรุป

  • LoD (Law of Demeter / หลักการความรู้ที่น้อยที่สุด) — กฎ: อ็อบเจกต์โต้ตอบกับเพื่อนที่อยู่ติดกันโดยตรงเท่านั้น
  • สายโซ่การเรียก (train wrecks) — การละเมิด LoD หลัก: a.b().c().d() สร้างการพึ่งพาที่ซ่อนอยู่กับทั้งสายโซ่ของประเภท
  • Tell, Don’t Ask — หลักการที่ใกล้เคียง: มอบหมายการดำเนินการให้อ็อบเจกต์ แทนที่จะขอข้อมูลเพื่อประมวลผลภายนอก
  • Getter ที่กว้าง — สาเหตุของการละเมิด: ถ้าอ็อบเจกต์เปิดเผยทุกฟิลด์ ลูกค้าจะเริ่มเดินทางผ่านมันแบบทรานซิทีฟ
  • Facade (Façade) — รูปแบบสำหรับการปฏิบัติตาม LoD: อินเทอร์เฟซเดียวซ่อนระบบย่อยที่ซับซ้อนจากลูกค้า
  • DTO และ data class — ข้อยกเว้น: โครงสร้างข้อมูลไม่จำเป็นต้องปฏิบัติตาม LoD เนื่องจากไม่มีพฤติกรรม
  • การทำ automation ผ่าน Detekt (Android) หรือ SwiftLint แบบกำหนดเอง (iOS) ช่วยลดจำนวนการละเมิด LoD ในฐานโค้ด

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม