DEV Community

Nokka
Nokka

Posted on AI-assisted

LEGO Architecture ใน Flutter วิธีจัดโค้ดให้ต่อได้เหมือนตัวต่อ

LEGO Architecture ใน Flutter วิธีจัดโค้ดให้ต่อได้เหมือนตัวต่อ

โดย Nokka (นก-กา) | 15 กันยายน 2026

บทความนี้เขียนโดย AI (โมเดล deepseek-v4.1-flash ของผู้ให้บริการ ollama-cloud) ผ่าน Hermes Agent จาก Nous Research ตรวจสอบและเรียบเรียงโดย Nokka

เคยมีไฟล์ในโปรเจกต์ที่ทั้งทีมแอบกลัวที่จะเปิดไหมครับ

มันดึงข้อมูล จัดรูปแบบ ตรวจสอบ และวาดหน้าจอ ทั้งหมดอยู่ใน build() เดียว

และมันทำงานได้ จึงไม่มีใครอยากแตะ แต่พอต้องแก้ขั้นตอนชำระเงิน ต้องเลื่อนผ่านสามเรื่องที่ไม่เกี่ยวข้องกัน เพื่อหาเส้นเดียวที่ต้องแก้ [1]

Atuoha Anthony เขียนคัมภีร์ฉบับเต็มความยาวหลายหมื่นตัวอักษรบน freeCodeCamp เมื่อวันที่ 11 กันยายน 2026 เพื่ออธิบายว่าทำไมเรื่องนี้ถึงเกิด และแก้อย่างไร [1]

ตัวต่อหนึ่งชิ้นมีสองส่วนที่ควรสนใจ

Atuoha เปิดด้วยภาพที่ทุกคนคุ้น คือการกดตัวต่อสองชิ้นเข้าด้วยกัน [1]

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

นั่นคือทั้งหมดของแนวคิดนี้ [1]

ตัวต่อมีสองส่วนที่ควรค่าแก่การสังเกต

  • สิ่งที่ตัวต่อเป็น: รูปร่าง สี จุดประสงค์
  • ปุ่มของมัน: จุดเชื่อมต่อมาตรฐานที่ทำให้ต่อกับชิ้นอื่นได้

ในโค้ด ตัวต่อคือหน่วยของแอป ซึ่งอาจเป็น widget, class, service หรือทั้งฟีเจอร์ และปุ่มคือสัญญาที่ตัวต่อเปิดออกให้โลกภายนอก ซึ่งมักเป็น abstract class, interface หรือ signature ของฟังก์ชัน [1]

"การต่อตัวต่อสองชิ้นในโค้ด หมายความว่าส่วนหนึ่งของแอปพึ่งพาอีกส่วนผ่านสัญญานั้นเท่านั้น ไม่ใช่เข้าไปหยิบใช้วิธีที่อีกส่วนสร้างไว้ข้างใน"

เริ่มจาก Padding ที่คุณเขียนอยู่แล้ว

ส่วนที่ผมชอบที่สุดของคัมภีร์นี้คือ มันเริ่มจากสิ่งที่คนอ่านทำอยู่แล้วทุกวัน [1]

Padding(
  padding: const EdgeInsets.all(8),
  child: const Text('Hello'),
)
Enter fullscreen mode Exit fullscreen mode

Padding ทำแค่เรื่องเดียว และไม่สนใจเลยว่าคุณส่งอะไรมาเป็น child จะเป็น Text, Image, Column หรืออะไรก็ได้ [1]

"Padding คือตัวต่อ และพารามิเตอร์ child ของมันคือปุ่ม เพราะ widget ใดก็ตามที่ผ่านประตูนั้นเข้าไปได้ ยินดีต้อนรับ คุณไม่เคยสอน Padding ว่าจะวาดข้อความหรือรูปอย่างไร และมันก็ไม่จำเป็นต้องรู้"

แล้ว Atuoha ก็แสดงฝั่งตรงข้าม ด้วย widget เล็ก ๆ ที่ดูไม่ผิดอะไร [1]

class PriceTag extends StatelessWidget {
  final double price;
  const PriceTag({super.key, required this.price});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(6),
      ),
      child: Text('\$${price.toStringAsFixed(2)}'),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

มันตัดสินใจสองเรื่องที่ไม่เกี่ยวกันพร้อมกันในคลาสเดียว คือหน้าตาของกล่อง กับตัวเนื้อหา [1]

พอคุณอยากได้กล่องแบบเดียวกันรอบข้อความ "Sale" คุณติดทันที ต้องก็อป Container กับ decoration ไปไว้ใน widget ใหม่ หรือใช้ extends สร้างลำดับชั้นคลาสขึ้นมาใหม่แค่เพื่อใช้ styling หกบรรทัด [1]

วิธีแก้คือสิ่งที่ Padding สาธิตให้ดูแล้ว ดึงกล่องออกมาเป็นของตัวเอง แล้วให้มันรับ child อะไรก็ได้ [1]

class SurfaceCard extends StatelessWidget {
  final Widget child;
  const SurfaceCard({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(6),
      ),
      child: child,
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

SurfaceCard รู้แค่เรื่องเดียว คือทำตัวเป็นกล่องมุมโค้งมีเงา และมันมีปุ่มเดียวคือ child ซึ่งรูปร่างเหมือนปุ่มของ Padding เป๊ะ [1]

และนี่คือบททดสอบว่าตัวต่อสร้างดีจริงไหม

"คุณใช้มันซ้ำในที่ใหม่ได้โดยไม่ต้องก็อปสักบรรทัดไหม ถ้าได้ ปุ่มของมันกำลังทำหน้าที่"

ปุ่มที่แท้จริง: สัญญาแทนการพึ่งพาของจริง

การประกอบ widget ให้ใช้ซ้ำได้นั้นยังไม่พอ ต้องสลับพฤติกรรมได้ด้วย [1]

Atuoha ยกตัวอย่างปุ่มเพิ่มสินค้าลงตะกร้า ที่ไม่ควรรู้ว่าข้างหลังเป็นการเรียก REST API เขียนลงเครื่อง หรือแค่พิมพ์ออกคอนโซล [1]

abstract class CartWriter {
  Future<void> add(String productId);
}
Enter fullscreen mode Exit fullscreen mode

abstract class CartWriter คือปุ่มของเรื่องนี้ มันประกาศความสามารถเดียว และไม่พูดอะไรเลยว่าทำงานอย่างไร [1]

"คุณเขียนเทสต์ที่ส่ง InMemoryCartWriter เข้าไป แล้วยืนยันว่า cartWriter.items มีสินค้าที่ถูกต้องได้ โดยไม่ต้องมี mocking framework และไม่ต้องมี network stub"

และนี่คือส่วนที่ Atuoha บอกว่าควรหยุดคิดสักครู่ เพราะมันคือผลตอบแทนจริงของการมีสัญญา [1]

จัดโฟลเดอร์แบบเดียวกัน

พอรับได้ว่าคลาสควรต่อกันผ่านสัญญา ตรรกะเดียวกันก็ใช้กับการจัดโฟลเดอร์ [1]

ความผิดพลาดที่พบบ่อยคือจัดตามชนิด โฟลเดอร์ screens, widgets, services วางเรียงข้างกัน ดูเรียบร้อย แต่มันตรงข้ามกับแนวคิดนี้ [1]

"การจะเข้าใจหรือแก้ฟีเจอร์ตะกร้า คุณต้องกระโดดไปมาระหว่างสามโฟลเดอร์ที่ไม่เกี่ยวข้องกัน และไม่มีอะไรห้ามไฟล์ service ของตะกร้าแอบ import อะไรบางอย่างจากไฟล์หน้าจอสินค้า"

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

หัวใจอยู่ที่ไฟล์ที่ชื่อ product.dart ซึ่งทำหน้าที่เป็น "barrel file" ไฟล์ที่ re-export เฉพาะสิ่งที่ฟีเจอร์อื่นควรใช้ [1]

// lib/features/product/product.dart
library product;

export 'src/widgets/product_card.dart';
export 'src/models/product.dart';
// note: cart_writer.dart is intentionally NOT exported.
// it's an internal implementation detail of this feature.
Enter fullscreen mode Exit fullscreen mode

src/ คือข้างในของตัวต่อ พลาสติกที่ขึ้นรูปไว้ และ barrel file คือปุ่ม พื้นผิวเดียวที่ตัวต่ออื่นแตะได้ [1]

"ไม่มีไฟล์ใดใน features/cart/src/ ควร import ไฟล์ใน src/ ของ features/product/ ถ้าตะกร้าต้องการอะไรจากสินค้าจริง ๆ มัน import barrel file product.dart ไม่ใช่เข้าไปหยิบของข้างในโดยตรง"

LEGO Architecture กับ Clean Architecture ไม่ใช่คู่แข่งกัน

นี่คือส่วนที่ผมคิดว่ามีค่าที่สุด และเป็นส่วนที่คนมักเข้าใจผิด

Atuoha เขียนไว้ชัดว่า ทั้งสองสร้างบน dependency inversion เหมือนกัน ตามที่ Martin อธิบายไว้ใน [2] ทั้งคู่พึ่งพาสัญญาแทนของจริง และทั้งคู่มีจุดประกอบชิ้นส่วนจุดเดียว [1]

ต่างกันที่คำถามซึ่งแต่ละอันถูกออกแบบมาตอบ [1]

หัวข้อ LEGO Architecture Clean Architecture
คืออะไร วิธีคิดเรื่องการประกอบและขอบเขต ชุดชั้นที่มีชื่อและรูปร่างตายตัว
หน่วยการประกอบ เลือกเอง (widget / service / ฟีเจอร์) entity · use case · repository
เป้าหมายหลัก สลับชิ้นส่วนได้โดยไม่พังข้างเคียง ทดสอบได้ และไม่ขึ้นกับ framework
จุดเริ่มเรียน เริ่มได้ด้วย Flutter เอง ไม่ต้องมีอะไรเพิ่ม ต้องมีหลายไฟล์ต่อฟีเจอร์ตั้งแต่วันแรก
ความละเอียด เลือกเองได้ ละเอียดตายตัว ทำซ้ำทุกฟีเจอร์
เหมาะกับ ขอบเขตฟีเจอร์ยืดหยุ่น · ทีมทำงานขนาน · ค่อย ๆ รับ กฎธุรกิจซับซ้อนที่ต้องอยู่รอดข้าม UI
ความเสี่ยง "LEGO แค่ชื่อ" แอบหยิบของข้างในกัน เขียน boilerplate เพราะเชื่อว่าต้องมี

และประโยคที่ผมคิดว่าควรจำที่สุดของทั้งคัมภีร์

"คุณไม่ได้เลือกระหว่าง LEGO กับ Clean Architecture คุณเลือกว่าจะใช้รูปร่างเฉพาะของ Clean Architecture มากแค่ไหน ภายในตัวต่อ LEGO ของคุณ"

สองวิธีที่มันล้มเหลว

Atuoha ระบุไว้สองแบบ และผมคิดว่าตรงกับที่ทีมส่วนใหญ่เจอ [1]

หนึ่ง: "LEGO แค่ชื่อ" โฟลเดอร์ตั้งชื่อว่า features/cart/ แต่ไฟล์ข้างในกลับเข้าถึง ../../product/src/services/product_repository.dart โดยตรง

"วินาทีที่ไฟล์ใดก็ตามเอื้อมข้าม barrel file ของฟีเจอร์อื่นเข้าไปใน src/ ตัวต่ออิสระก็หมดไป สิ่งที่เหลือคือก้อนเดียวที่ใส่ชุดโฟลเดอร์ฟีเจอร์"

วิธีแก้เหมือนเดิมทุกครั้ง ส่งความพึ่งพาผ่านสัญญาใน core/contracts/ [1]

สอง: "Clean Architecture แบบบูชารูปแบบ" ฟีเจอร์ที่แค่ดึงรายการมาแสดง กลับมี entity, repository interface, repository implementation, DTO, use case และ cubit หกไฟล์ สามชั้น สำหรับหน้าจอที่ไม่มีตรรกะธุรกิจจริง [1]

และนี่คือข้อสรุปที่ผมชอบ งานนี้ไม่ใช่ความผิด แต่เป็นความพยายามที่เสียเปล่า [1]

"ประเด็นทั้งหมดของ Dependency Rule คือปกป้องตรรกะธุรกิจที่ผันผวนจากความเปลี่ยนแปลงของ framework และที่นี่ไม่มีตรรกะธุรกิจให้ปกป้อง"

คำแนะนำของเขาคือ ถ้าฟีเจอร์ไม่มีกฎอะไรนอกจากแสดงสิ่งที่เซิร์ฟเวอร์ส่งมา ปล่อยให้ repository คืนรูป DTO ตรง ๆ แล้วข้าม entity กับ use case ไปได้ ชั้นพวกนี้เพิ่มทีหลังได้เสมอในวันที่ตรรกะจริงมาถึง [1]

ระดับที่ควรไปถึง

คัมภีร์นี้แบ่งระดับไว้ชัด และบอกว่าแบบไหนเหมาะกับสถานการณ์ใด [1]

ระดับสูงสุดคือ modular monorepo ที่ฟีเจอร์กลายเป็นแพ็กเกจจริง และใช้เครื่องมืออย่าง Melos [4] ในการจัดการ workspace ทั้งชุด [1]

สถานการณ์ ระดับที่ควรใช้
แอปเล็ก เวลาสั้น กฎน้อย ประกอบ widget + สัญญาไม่กี่ตัวที่ต้องสลับจริง
ทีมโตขึ้น หลายคนแตะโค้ดเดียวกัน โฟลเดอร์ตามฟีเจอร์ + barrel file + FeatureModule
ตรรกะซับซ้อนที่ต้องอยู่ข้าม UI Clean Architecture เต็มรูปแบบในแต่ละฟีเจอร์
หลายทีมปล่อยงานอิสระ / design system ใช้ร่วมกัน modular monorepo จัดการด้วย ให้ compiler บังคับขอบเขตแทนการรีวิว

เริ่มจากอะไรได้พรุ่งนี้

Atuoha ปิดท้ายด้วยสิ่งที่จับต้องได้ ซึ่งผมคิดว่าเป็นคำแนะนำที่ดี ลองดึงตรรกะการตกแต่งออกจาก widget ตัวถัดไปของคุณไปเป็น component แบบ SurfaceCard

จากนั้นครั้งถัดไปที่คุณเขียนคลาส service ลองให้มัน implement abstract class แทนการเรียกใช้ตรง ๆ

ทุกอย่างที่เหลือในคัมภีร์ ทั้งฟีเจอร์โมดูล, service locator, monorepo, ชั้น Clean Architecture เป็นนิสัยเดียวกันที่ทำซ้ำในขนาดใหญ่ขึ้น [1]

ข้อควรระวัง

หนึ่ง ผมไม่ได้ทดสอบโค้ดใด ๆ ในบทความนี้ บทความนี้สรุปเนื้อหาจากคัมภีร์ของ Atuoha Anthony บน freeCodeCamp ไม่ใช่การทดลองของผมเอง [1]

สอง โค้ดตัวอย่างที่ยกมาเป็นของต้นฉบับ ผมไม่ได้แก้ไข มีเพียงคำอธิบายที่ผมเรียบเรียงใหม่ [1]

สาม คำแปลจากภาษาอังกฤษเป็นของผม หากต้องการถ้อยคำต้นฉบับครบถ้วน ควรอ่านคัมภีร์ฉบับเต็มตามลิงก์ในแหล่งอ้างอิง [1]

สี่ แพ็กเกจที่อ้างถึง (flutter_bloc, get_it, dio, go_router, melos) เป็นตัวอย่างที่ผู้เขียนเลือกใช้ ไม่ใช่ข้อบังคับ แนวคิดนี้ใช้ได้กับแพ็กเกจอื่นในหมวดเดียวกัน [1]

ห้า บทความนี้ยาวและมีโค้ดจำนวนมาก ผมเลือกยกมาเฉพาะส่วนที่อธิบายแนวคิดหลัก ผู้อ่านที่ต้องการลงมือทำควรเปิดคัมภีร์ฉบับเต็มประกอบ [1]

หก ผมทำงานบนระบบที่ใช้โมเดล AI และเขียนบทความนี้ด้วยความช่วยเหลือของ AI [5]

แหล่งอ้างอิง

[1] Anthony, A., "How to Implement LEGO Architecture in Flutter [Full Handbook]", freeCodeCamp News (11 ก.ย. 2026), https://www.freecodecamp.org/news/how-to-implement-lego-architecture-in-flutter-handbook/

[2] Martin, R. C., "The Clean Architecture", Clean Coder Blog (13 ส.ค. 2012), https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

[3] "Flutter's official app architecture guide", Flutter Documentation (เข้าถึง ก.ย. 2026), https://docs.flutter.dev/app-architecture

[4] "Melos, a tool for managing Dart and Flutter monorepos", Invertase (เข้าถึง ก.ย. 2026), https://melos.invertase.dev/

[5] การเปิดเผยของผู้เขียน: บทความนี้เขียนโดยใช้ AI

Top comments (0)