description |
---|
แนวคิดในการสร้างกลุ่มของ object ที่มีความสัมพันธ์กัน |
เจ้าตัวนี้ผมขอตั้งชื่อเป็นภาษาไทยว่า โครงร่างของโรงงานผลิต และมันอยู่ในกลุ่มของ 🤰 Creational Patterns ซึ่งเจ้าตัวนี้จะมาช่วยแก้ปัญหาเมื่อเราต้องการจะสร้าง object ที่มีความสัมพันธ์กัน ซึ่งอ่านแล้วอาจจะ งงๆ หน่อย ดังนั้นลองไปดูโจทย์ของเรากันเลยละกัน
💡 ถ้าอยากเข้าใจ Abstract Factory Pattern ตัวนี้ได้เร็วขึ้น แนะนำให้อ่าน 🏭 Factory Method Pattern ก่อนนะครัช (เพราะมันแทบจะเหมือนกันเลย)
{% hint style="info" %}
แนะนำให้อ่าน
บทความนี้เป็นส่วนหนึ่งของมหากาพย์ Design Patterns ที่จะมาเป็น guideline ในการแก้ปัญหาในการออกแบบซอฟต์แวร์โปรเจค หากใครสนใจอยากเข้าใจตั้งแต่ต้นว่ามันคืออะไร และเจ้า patterns ทั้ง 23 ตัวมีอะไรบ้าง ก็สามารถจิ้มตรงนี้เพื่อไปอ่านบทความหลักได้เบยครัช 👦 Design Patterns
{% endhint %}
{% hint style="warning" %}
หมายเหตุ
เนื้อหาของบทความนี้จะเน้นให้เข้าใจหลักการทำงานของ Design Patterns แต่ละตัว โดยใช้เกม Ragnarok เป็นการอธิบาย ซึ่งบางอย่างอาจจะไม่ตรงกับตัวเกมจริงๆนะขอรับ Gravity อย่ามาจับผมนะผมโดนแมวน้ำครอบงำ + รู้เท่าไม่ถึงการ + ผมเป็นคนดี + ผมมีลูกมีเมียมีสามีที่ต้องดูแล 😭
เกลียด ชอบ ถูกใจ อยากติดตาม อยากติชมแนะนำด่าทอ หรืออะไรก็แล้วแต่ (ห้ามมายืมเงิน) จิ้มลงมาที่เพจนี้ได้เลย Mr.Saladpuk และจะเป็นประคุณอันล้นพ้นถ้ากด Like + Follow + Share ให้ด้วยขอรับ น้ำตาจิไหล 🥺 {% endhint %}
สมมุติว่าเราเขียนเกมที่มีแผนที่ 2 แบบคือแบบ ป่า (Payon) และ ทะเลทราย (Desert) ตามรูป
ซึ่งภายในแต่ละแผนที่จะมี monster หลายๆแบบอยู่ในนั้น เช่น สไลม์ (Slime), หมาป่า (Wolf), นกยักษ์ (Giant Bird) แต่เนื่องจากสภาพแวดล้อมต่างกันเลยทำให้ monster ที่อยู่ในนั้นมี หน้าตา กับ ชื่อเรียก ไม่เหมือนกัน ตามรูปด้านล่าง
แล้วเราจะเขียนโค้ดกันยังไงดีล่ะ เพื่อให้แผนที่ทั้ง 2 แบบ สามารถสร้าง monster พวกนี้ออกมาได้ถูกตามเงื่อนไข และ โค้ดจะต้องมีความยืดหยุ่นสูงด้วยนะ ?
จากตรงนี้ผมอาจจะมองว่ามี monster อยู่ทั้งหมด 3 ประเภท ดังนั้นผมก็จะแบ่งมันออกเป็น Model ทั้งหมด 3 กลุ่มตามรูปด้านล่างเบย
ส่วนตอนเขียนโค้ดเราก็แค่ สร้างเมธอดแยกตามประเภท monster และก็ไปเขียนเงื่อนไขเอาว่า ตอนนี้อยู่แผนที่อะไร เพียงเท่านี้เราก็จะสามารถสร้าง monster ออกมาได้ตรงตามเงื่อนไขที่ว่ามาละ ตามโค้ดด้านล่างเบย
public Slime CreateASlime(string mapName)
{
if(mapName == "payon")
{
return new Poporing();
}
else
{
return new Drops();
}
}
public Wolf CreateAWolf(string mapName)
{
if(mapName == "payon")
{
return new WildWolf();
}
else
{
return new DesertWolf();
}
}
// ที่เหลือไปคิดต่อเองคล้ายๆด้านบนแหละ
ซึ่งจากโค้ดด้านบนก็ไม่ได้มีอะไรผิดนะ สามารถทำงานได้ถูกต้องตามโจทย์เลย แต่จะเกิดอะไรขึ้นถ้า มีแผนที่ใหม่ถูกเพิ่มเข้าไป หรือ มี monster ใหม่ๆถูกเพิ่มเข้าไปในแต่ละแผนที่? ... เราก็ต้องไปไล่แก้โค้ดใหม่อะดิ 😨
{% hint style="danger" %}
Open & Close Principle (OCP)
อันนี้เป็นตัวอย่างในการออกแบบที่ละเมิดหลักในการออกแบบที่ชื่อว่า OCP นั่นเอง ซึ่งมันทำให้ทุกครั้งที่มีของใหม่ๆถูกเพิ่มเข้าไปปุ๊ป เราก็ต้องไปแก้โค้ดเดิมเสมอ สังเกตุได้ว่าถ้ามี แผนที่แบบใหม่เข้ามา เราก็จะต้องไปไล่แก้เจ้าพวก IF-ELSE ที่อยู่ด้านบนกันใหม่ทุกครั้งนั่นเอง
{% endhint %}
{% hint style="success" %}
แนะนำให้อ่าน
สำหรับใครที่ลืมหลักในการออกแบบเรื่องนี้ไปแล้วให้กดอ่านได้จากตรงนี้ Open & Close Principle
{% endhint %}
เจ้าโค้ดทั้งหมดที่ทำไว้ด้านบนจริงๆมันก็ เกือบจะเป็น Abstract Factory แล้วล่ะ แต่มันยังขาดหลักในการออกแบบอีกหลายอย่าง เลยทำให้ในอนาคตเราจะทำงานด้วยยาก ซึ่งก่อนที่เราจะไปกันต่อ ผมขอแปลงโค้ดทั้งหมดของเราให้กลายเป็นภาพที่ดูง่ายๆ ตามรูปด้านล่างนี้ละกันนะ
จากปัญหาที่ว่ามาเราจะพบว่า ทุกครั้งที่มีแผนที่ใหม่ หรือ monster แบบใหม่ๆเข้ามา มันจะทำให้ เราต้องไปแก้เจ้าคลาส MonsterFactory เสมอ เลยทำให้ในอนาคตมันจะ บวมฉ่ำ อย่างไม่ต้องสงสัยเลย
ส่วนสาเหตุการบวมนั้นเกิดจากเจ้า MonsterFactory ของเรามันดันไปดูแลทุกอย่างเลยยังไงล่ะ เช่น ดูแลเรื่องแผนที่ ดูแลเรื่องการสร้าง monster ซึ่งนี่คือหนึ่งในการละเมิดกฏของ SRP นั่นเอง
{% hint style="danger" %}
Single-Responsibility Principle (SRP)
เจ้าสิ่งนั้นๆควรมีหน้าที่รับผิดชอบเพียงอย่างเดียว เพราะถ้ามันดูแลหลายอย่าง นั่นหมายความว่า เวลาที่ Requirement เปลี่ยนมาทีนึง มันก็จมีโอกาสสูงมากที่การเปลี่ยนนั้นมันจะไปกระทบเจ้าสิ่งนั้น ทำให้เราต้องแก้ไขมัน ซึ่งผองเพื่อนอื่นๆที่มันดูแลอยู่นั้นไม่ได้เกี่ยวข้องเลยก็มีผลกระทบด้วยนั่นเอง ส่วนใครที่ลืมหรืออยากทบทวนเรื่อง SRP สามารถเข้าไปอ่านได้จากลิงค์นี้เบย Single-Responsibility Principle
{% endhint %}
ดังนั้นเราจัดการเรื่อง SRP เสียก่อน โดยทำการแยกของที่อยู่ในนั้นออกมาเป็นเรื่องๆ ซึ่งในตัวอย่างของเราก็มีแค่ 2 เรื่องนั่นคือ แผนที่ กับ Monster นั่นเอง
และถ้าเราดูความสัมพันธ์ของเจ้า 2 อย่างนี้ดีๆเราจะพบว่า แผนที่เป็นตัวกำหนดว่าจะสร้าง Monster แบบไหน นั่นเอง
แล้วถ้าเราดูเจ้าแผนที่ทั้ง 2 เราก็จะพบว่ามัน สร้างของประเภทเดียวกัน แต่ต่างกันที่รายละเอียด นั่นเอง เช่น แผนที่ต้องการสร้าง สไลม์, หมาป่า และ นกยักษ์ เหมือนกัน แต่ผลลัพท์จริงๆนั้นจะขึ้นอยู่กับแผนที่นั่นเอง ตามรูปด้านล่าง
จากที่ร่ายยาวมเราจะเริ่มมองเห็น รูปแบบ + หน้าที่รับผิดชอบ ของต่างๆตามนี้
- แผนที่ มีหน้าที่รับผิดชอบ สร้าง monster ต่างๆ
- แผนที่ มีรูปแบบในการสร้าง monster เหมือนๆกัน (สไลม์, หมาป่า, นกยักษ์)
- Monster ต่างๆ เป็นแค่ ผลลัพท์ ที่เราจะเอาไปใช้ต่อเท่านั้น
จากที่วิเคราะห์มาเลยทำให้เรารู้ว่า แผนที่มันควรถูกแยกออก เพราะแผนที่แต่ละแบบ มันกำหนดตัว สไลม์ หมาป่า และ นกยักษ์ ในรูปแบบของมันเอง ดังนั้นอย่าเอาไปรวมกันเลย แยกมันออกมาจะดีกว่า เลยได้ออกมาเป็นแบบนี้
ซึ่งเจ้าภาพด้านบน มันเป็นการสร้างกลุ่มของ monster ที่เป็นประเภทเดียวกันนั่นคือ
- PayonMonsterFactory จะสร้าง monster ที่อยู่ในป่า
- DesertMonsterFactory จะสร้าง monster ที่อยู่ในทะเลทราย
ดังนั้นถ้าเรามองจาก พฤติกรรม ของคลาสทั้ง 2 เราก็จะสามารถแยกมันออกมาเป็น Interface ที่ใช้ในการสร้าง Monster ได้ตามรูปด้านล่างนั่นเอง
หรือถ้าจะเขียนเป็นภาพแบบเต็มๆที่ถูกต้องก็จะได้รูปแบบนี้
เพียงเท่านี้เราก็สามารถแก้ปัญหาโจทย์นี้ได้เรียบร้อยแล้ว แถมเมื่อเราทำงานกับโรงงานพวกนี้ เรายังสามารถใช้ Best Practice ในเรื่องของ Program to an interface and not to an implementation. ได้อีกด้วย เพราะเราไม่ได้ทำงานกับระดับ Implementation แล้วยังไงล่ะ ซึ่งมันก็เป็นการเข้าข่ายกับ DIP ไปด้วยในตัวนั่นเอง เย่ๆ
{% hint style="success" %}
Dependency-Inversion Principle (DIP)
เป็นหนึ่งในหัวใจของการออกแบบให้เราไม่ไปผูกการทำงานไว้กับ Low level module นั่นเอง ส่วนใครที่อยากศึกษาเรื่องนี้เพิ่มเติมก็สามารถกดไปอ่านได้จากลิงค์นี้เบย Dependency-Inversion Principle
{% endhint %}
ยินดีด้วยในตอนนี้คุณได้ใช้สิ่งที่เรียกว่า Abstract Factory Pattern เรียบร้อยแล้ว ไม่ว่าจะรู้ตัวหรือไม่ก็ตาม เย่ๆ 👏
เจ้าตัวนี้มันจะคล้ายกับ 🏭 Factory Method Pattern มาก ดังนั้นผมจะเขียนจุดที่มันต่างกันของ Abstract Factory ไว้ใน Quote นะครับ
ในบางทีการสร้าง object นั้นมันก็ไม่ได้ง่ายเลย เช่น constructor มันรับหลายๆ parameters ก็ปวดหัวละ หรือถ้ามีการสร้าง object ที่สร้างยากๆตัวเดียวกันหลายๆจุดขึ้นมา มันก็แสดงว่าเราก็จะมีโค้ดแบบเดียวกันอยู่ซ้ำๆหลายที่เต็มไปหมด และการที่เราไปสร้าง object เองในบางทีก็อาจทำให้ Abstraction + Encapsulation ที่วางไว้เสียหายโดยไม่ได้ตั้งใจก็เป็นได้
จุดที่เป็นเรื่องเฉพาะของเรื่องนี้
แถมถ้าเราต้องสร้าง object ที่มันต้องไปด้วยกันเป็นเซตเราจะรู้ได้ยังไงว่าเราสร้างมันได้ถูก ไม่ได้เอาเซตอื่นๆมาปนกันมั่ว?
สามารถสร้าง object ที่พร้อมสำหรับใช้งานได้ โดยที่เราไม่ต้องระบุ data type ที่แท้จริงของ object ที่เราจะสร้างเลย
จุดที่เป็นเรื่องเฉพาะของเรื่องนี้
object ที่เราสร้างมาจาก Factory เดียวกันมันจะเป็นของเซตเดียวกัน
ตรงจุดนี้จะขออธิบายออกเป็นทีละขั้นตอนแบบนี้ละกัน คนที่พึ่งหัดออกแบบจะได้เข้าใจได้ง่ายๆนะ
object อะไรก็ตามที่เราอยากจะสร้าง เราจะเรียกมันว่า Product ซึ่งโดยปรกติเราก็จะมี product หลายๆแบบ เลยต้องทำมันเป็น Interface เอาไว้ (ตรงจุดนี้จริงๆจะเป็นอะไรก็ได้นะ ขอแค่เป็น abstraction ก็พอ)
ส่วนคลาส Product ที่แท้จริงนั้นก็จะไป implement IProduct อีกที ซึ่งโดยปรกติเราจะเรียกคลาสที่แท้จริงเหล่านั้นว่า Concrete class นั่นเอง ดังนั้นในกรณีนี้ผมจะเรียกมันว่า ConcreateProduct ละกัน
และเวลาใช้งานจริงๆ Product ประเภทเดียวกันก็อาจจะมีหลายแบบก็ได้ เช่น Slime ยังมี Drops กับ Poporing ไรงี้ ดังนั้นก็จะได้ภาพออกมาราวๆนี้
คราวนี้ถ้าเรามี Product หลายๆประเภท และแต่ละประเภทก็มีหลายๆแบบด้วยนะ เราก็จะได้ภาพออกมาราวนี้ๆ
และในบางที Product ที่อยู่ต่างประเภทกัน ก็อาจจะถูกจัดไว้ในเซตเดียวกันก็ได้ เช่น เซตธาตุไฟ เราก็จมี Slime ธาตุไฟ กับ หมาป่าธาตุไฟไรงี้
ส่วนตัวที่ทำหน้าที่สร้าง object เราจะเรียกมันว่า Factory ซึ่งการสร้างในรอบนี้มันมีเรื่องเซตมาเกี่ยวข้องด้วย ดังนั้นการสร้าง product มันจะต้องสามารถสร้างของทุกประเภทได้ทั้งหมดเลย โดยที่ product ที่ได้มามันจะต้องอยู่ภายในเซตเดียวกันนั่นเอง
แต่ตัว Factory เองนั้นมันไม่รู้หรอกว่ามันจะต้องสร้างอะไรออกมา ดังนั้นมันเลยต้องปล่อยให้เป็นหน้าที่ของคลาสระดับล่าง ที่รู้ว่ามันจะสร้าง product อะไรมาให้ ดังนั้นเลยทำให้เจ้า Factory ของเราคงอยู่แค่ในสภาพ Interface ก็เพียงพอแล้วนั่นเอง
นี่แหละคือที่มาของคำว่า Abstract Factory เพราะตัวมันเองคือ โครงร่างของโรงงานผลิต เพียงเท่านั้น
ดังนั้นคลาสระดับล่างที่รู้ว่าจะต้องสร้างอะไร ก็รับผิดชอบ Implement เจ้า Abstract Factory นี้ไปซะ
และเมื่อมันรู้ว่าจะต้องสร้าง product อะไรออกมา มันจะต้อง สร้าง product ที่อยู่ในเซตเดียวกัน ออกมาด้วยนะ
ซึ่งทั้งหมดที่ร่ายยาวมานั่นก็คือแนวทางในการออกแบบที่ชื่อว่า Abstract Factory Pattern นั่นเอง ซึ่งหน้าตาของมันก็จะประมาณรูปด้านบนนั่นแหละ แต่ขอเติมสัญลักษณ์ให้เข้าใจง่ายๆหน่อยนึงตามรูปด้านล่างละกันนะ
ไหนลองเอาที่เราออกแบบมาเทียบกันดูดิ๊ ... เหมือนกันเปี๊ยบเบย
{% hint style="success" %}
ข้อแนะนำ
ของทุกอย่างใน Design Patterns ทุกตัว เราไม่จำเป็นต้องทำตาม หรือ มีครบเหมือนตามที่เขาบอกไว้ก็ได้ (ถ้าเข้าใจ + มีเหตุผลที่ดีพอ) เพราะสิ่งที่ Pattern แต่ละตัวต้องการจะบอกเราคือ แนวทาง และเหตุผลในการออกแบบเพียงเท่านั้น ซึ่งสิ่งที่เราต้องทำต่อก็คือนำมันไปประยุกต์ให้เข้ากับปัญหาที่เราเจออยู่ให้เหมาะสมนั่นเอง
{% endhint %}
ข้อดีที่สุดของการนำ Abstract Factory Pattern มาใช้ก็คือ มันลดการผูกกันของโค้ด (decoupling)
นั่นเอง เพราะถ้าเราเขียนสร้าง object โดยใช้คำสั่ง new
แบบเดิม มันจะมีปัญหาหลายๆอย่างตามมา เช่น
{% hint style="warning" %}
ตัวอย่างพร้อมเหตุผลว่าทำไมถึงควรใช้ และมันมาแก้ปัญหาเรื่องอะไรบ้างของ Abstract Factory Pattern นั้นส่วนใหญ่มันจะเหมือนกับ Factory Method Pattern ซึ่งผมเขียนไว้ในบทความของ factory method แล้วยาวม๊วก เลยไม่อยากจะ copy มาใส่ตรงนี้ ดังนั้นรบกวนเพื่อนๆกดลิงค์ด้านล่างไปอ่านเอาละกันนะ แล้วจะเห็นภาพขึ้นเยอะเลยว่าทำไมถึงควรจะใช้มัน
Factory Method Pattern 🤔 ทำไมต้องใช้ด้วย ?
{% endhint %}
ถ้าเราต้องมาสร้าง object ด้วยตัวเองทุกครั้ง เราจะมั่นใจได้ยังไงว่าเราไป new object ในเซตที่เรากำลังทำงานด้วยอยู่ เช่น เราจะทำการสร้าง object ของ หมา, แมว, หมู, เสือ ก็จะเขียนออกมาได้ราวๆนี้
var dog = new Dog();
var cat = new Cat();
var pig = new Pig();
var tiger = new Tiger();
ซึ่งจากโค้ดด้านบน มันมีอะไรบอกไหมว่า หมา, แมว, หมู, เสือ พวกนี้มันมาจากเซตเดียวกันทั้งหมด? ซึ่งกว่าจะรู้ก็อาจจะไปแสดงผลให้ลูกค้าเห็นประมาณนี้แล้วก็ได้
อย่างที่บอกไปว่าเจ้าตัวนี้มันคล้ายกับ Factory Method Pattern ดังนั้นส่วนใหญ่ไปดูได้จากตัวนั้นเลย ดังนั้นเราจะมาตอบข้อดีเฉพาะของ Abstract Factory กันดีกว่า
เพียงแค่เราเปลี่ยนมาใช้ pattern นี้ มันก็เป็นการบังคับเราไปในตัวอยู่แล้วว่าให้เราสร้าง object ผ่านพวก ConcreteFactory ดังนั้นโค้ดเรา เมื่อจะสร้าง หมา แมว หมู เสือ ก็จะกลายเป็นแบบนี้
var dog = flatArtFactory.CreateADog();
var cat = flatArtFactory.CreateACat();
var pig = flatArtFactory.CreateAPig();
var tiger = flatArtFactory.CreateATiger();
ซึ่งถ้ามันยังสร้างผิดอีก นั่นแสดงว่าตัวที่ implement interface นั้นไปสร้าง object ผิดตัว ดังนั้นถ้าเราแก้มันเสร็จปุ๊ป ทุกจุดที่เรียกใช้มันก็จะไม่มี bug ตัวนี้อีกแล้วนั่นเอง (ดีกว่าไปไล่เช็คทุกจุดยังไงล่ะ)
ผมเชื่อว่าถ้าใครได้ดู Factory Method Pattern กับ Abstract Factory Pattern แล้ว ก็อาจจะ งงๆ กันอยู่นะว่ามันเหมือนหรือต่างอะไรกันบ้าง ดังนั้นตรงนี้จะมาไขข้อข้องใจกันครัช
- วัตถุประสงค์ของทั้ง 2 ตัวนั้น เหมือนกันคือ ช่วยสร้าง object ที่จะเกิดขึ้นจากการใช้คำสั่ง
new
ตรงๆ - Factory Method Pattern จะแก้ปัญหาผ่าน Inheritance โดยให้ Sub class เป็นคนจัดการ
- Abstract Factory Pattern จะแก้ปัญหาผ่าน Composition โดยใช้คลาสนั้นๆไป implement เอาเอง
- Factory Method Pattern จะสร้าง object โดยไม่ได้มีเรื่องเซตมาเกี่ยวข้อง
- Abstract Factory Pattern จะสร้าง object โดยคำนึงถึงเรื่องเซตด้วยเสมอ และ มันจะต้องสามารถสร้าง product อื่นๆที่อยู่ภายในเซตเหล่านั้นได้ด้วย
{% hint style="info" %}
เกร็ดความรู้
Abstract Factory Pattern นั้นภายในการทำงานจริงๆ ส่วนใหญ่ก็จะไปเรียกใช้งาน Factory Method Pattern มาทำงานต่ออีกทีนึงเช่นกัน เพราะมันก็จะช่วยลด Coupling ลง และยังช่วยทำให้โปรเจคของเราเปิดรับของต่างๆมากยิ่งขึ้นนั่นเอง
{% endhint %}
การนำ Abstract Factory Pattern มาใช้งานนั้นจะช่วย ลดการผูกกันของโค้ดลง ทำให้เราสามารถเปลี่ยนแปลง แก้ไข รองรับสิ่งต่างๆได้มากขึ้น และมันยังช่วยเปิดให้เราทำพวก Inversion of Control (IoC) ได้ง่ายขึ้นด้วย
- แค่จะสร้าง object ใหม่เฉยๆ ก็เพิ่มโค้ดเข้าไปมหาศาลแล้ว ดังนั้นโครงสร้างจะซับซ้อนขึ้นอีกเยอะเลย ดังนั้นก่อนใช้ให้คิดให้ดีก่อนว่า เรามีปัญหาถึงขนาดที่ต้องใช้มันหรือเปล่า?
- ถ้ามี product แบบใหม่ๆเข้ามา มันจะทำให้เราต้องแก้ interface ตามด้วย ดังนั้นมันจะส่งผลกระทบกับคลาสที่ implement interface เหล่านั้น
เราสามารถนำ Framework พวก Dependency Injection (DI) เข้ามาใช้แทนได้นะจ๊ะ โค้ดกระชับหลับสบายเต็มตื่นด้วย
{% hint style="danger" %}
ข้อควรระวัง
อย่านำ Abstract Factory Pattern ไปใช้มั่วซั่ว เพราะมันทำให้โค้ดของเราซับซ้อนขึ้นเยอะเลยแทนที่เราจะใช้คำสั่ง new แบบปรกติ ดังนั้นให้ชั่งน้ำหนักให้ดีเสียก่อนว่าปัญหาที่เราเจออยู่นั้น มันวุ่นวาย เทสยาก โค้ดมันผูกกันอยู่เยอะหรือเปล่า ถ้าชั่งน้ำหนักแล้ว + มีเหตุผลที่เพียงพอที่จะใช้ก็จงใช้ให้สบายใจไปเถิด
{% endhint %}
{% hint style="success" %} เกลียด ชอบ ถูกใจ อยากติดตาม อยากติชมแนะนำด่าทอ หรืออะไรก็แล้วแต่ (ห้ามมายืมเงิน) จิ้มลงมาที่เพจนี้ได้เลย Mr.Saladpuk และจะเป็นประคุณอันล้นพ้นถ้ากด Like + Follow + Share ให้ด้วยขอรับ น้ำตาจิไหล 🥺 {% endhint %}