🏭Factory Method

แนวคิดในการสร้าง object ที่เหมาะสมกับสถานะการณ์ที่กำลังเป็นอยู่

เจ้าตัวนี้มีชื่อเป็นภาษาไทยว่า โรงงานผลิต และมันอยู่ในกลุ่มของ 🤰 Creational Patterns โดยเจ้าตัวนี้มันจะมาช่วยให้เราไม่ต้องกังวลในการสร้าง object อีกเลย ซึ่งผมอธิบายหลักในการออกแบบพื้นฐานเอาไว้ที่บทความก่อนหน้านี้แล้ว ดังนั้นหากใครที่กระโดดข้ามมาอ่านตรงนี้เลย แนะนำให้กดกลับไปอ่านเจ้าลิงค์สีฟ้าๆด้านบนก่อนซัก 2 นาทีจะได้เข้าใจวัตถุประสงค์ของกลุ่มนี้ได้ดียิ่งขึ้นครัช

แนะนำให้อ่าน บทความนี้เป็นส่วนหนึ่งของมหากาพย์ Design Patterns ที่จะมาเป็น guideline ในการแก้ปัญหาในการออกแบบซอฟต์แวร์โปรเจค หากใครสนใจอยากเข้าใจตั้งแต่ต้นว่ามันคืออะไร และเจ้า patterns ทั้ง 23 ตัวมีอะไรบ้าง ก็สามารถจิ้มตรงนี้เพื่อไปอ่านบทความหลักได้เบยครัช 👦 Design Patterns

หมายเหตุ เนื้อหาของบทความนี้จะเน้นให้เข้าใจหลักการทำงานของ Design Patterns แต่ละตัว โดยใช้เกม Ragnarok เป็นการอธิบาย ซึ่งบางอย่างอาจจะไม่ตรงกับตัวเกมจริงๆนะขอรับ Gravity อย่ามาจับผมนะผมโดนแมวน้ำครอบงำ + รู้เท่าไม่ถึงการ + ผมเป็นคนดี + ผมมีลูกมีเมียมีสามีที่ต้องดูแล 😭

เกลียด ชอบ ถูกใจ อยากติดตาม อยากติชมแนะนำด่าทอ หรืออะไรก็แล้วแต่ (ห้ามมายืมเงิน) จิ้มลงมาที่เพจนี้ได้เลย Mr.Saladpuk และจะเป็นประคุณอันล้นพ้นถ้ากด Like + Follow + Share ให้ด้วยขอรับ น้ำตาจิไหล 🥺

🧐 โจทย์

สมมุติว่าเราเขียนเกมที่มีแผนที่ 2 แบบคือแบบ ป่า (Payon) และ ทะเลทราย (Desert) ตามรูป

ซึ่งภายในแผนที่นั้นจะมีตัว Slime น้อยน่ารักเป็น monster เดินอยู่ภายในแผนที่เหล่านั้น แต่ความวุ่นวายของมันก็คือ

  • Slime ที่อยู่ใน Payon นั้นจะเป็นตัวสีเขียว ชื่อว่า Poporing ที่มีความสามารถในการปล่อยพิษได้

  • Slime ที่อยู่ใน Desert จะเป็นตัวสีเหลือง ชื่อว่า Drops ที่สามารถปล่อยไฟได้

  • แผนที่ Payon ถ้าเป็นเวลา 18:00 ตรงเราจะเจอบอส Slime ที่ชื่อว่า Angeling

  • แผนที่ Desert ถ้าเป็นเวลา 19:00 ตรงเราจะเจอบอส Slime ที่ชื่อว่า Ghostring

  • และแผนที่ทั้ง 2 ถ้าเป็นเวลา 12:00 เราจะได้ Slime ที่ชื่อว่า Poring ซึ่งถ้ามันเกิดในแผนที่ Payon มันจะปล่อยพิษได้ แต่ถ้ามันเกิดในแผนที่ Desert มันจะปล่อยไฟได้

ซึ่งทั้งหมดที่ว่ามาก็จะสรุปเป็นภาพคร่าวๆได้ประมาณนี้

แล้วเราจะเขียนโค้ดกันยังไงดีล่ะ เพื่อให้แผนที่ทั้ง 2 แบบ สามารถสร้าง Slime ออกมาได้ถูกตามเงื่อนไข และ โค้ดจะต้องมีความยืดหยุ่นสูงด้วยนะ ?

🧒 แก้โจทย์ครั้งที่ 1

จากที่อ่านมาก็ดูเหมือนมันจะไม่ยากอะไรเลยชิมิ? เจ้าพวก Slime นั้นก็น่าจะเป็นกลุ่มตระกูลเดียวกันดังนั้นก็น่าจะอยู่ในรูปนี้ได้

ส่วนตอนสร้าง Slime ก็แค่สร้างเงื่อนไขว่า ตอนนี้อยู่แผนที่อะไร และ เป็นเวลาเท่าไหร่ เพียงเท่านี้เราก็จะสามารถสร้าง Slime ได้ถูกต้องตามเงื่อนไขที่ว่ามาละ ตามโค้ดด้านล่างเบย (ขอเขียนเวลาเป็น string นะจะได้เข้าใจง่ายๆหน่อย)

public Slime CreateNewSlime(string mapName, string currentTime)
{
    if(mapName == "Payon")
    {
        if(currentTime == "12:00")
        {
            return new Poring("Poison");
        }
        else if(currentTime == "18:00")
        {
            return new Angeling();
        }
        else
        {
            return new Poporing();
        }
    }
    else
    {
        // โค้ดคล้ายๆด้านบนแหละลองคิดเล่นๆดู
    }
}

ซึ่งจากโค้ดด้านบนก็ไม่ได้มีอะไรผิดนะ สามารถทำงานได้ถูกต้องตามโจทย์เลย แต่จะเกิดอะไรขึ้นถ้า Slime ที่อยู่ในแผนที่มันมีมากกว่าแค่ 4 แบบที่เราเห็น? หรือเงื่อนไขเวลามันเปลี่ยนแปลง? ... เราก็ต้องไปไล่แก้โค้ดใหม่อะดิ 😨

Open & Close Principle (OCP) อันนี้เป็นตัวอย่างในการออกแบบที่ละเมิดหลักในการออกแบบที่ชื่อว่า OCP นั่นเอง ซึ่งมันทำให้ทุกครั้งที่มีของใหม่ๆถูกเพิ่มเข้าไปปุ๊ป เราก็ต้องไปแก้โค้ดเดิมเสมอ สังเกตุได้ว่าถ้ามี Slime แบบใหม่เข้ามา หรือมีแผนที่แบบอื่นๆอีก เราก็จะต้องไปไล่แก้เจ้าพวก IF-ELSE ที่อยู่ด้านบนกันใหม่ทุกครั้งนั่นเอง

แนะนำให้อ่าน สำหรับใครที่ลืมหลักในการออกแบบเรื่องนี้ไปแล้วให้กดอ่านได้จากตรงนี้ Open & Close Principle

ถ้าใครคิดตามแล้วได้คำตอบออกมาเหมือนในโค้ดด้านบน ก็ไม่ต้องตกใจอะไรนะ เพราะจริงๆเจ้าโค้ดด้านบนนั้นมันก็คือ โรงงานผลิตอย่างง่าย ของ pattern ตัวนี้ ซึ่งเรามักเรียกมันว่า Simple Factory นั่นเอง โดยเจ้ารูปแบบนี้มันจะยังไม่สมบูรณ์เพราะมันยังขัดกับหลักการออกแบบหลายๆอย่างอยู่นั่นเอง

ก่อนที่จะไปทำให้มันสมบูรณ์ ผมจะขอแยกเจ้าเมธอด CreateNewSlime() ออกมาเป็น Model ที่ใช้ในการผลิต Slime ก่อนดีกว่า ซึ่งภาพด้านล่างคือโครงสร้างของ Simple Factory นั่นเอง

🧒 แก้โจทย์ครั้งที่ 2

🔥 วิเคราะห์ปัญหา

จากปัญหาที่ว่ามาเราจะพบว่า ทุกครั้งที่มีเงื่อนไขใหม่ มี Slime แบบใหม่ มีแผนที่แบบใหม่ หรืออะไรก็ตาม มันจะทำให้ เราต้องไปแก้เจ้าคลาส SimpleSlimeFactory เสมอ เลยทำให้ในอนาคตมันจะ บวมฉ่ำ อย่างไม่ต้องสงสัยเลย

ส่วนสาเหตุที่มันบวมก็เพราะว่า มันดูแลเรื่องจุกจิกจากหลายเรื่อง เช่น ต้องคอยรับแผนที่ใหม่ๆ ต้องคอยรับ Slime แบบใหม่ๆ และไหนจะเงื่อนไขจุกจิกอย่างเวลาในแต่ละแผนที่อีกนั่นเอง

ดังนั้นเพื่อที่จะแก้ปัญหาการบวมฉ่ำของมัน เราก็ควรที่จะแยกเรื่องจุกจิกออกเป็นเรื่องๆ โดยถ้ามองจากรูปด้านบนเราก็จะสามารถแบ่งออกมาได้เป็น 2 กลุ่มคือ แผนที่ กับ Slime นั่นเอง

และถ้าเราดูความสัมพันธ์ของเจ้า 2 อย่างนี้ดีๆเราจะพบว่า แผนที่เป็นตัวกำหนดว่าจะสร้าง Slime แบบไหน นั่นเอง และรวมถึงความสามารถของ Slime ด้วย ตามรูปด้านล่างเบย

🔥 แก้ไขปัญหา

จากที่ร่ายมาทั้งหมดเราจะสามารถหา พฤติกรรม + หน้าที่การรับผิดชอบ ออกมาได้ว่า

  • แผนที่ มีหน้าที่รับผิดชอบในการ สร้าง Slime

  • แผนที่ ต้องเป็นคน ดูแลเงื่อนไขต่างๆ ในการสร้าง Slime (เช่น เวลา, ความสามารถพิเศษ)

  • Slime เป็นแค่ ผลลัพท์ ที่เราอยากได้เพียงแค่นั้น

จากที่วิเคราะห์มาเลยทำให้เรารู้ว่า แผนที่มันควรถูกแยกออก ไปรับผิดชอบในการสร้าง Slime ของใครของมันเอง เพรามันมีเงื่อนไขจุกจิกเยอะ อย่าเอาไปรวมกันเลย แยกเป็นเรื่องๆจะดีกว่า ดังนั้นเราเลยจะได้โรงงานผลิตผลิต Slime ออกมา 2 โรงงานตามนี้

ซึ่งแต่ละโรงงานก็จะมีหน้าที่คอยดูแลการผลิต Slime ของใครของมันเอง ซึ่งตรงตามหลัก SRP อีกด้วย

Single-Responsibility Principle (SRP) เจ้าสิ่งนั้นๆควรมีหน้าที่รับผิดชอบเพียงอย่างเดียว ซึ่งเจ้าโรงงานแต่ละโรงงานด้านบน มันก็จะดูแลแค่หน้าที่การผลิต Slime เพียงอย่างเดียวเท่านั้น ซึ่งถ้าเงื่อนไขในการผลิต Slime ของโรงงาน Desert เปลี่ยนก็ไม่ไปกระทบอะไรกับโรงงาน Payon นั่นเอง ส่วนใครที่ลืมหรืออยากทบทวนเรื่อง SRP สามารถเข้าไปอ่านได้จากลิงค์นี้เบย Single-Responsibility Principle

แต่มันยังไม่จบเท่านั้น เพราะมันยังขัดกับ OCP (ปัญหาตัวแรกที่อยู่ด้านบน) อยู่เหมือนเดิม เพราะเวลามีอะไรใหม่ๆเข้ามา เราก็ต้องไปไล่แก้โค้ดเก่าของเราอยู่ดี ... แต่ถ้าเรามาคิดดีๆ เราก็จะพบว่า Payon และ Desert นั้นมันก็ เป็นโรงงานทั้งคู่ อยู่แล้ว ดังนั้นมันก็น่าจะรวมร่างเป็นแบบนี้ได้นะ

แต่จากรูปด้านบนจะเห็นว่าเมธอด CreateNewSlime() ที่อยู่ใน Base class (เส้นสีแดง) นั้น มันไม่รู้ว่าจะต้องสร้าง Slime จริงๆยังไง เพราะ Sub class ของมันต่างหาก (เส้นสีเขียว) คือตัวที่รู้ว่าในสถานะการณ์นี้จะต้องสร้าง Slime อะไรออกมา ดังนั้น Base class ควรจะปล่อยให้ Sub class ทำหน้าที่ในการสร้าง Slime ไปซะ เลยทำให้เราต้องเพิ่ม Abstract method เข้ามาอีกตัวให้ sub class รับผิดชอบเรื่องการสร้าง slime นั่นเอง

public abstract class SlimeFactory
{
    public Slime CreateNewSlime(string currentTime)
    {
        return BuildASlime(currentTime);
    }

    protected abstract Slime BuildASlime(string currentTime);
}

เพียงเท่านี้เราก็สามารถแก้ปัญหาโจทย์นี้ได้เรียบร้อยแล้ว แถมเมื่อเราทำงานกับโรงงานพวกนี้ เรายังสามารถใช้ Best Practice ในเรื่องของ Program to an interface and not to an implementation. ได้อีกด้วย เพราะเราไม่ได้ทำงานกับระดับ Implementation แล้วยังไงล่ะ ซึ่งมันก็เป็นการเข้าข่ายกับ DIP ไปด้วยในตัวนั่นเอง เย่ๆ

Dependency-Inversion Principle (DIP) เป็นหนึ่งในหัวใจของการออกแบบให้เราไม่ไปผูกการทำงานไว้กับ Low level module นั่นเอง ส่วนใครที่อยากศึกษาเรื่องนี้เพิ่มเติมก็สามารถกดไปอ่านได้จากลิงค์นี้เบย Dependency-Inversion Principle

ยินดีด้วยในตอนนี้คุณได้ใช้สิ่งที่เรียกว่า Factory Method Pattern เรียบร้อยแล้ว ไม่ว่าจะรู้ตัวหรือไม่ก็ตาม เย่ๆ 👏

🤔 Factory Method คือไย ?

🔥 จุดกำเหนิด

ในบางทีการสร้าง object นั้นมันก็ไม่ได้ง่ายเลย เช่น constructor มันรับหลายๆ parameters ก็ปวดหัวละ หรือถ้ามีการสร้าง object ที่สร้างยากๆตัวเดียวกันหลายๆจุดขึ้นมา มันก็แสดงว่าเราก็จะมีโค้ดแบบเดียวกันอยู่ซ้ำๆหลายที่เต็มไปหมด และการที่เราไปสร้าง object เองในบางทีก็อาจทำให้ Abstraction + Encapsulation ที่วางไว้เสียหายโดยไม่ได้ตั้งใจก็เป็นได้

🔥 ผลจากการใช้

สามารถสร้าง object ที่พร้อมสำหรับใช้งาน โดยที่เราไม่ต้องระบุ data type ที่แท้จริงของ object ที่เราจะสร้างเลย และเปิดให้คลาสลูกเป็นคนตัดสินใจในการสร้าง object ที่เหมาะสมมาให้แทน

🔥 วิธีการใช้

ตรงจุดนี้จะขออธิบายออกเป็นทีละขั้นตอนแบบนี้ละกัน คนที่พึ่งหัดออกแบบจะได้เข้าใจได้ง่ายๆนะ

object อะไรก็ตามที่เราอยากจะสร้าง เราจะเรียกมันว่า Product ซึ่งโดยปรกติเราก็จะมี product หลายๆแบบ เลยต้องทำมันเป็น Interface เอาไว้ (ตรงจุดนี้จริงๆจะเป็นอะไรก็ได้นะ ขอแค่เป็น abstraction ก็พอ)

ส่วนคลาส Product ที่แท้จริงนั้นก็จะไป implement IProduct อีกที ซึ่งโดยปรกติเราจะเรียกคลาสที่แท้จริงเหล่านั้นว่า Concrete class นั่นเอง ดังนั้นในกรณีนี้ผมจะเรียกมันว่า ConcreateProduct ละกัน

ส่วนตัวที่ทำหน้าที่สร้าง object เราจะเรียกมันว่า Factory ซึ่งโดยปรกติ เจ้าตัวนี้มันจะไม่รู้หรอกว่ามันต้องสร้าง Product อะไรออกมา มันมีหน้าที่เป็นแค่โครงให้คนอื่นมาเรียกใช้มันเท่านั้น มันเลยเป็นได้แค่ Abstract class ซึ่งมันจะมี Abstract method ไว้ให้ตัว Factory ที่รู้ว่าจะสร้าง Product อะไรบ้าง เป็นคนมากำหนดการทำงานนั่นเอง

ดังนั้นสุดท้าย Factory ที่แท้จริง หรือ ConcreteFactory ก็จะเป็นคนมากำหนดว่าจะสร้าง Product ที่แท้จริงอะไรนั่นเอง

จากที่ร่ายยาวมาทั้งหมดเราก็จะได้แนวทางในการออกแบบที่ชื่อว่า Factory Method Pattern ออกมา ซึ่งมีหน้าตาประมาณรูปด้านล่างนี้นั่นเองครัช

ไหนลองเอาที่เราออกแบบมาเทียบกันดูดิ๊ ... เหมือนกันเปี๊ยบเบย

ข้อแนะนำ ของทุกอย่างใน Design Patterns ทุกตัว เราไม่จำเป็นต้องทำตาม หรือ มีครบเหมือนตามที่เขาบอกไว้ก็ได้ (ถ้าเข้าใจ + มีเหตุผลที่ดีพอ) เพราะสิ่งที่ Pattern แต่ละตัวต้องการจะบอกเราคือ แนวทาง และเหตุผลในการออกแบบเพียงเท่านั้น ซึ่งสิ่งที่เราต้องทำต่อก็คือนำมันไปประยุกต์ให้เข้ากับปัญหาที่เราเจออยู่ให้เหมาะสมนั่นเอง

🤔 ทำไมต้องใช้ด้วย ?

ข้อดีที่สุดของการนำ Factory Method Pattern มาใช้ก็คือ มันลดการผูกกันของโค้ด (decoupling) นั่นเอง เพราะถ้าเราเขียนสร้าง object โดยใช้คำสั่ง new แบบเดิม มันจะมีปัญหาหลายๆอย่างตามมา ซึ่งผมขอยกตัวอย่างให้เห็นภาพเป็นตามด้านล่างละกัน

😱 Hardcode

สมมุติว่าเราเขียนโค้ดเป็นแบบด้านล่างนี้ละกัน

var animal = new Dog();

ซึ่งดูแล้วก็เหมือนจะไม่ผิดบาปอะไรใช่ไหม แต่จริงๆมันหมายความว่า โค้ดของเราต้องทำงานกับคลาส Dog เท่านั้น!! ไม่มีวันเปลี่ยนเป็นอย่างอื่นเลย ถ้าอยากเปลี่ยนให้มันไปทำงานกับคลาส Cat แล้วล่ะก็ เราก็ต้อง Build & Compile & Deploy ใหม่ทุกครั้งที่มีการแก้โดยเลี่ยงไม่ได้เลย

😱 Dependencies

สมมุติว่าเราต้องไปสร้าง object ของคลาสด้านล่างนี้ โดยที่มันมี Constructor หน้าตาประมาณนี้

public class DatabaseConnector
{
    public DatabaseConnector(IConnection conn,
        IResource resource,
        IProxy proxy,
        ICancelationToken cancelation,
        ..และอื่นๆ..)
    {
        // ทำอะไรซักอย่างไม่ต้องไปสนใจ
    }
}

เพียงแค่เห็น parameters ที่เราต้องส่งให้มันก็น่าจะหนาวแล้ว แต่ประเด็นไม่ใช่ที่จำนวน parameters ของมันนะ เพราะสิ่งที่น่ากลัวคือ Dependencies ของมันต่างหาก ลองนึกดูว่าจะสร้างเจ้าตัวนี้ทีนึง โค้ดง่ายๆของเราก็อาจจะซับซ้อนและมีของต่างๆที่ไม่เกี่ยวข้องกับมันเข้ามันเต็มไปหมดทั้งๆที่ตรงจุดนั้นไม่ได้จะใช้เลยด้วยซ้ำ!!

😱 เทสยาก

สมมุติว่าโค้ดของเราเขียนเป็นแบบนี้ละกัน

var dbConn = new MySqlConnector();

คำถามคือแล้วเวลาที่เราจะเทสมันล่ะ เราจะต้องยิงไปที่ตัว database จริงเพื่อเทสเลยเหรอ? หรือถ้าเราจะ Mock มันล่ะจะทำยังไง?

เขียนจนหน้ากระดาษจะเต็มอยู่ละ เพียงเท่านี้ก็น่าจะเห็นข้อเสียที่ซ่อนอยู่กับการใช้คำสั่ง new ตรงๆกันแล้วนะว่ามันจะนำพาความสนุกอะไรมาให้เราบ้าง ลองไปคิดต่อเอาดูก็น่าจะได้อีกหลายเหตุผลเลยเพราะเจ้า Coupling ตัวเดียวนี่แหละ

🤔 ใช้แล้วดียังไง ?

อย่างที่บอกว่า Design Pattern ตัวนี้ มันจะทำให้โค้ดของเรา มันลดการผูกกันของโค้ด (decoupling) ดังนั้นปัญหาที่ว่าด้านบนมันจะละลายหายไปจนหมดเลย ตามนี้

👨‍🔧 Hardcode

จากเดิมแทนที่เราจะใช้คำสั่ง new ไปสร้าง object ให้มันผูกติดกับคลาสนั้นๆเอง มันก็จะเปลายเป็นแบบนี้

var animal = animalFactory.GetAnAnimal();

ซึ่งจากโค้ดใหม่นี้จะเห็นว่า ไม่มีการผูกติดกันอีกแล้ว มันจะสร้าง object อะไรมาให้เราก็ได้ ขึ้นกับความเหมาะสมของคลาสลูกนั่นเอง

👨‍🔧 Dependencies

แม้ว่าคลาสที่เราอยากจะสร้าง object จะวุ่นวายขนาดไหนก็ตาม คนที่จะใช้งานก็ไม่ต้องรับรู้ความวุ่นวายเหล่านั้นเลย เพราะมันโดนกลืนสิ่งเหล่านั้นไปเรียบร้อยใน ConcreteFactory แล้วยังไงล่ะ ตามโค้ดด้านล่างนี้นั่นเอง

var dbConn = dbConnectorFactory.CreateConnector();

👨‍🔧 เทสยาก

เมื่อเราอยากจะเทสอะไรก็จะไม่ใช่เรื่องยากอีกต่อไป เพราะโค้ดของเราไม่ได้ผูกกับอะไรอีกแล้ว ดังนั้นเราก็จะสามารถ Mock เข้าไปตรงๆ หรือจะทำเป็น MockFactory เข้าไปก็ยังได้เลย ตามโค้ดด้านล่าง

var dbConn = dbConnectorFactory.CreateConnector();

// เล่นตามเกมก็ได้
public class MockDbConnectorFactory : DbConnector { ... }

// หรือจะส่ง mock object เข้าไปแทนที่ผ่าน constructor ก็ได้
Constructor( mockFactory )

🎯 บทสรุป

👍 ข้อดี

การนำ Factory Method Pattern มาใช้งานนั้นจะช่วย ลดการผูกกันของโค้ดลง ทำให้เราสามารถเปลี่ยนแปลง แก้ไข รองรับสิ่งต่างๆได้มากขึ้น และมันยังช่วยเปิดให้เราทำพวก Inversion of Control (IoC) ได้ง่ายขึ้นด้วย

👎 ข้อเสีย

แค่จะสร้าง object ใหม่เฉยๆ ก็เพิ่มโค้ดเข้าไปมหาศาลแล้ว ดังนั้นโครงสร้างจะซับซ้อนขึ้นอีกเยอะเลย ดังนั้นก่อนใช้ให้คิดให้ดีก่อนว่า เรามีปัญหาถึงขนาดที่ต้องใช้มันหรือเปล่า?

🤙 ทางเลือก

เราสามารถนำ Framework พวก Dependency Injection (DI) เข้ามาใช้แทนได้นะจ๊ะ โค้ดกระชับหลับสบายเต็มตื่นด้วย

ข้อควรระวัง อย่านำ Factory Method Pattern ไปใช้มั่วซั่ว เพราะมันทำให้โค้ดของเราซับซ้อนขึ้นเยอะเลยแทนที่เราจะใช้คำสั่ง new แบบปรกติ ดังนั้นให้ชั่งน้ำหนักให้ดีเสียก่อนว่าปัญหาที่เราเจออยู่นั้น มันวุ่นวาย เทสยาก โค้ดมันผูกกันอยู่เยอะหรือเปล่า ถ้าชั่งน้ำหนักแล้ว + มีเหตุผลที่เพียงพอที่จะใช้ก็จงใช้ให้สบายใจไปเถิด

เกลียด ชอบ ถูกใจ อยากติดตาม อยากติชมแนะนำด่าทอ หรืออะไรก็แล้วแต่ (ห้ามมายืมเงิน) จิ้มลงมาที่เพจนี้ได้เลย Mr.Saladpuk และจะเป็นประคุณอันล้นพ้นถ้ากด Like + Follow + Share ให้ด้วยขอรับ น้ำตาจิไหล 🥺

Last updated