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'),
)
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)}'),
);
}
}
มันตัดสินใจสองเรื่องที่ไม่เกี่ยวกันพร้อมกันในคลาสเดียว คือหน้าตาของกล่อง กับตัวเนื้อหา [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,
);
}
}
SurfaceCard รู้แค่เรื่องเดียว คือทำตัวเป็นกล่องมุมโค้งมีเงา และมันมีปุ่มเดียวคือ child ซึ่งรูปร่างเหมือนปุ่มของ Padding เป๊ะ [1]
และนี่คือบททดสอบว่าตัวต่อสร้างดีจริงไหม
"คุณใช้มันซ้ำในที่ใหม่ได้โดยไม่ต้องก็อปสักบรรทัดไหม ถ้าได้ ปุ่มของมันกำลังทำหน้าที่"
ปุ่มที่แท้จริง: สัญญาแทนการพึ่งพาของจริง
การประกอบ widget ให้ใช้ซ้ำได้นั้นยังไม่พอ ต้องสลับพฤติกรรมได้ด้วย [1]
Atuoha ยกตัวอย่างปุ่มเพิ่มสินค้าลงตะกร้า ที่ไม่ควรรู้ว่าข้างหลังเป็นการเรียก REST API เขียนลงเครื่อง หรือแค่พิมพ์ออกคอนโซล [1]
abstract class CartWriter {
Future<void> add(String productId);
}
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.
src/ คือข้างในของตัวต่อ พลาสติกที่ขึ้นรูปไว้ และ barrel file คือปุ่ม พื้นผิวเดียวที่ตัวต่ออื่นแตะได้ [1]
"ไม่มีไฟล์ใดใน
features/cart/src/ควร import ไฟล์ในsrc/ของfeatures/product/ถ้าตะกร้าต้องการอะไรจากสินค้าจริง ๆ มัน import barrel fileproduct.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)