แชร์เรื่องนี้
หลักการทำงานแบบ SOLID สำหรับ DI และ GraphQL ใน .Net
โดย Seven Peaks เมื่อ 14 ส.ค. 2022, 17:14:00
ในบทความนี้เราจะสรุปสาระสำคัญจากงาน .Net Summer Meetup ที่จัดขึ้น ณ ออฟฟิศของ Seven Peaks ให้ผู้ที่สนใจได้รับรู้ โดยผู้เชี่ยวชาญของเราได้แนะนำให้ผู้ชมได้รู้จักกับโลกของ .Net เริ่มจากสิ่งที่ควรรู้ขั้นพื้นฐานอย่างหลักการทำงานแบบ SOLID ใน .Net และความรู้ที่นักพัฒนาสาย .Net ทุกคนควรรู้ จากนั้นจึงนำเข้าสู่เรื่องของเคสที่พบได้ทั่วไปเกี่ยวกับ dependency injection และปิดท้ายด้วยการใช้งาน GraphQL API ใน .Net เพื่อแชร์ข้อมูลไปยังหลายๆ แพลตฟอร์ม
ในขั้นแรก Giorgio Desideri ตำแหน่ง Tech Lead Cloud Solutions ผู้มีอารมณ์ขันเต็มเปี่ยมของเราจะให้ความรู้คุณเกี่ยวกับหลักการทำงานแบบ SOLID ใน .Net ซึ่งเป็นหนึ่งในชุดของหลักการที่โดดเด่นที่สุดในประวัติศาสตร์ของวิศวกรรมซอฟต์แวร์ จะเป็นอย่างไรบ้าง มาเริ่มกันเลย
หลักการทำงานแบบ SOLID และการทำงานรูปแบบต่างๆ ใน .Net
หลักการทำงานแบบ SOLID ที่ใช้ในภาษา C# และ .Net นั้นไม่ใช่เรื่องใหม่สำหรับวงการพัฒนาซอฟต์แวร์แต่อย่างใด แต่ก็น่าแปลกใจที่มีนักพัฒนาจำนวนไม่น้อยที่ยังไม่รู้ แม้ว่าทั้งภาษาและแพลตฟอร์มนี้จะมีมานานกว่า 40 แล้วก็ตาม โดย Giorgio ได้พูดถึงเรื่องนี้เพื่อย้ำเตือนให้นักพัฒนาได้เข้าใจคอนเซปต์นี้อีกครั้ง เพื่อช่วยให้พวกเขาสามารถพัฒนาแอปพลิเคชันได้ประสบความสำเร็จตามเป้าหมายที่วางไว้
SOLID เป็นคำที่ย่อมาจากSingle Responsibility Principle (SRP), Open closed Principle (OSP), Liskov substitution Principle (LSP), Interface Segregation Principle (ISP), และ Dependency Inversion Principle (DIP).
ซึ่ง SOLID เป็นรูปแบบการทำงานที่ช่วยให้การพัฒนาผลิตภัณฑ์ออกมามีคุณภาพสูงสุด โดยคอนเซปต์ทั้งหมดจะมีความเชื่อมโยงกัน เนื่องจากเมื่อเลือกใช้คอนเซปต์หนึ่ง ส่วนที่เหลือก็ต้องทำตามให้สอดคล้องไปพร้อมกัน
S – Single Responsibility Principle (SRP)
“ควรมีแค่เหตุผลเดียวเท่านั้นที่จะต้องไปแก้ไขคลาส” – Martin, Robert C.
เป็นที่รู้กันดีว่า “ทุกๆ โมดูล คลาส และฟังก์ชัน ควรมีหน้าที่เพียงแค่อย่างเดียวในแต่ละส่วนของการใช้งานซอฟต์แวร์” นั่นหมายความว่า แต่ละคลาสหรือโครงสร้างที่คล้ายคลึงกันในโค้ดของคุณควรมีหน้าที่เดียวเท่านั้น และองค์ประกอบย่อยทั้งหมดของมันควรเกี่ยวข้องกับหน้าที่นั้นๆ ด้วย ถ้าคุณต้องการแก้ไขมัน คุณก็จำเป็นต้องแก้พวกมันทั้งหมด
O – Open Closed Principle (OCP)
“Entity ของซอฟต์แวร์ (เช่น คลาส, โมดูล, ฟังก์ชัน, และอื่นๆ) ควรเปิดให้พร้อมสำหรับการเพิ่มความสามารถได้ แต่ก็ต้องปิดไว้ไม่ให้สามารถแก้ไขได้”
หมายความว่า เมื่อคุณเขียนโค้ด คลาส ฟังก์ชัน โมดูล หรืออะไรก็ตาม คุณควรปรับให้มันมีความยืดหยุ่นและรองรับการเพิ่มความสามารถในอนาคต แต่ต้องมีความปลอดภัยมากพอที่จะป้องกันการแก้ไขโดยไม่ได้รับอนุญาตได้
L – Liskov Substitution Principle (LSP)
ในปี 1988 Barbara Liskov ได้เสนอคอนเซปต์หนึ่งเอาไว้ว่า
“Let ϕ ( x ) be a property provable about objects x of type T.
Then ϕ ( y ) should be true for objects y of type S where S is a subtype of T.”
ซึ่งสามารถแปลสรุปให้เข้าใจง่ายๆ ได้ว่า “คลาสย่อยต้องสามารถทำหน้าที่ของคลาสแม่ได้” หลักการนี้พัฒนามาจาก Open-Closed Principle โดยในการพัฒนาซอฟต์แวร์จริงนั้น แนวคิดนี้คือการบอกว่าคลาสย่อยอะไรก็ตามต้องสามารถทำงานแทนคลาสแม่ได้ รวมถึงต้องทำงานได้เหมือนกันโดยไม่ต้องแก้ไขอะไรเลย
I – Interface Segregation Principle (ISP)
“ไม่ควรมีโค้ดใดของ client ถูกบังคับให้ไป implement method ที่ไม่ได้นำไปใช้งาน”
ข้อความนี้ทำให้เราสรุปได้ว่า การออกแบบอินเทอร์เฟซให้มีประโยชน์นั้น มันควรจะเชื่อมโยงกับโค้ดที่ใช้งานมันอยู่ได้เป็นอย่างดี มากกว่าโค้ดที่นำมันไป implement ซึ่งแทนที่จะกำหนด method ด้วยการอ้างอิงจากการนำคลาสไป implement แต่สิ่งที่ควรจะเป็นคือ method ของอินเทอร์เฟซควรถูกกำหนดโดย client code ว่าจำเป็นต้องนำไปใช้งานจริงๆ
D – Dependency Inversion Principle (DIP)
“โมดูลในระดับสูงไม่ควร import อะไรมาจากโมดูลระดับต่ำ ทั้งสองแบบควรขึ้นอยู่กับ abstraction”
“Abstraction ไม่ควรอ้างอิงละเอียดต่างๆ แต่รายละเอียด (ที่เป็นการทำ concrete implementation) ควรอ้างอิง abstraction”
พูดอีกนัยหนึ่งก็คือ โมดูลระดับสูงจะสร้างตรรกะอันซับซ้อนเอาไว้ในระบบ (แอปพลิเคชัน) ในขณะที่โมดูลระดับต่ำจะสร้างฟีเจอร์เพื่อประโยชน์ใช้สอยต่างๆ สิ่งสำคัญคือโมดูลระดับสูงต้องสามารถนำกลับมาใช้ได้ง่ายและจะต้องไม่เกิดความเปลี่ยนแปลงหากมีการแก้ไขที่โมดูลระดับต่ำ
Dependency Injection คืออะไร
dependency injection (DI) คือหลักการออกแบบที่นำไปใช้ในการ implement Inversion of Control(IoC)
รูปแบบของ dependency injection จะมีความเกี่ยวข้องกับคลาส 3 ประเภท ได้แก่
- client class
client class (dependent class) คือคลาสที่ขึ้นอยู่กับ service class
- service classs
service class (dependency) คือคลาสที่สร้าง service ให้กับ client class
- injector class
injector class ทำหน้าที่ส่งต่อ object ของ service class ไปยัง client class
Types of dependency injection
- constructor injection
injector ทำหน้าที่ส่งต่อ service (dependency) ไปทาง constructor ของ client class ซึ่งเป็นประเภทที่ใช้กันอย่างแพร่หลายที่สุด
- property injection(หรืออีกชื่อหนึ่งคือ setter injection)
injector ทำหน้าที่ส่งต่อ dependency ไปทาง public property ของ client class
- method injection
client class ทำการ implement อินเทอร์เฟซที่ประกาศ method เพื่อส่งต่อ dependency และ injector จะใช้อินเทอร์เฟซนี้เพื่อส่งต่อ dependency ไปยัง client class
เหตุผลที่ควรใช้ dependency injection
เรากำลังจะพูดถึง constructor injection การที่จะเตรียม client class ให้พร้อมสำหรับ constructor injection ได้นั้น เราจำเป็นต้องเปลี่ยนจากการ implement ในกรอบสีแดงข้างล่างซ้ายที่เราเห็นว่ามี dependency อยู่ใน constructor ไปยังกรอบสีเขียวด้านล่างขวา ที่เราจะได้รับ dependency โดยตรงในรูปแบบของพารามิเตอร์ที่อยู่ใน constructor แทน
ด้วยการแก้ไขง่ายๆ นี้ เราจะได้ประโยชน์มากมายจากการใช้งาน dependency injection เช่น
- รองรับการสร้าง component ที่พึ่งพากันน้อย
เราสามารถเขียนโปรแกรมแบบ loose coupling ที่ component พึ่งพากันน้อยได้ เพราะว่าไม่ต้อง implement คลาสนี้กับ service class อีกต่อไปแล้ว ทำให้คลาสใดก็ตามที่ implement _productRepository อินเทอร์เฟซของมันจะถูกส่งต่อไปและทำงานได้ตามปกติ
- รองรับการทำ unit testing
การใช้ dependency injection คือหนทางเดียวที่สามารถรองรับการทำ unit testing ที่เหมาะสมได้ เพราะถ้าเราไม่ส่งต่อ repositories กันแบบนี้ เราก็ไม่มีทางที่จะมาร์กมันทีหลังได้ เว้นเสียแต่ว่าเราจะเริ่มใช้ interceptor แปลกๆ มาช่วย แต่นั่นก็เป็นเรื่องที่ยุ่งยากมากเกินไป
- โค้ดอ่านง่าย
ช่วยให้โค้ดที่เราเขียนดูเรียบร้อย อ่านง่าย สบายตาขึ้น
- ทำตามหลักการของ dependency inversion (ซึ่งเป็น D ที่อยู่ในหลักการทำงานแบบ SOLID)
โมดูลระดับสูงไม่ควรอ้างอิงโมดูลระดับต่ำ และทั้งสองอย่างควรอ้างอิง abstraction ซึ่งในที่นี้คืออินเทอร์เฟซ
- ทำตามหลักการของ Inversion of Control (IoC)
เราควรส่งสิ่งที่ object สร้างขึ้นนอก dependant class ไปยังคลาสที่อยู่ภายนอก นั่นคือ container หรือ framework ที่ชื่อว่า IoC container
เกร็ดความรู้: IoC Container คืออะไร
IoC Container (หรือ DI Container) คือ framework สำหรับการ implement ตัว dependency injection แบบอัตโนมัติ
IoC Container ช่วยให้การสนับสนุนวงจรชีวิตของ DI เป็นเรื่องง่าย ดังต่อไปนี้
การใช้งาน constructor DI ใน .NET
ASP.NET Core มี IoC container พื้นฐานที่ built-in มาให้ในระบบเลย ซึ่งจะแสดงอยู่ในอินเทอร์เฟซของ IServiceProvider และ IServiceCollection ที่รองรับ constructor dependency injection แบบ default
จากนั้น คุณก็สามารถ register service ได้ ด้วยคำสั่งต่อไปนี้,
หมายเหตุ:
- Service lifetime จะเป็นแบบ transient
- IProductService จะเป็น service type ใช้สำหรับ resolve
- ProductService คือการ implement ที่ระบบมีให้
จากนั้นจึงทำการ resolve service ดังนี้
หมายเหตุ:
- พารามิเตอร์ของ service type ที่ทำการ register ไปแล้วจะถูกเพิ่มลงไปใน dependant class constructor
- เมื่อ dependant class (ProductsController) ถูก instantiate แล้ว DI container จะทำการส่งต่อ instance ของ dependency (ProductsService)
Service Scopes
ก่อนที่จะพูดถึงคอนเซปต์ของ Service Lifetime คุณควรทำความเข้าใจก่อนว่า service scope คืออะไร
service scope คือ child DI container ที่มีอายุสั้น ดังนั้น ทุกครั้งที่คุณสร้าง scope ขึ้นมาใหม่ คุณจะสร้าง DI container ที่มีอายุสั้นกว่า Root scope และในการสร้าง scope ใหม่ขึ้นมานั้น คุณจะมีทางเลือกอยู่สองทาง
ทางเลือกแรกคือ การใช้ CreateScope ที่แสดงโดย IServiceProvider อีกทางคือการปล่อยให้ web application ของ .Net สร้าง scope ของแต่ละ request ที่ได้รับมาโดยอัตโนมัติ
Services Lifetime
Services lifetime คือวิธีการควบคุมว่า service ควรมีอายุยาวนานแค่ไหนก่อนที่จะโดน dispose และมีวิธีการ register service อยู่ด้วยกัน 3 วิธี ได้แก่ transient, singleton, และ scoped โดยคุณมีหน้าที่เลือกอายุขัยที่เหมาะสมที่สุดให้กับ service ของคุณ
Transient
service instance จะถูกสร้างขึ้นทุกครั้งที่ service ได้รับ request หาก service สามารถ dispose ได้ มันก็จะโดน dispose เมื่อ service scope โดน dispose แล้ว
คุณสามารถใช้ transient lifetime ได้หาก service ของคุณ…
- มี non-shared state สำหรับ execution context
- ใช้ thread จำนวนมากพร้อมๆ กัน และไม่ได้เป็นแบบ thread safe
- มี transient dependency เช่น HttpClient ที่ตั้งใจให้อายุสั้น
Singleton
มีเพียง instance เดียวของ service ที่ถูกสร้างขึ้น singleton service จะไม่โดน dispose จนกว่า root scope โดน dispose ซึ่งมักจะเกิดขึ้นเมื่อออกจากแอปพลิเคชัน
คุณสามารถใช้ singleton lifetime ได้หาก service ของคุณ…
- มี shared state เช่น cache service
- เป็นแบบ stateless
Scoped
instance ของ service จะถูกสร้างขึ้นใหม่ในแต่ละ scope ซึ่งมันจะทำหน้าที่เสมือนเป็น singleton อยู่ใน scope นั้น หาก service เป็นแบบที่ dispose ได้ มันจะโดน dispose เมื่อ service scope โดน dispose
คุณสามารถใช้ scoped lifetime ได้หาก…
- คุณต้องการให้ service ทำหน้าที่เสมือน singleton ใน scope ที่ได้รับ request มา
- ฐานข้อมูลและ repository service มักได้รับการ register เป็น scoped service
- default registration ของ DbContext ใน EntityFramework Core เป็นแบบ scoped
GraphQL คืออะไร
“GraphQL คือ ภาษาสำหรับการเข้าถึงข้อมูล หรือ query language สำหรับเชื่อมต่อกับ API และการประมวลผลเพื่อเชื่อมโยงข้อมูลของ query กับข้อมูลที่คุณมีอยู่”
GraphQL ทำให้การพัฒนาความสามารถของ API เป็นเรื่องง่ายขึ้น ทำให้ client สามารถ request เฉพาะแค่ข้อมูลที่ต้องการได้ และเป็นเครื่องมือที่ดีของนักพัฒนา ทั้งยังสร้างคำบรรยายข้อมูลที่เข้าใจง่ายให้กับ API ของคุณได้อีกด้วย
GraphQL Type System
เป็นเหมือนระบบที่มี query จากนั้นเราก็ต้องกำหนดบาง type หรือบาง object ด้วย entity ที่เราสามารถนำไปรัน query ได้ ซึ่งใน GraphQL มี object ต่อไปนี้
- Object types – คุณสามารถใช้มันเพื่อกำหนด type ในการจัดการ ได้รับ หรือนำมาปรับแต่งได้ตามต้องการ
- Interfaces – เป็นประเภทของ abstraction ที่คุณมีในทุกภาษาแบบ object-oriented ซึ่งสามารถนำมา abstract และใช้ซ้ำได้
- Unions – เป็นเหมือนกับส่วนที่เชื่อมโยงระหว่างสอง object ที่คุณต้องการ manipulate
- Enumerations – เป็นค่าที่เหมือนกับที่คุณมีใน API ซึ่งคุณสามารถกำหนดค่าที่เป็นไปได้แต่ละค่า และมั่นใจได้ว่าจะมีค่าที่คุณกำหนดไว้ในรายกา
- Fields
- Lists – ก่อนหน้านี้เป็นพวก object ที่กำหนดไว้ แต่เป็นสิ่งที่อยู่ข้างใน เรายังคงต้องการ Scalars ซึ่งเป็นเครื่องมือจัดการกับ primitive ใน Java หรือภาษาใดๆ ก็ตาม โดยในท้ายที่สุดแล้ว คุณจะต้องเรียนรู้สิ่งต่อไปนี้
- Scalars
- String
- Float
- Int
- Boolean
- ID
- Custom scalars เช่น Date
GraphQL Operations
GraphQL มี operation ที่ใช้งานกันบ่อยที่สุด ดังนี้
|
|
|
อุปสรรคที่เรากำลังพยายามผ่านมันไปให้ได้
- Data requirement มีความแตกต่างกันไปในแต่ละอุปกรณ์และจัดการได้ยากขึ้นเมื่อผู้ใช้งานหลายคนใช้ข้อมูลร่วมกัน
- ผู้ใช้ต้องการเข้าถึงข้อมูลทันที
- ผู้ใช้ต้องการใช้แอปฯ ของพวกเขาต่อไปแม้ว่าสัญญาณอินเทอร์เน็ตจะแย่หรือไม่มีสัญญาณ
- การสร้างแอปฯ ที่ขับเคลื่อนด้วยข้อมูลและพร้อมในการขยายขีดความสามารถนั้นเป็นเรื่องยากหากเราไม่ได้เรียนรู้เกี่ยวกับคอนเซปต์ของ distributed system เลย
เปรียบเทียบการทำงานระหว่าง GraphQL และ REST แบบคร่าวๆ
GraphQL | REST |
---|---|
แอปพลิเคชันที่ทำงานแบบเรียลไทม์ | Non-Interactive (System to System) |
แอปพลิเคชันมือถือ | Microservices |
มี object hierarchy ที่ซับซ้อน | มี object hierarchy ที่เรียบง่าย |
มี query ที่ซับซ้อน | มี query ที่เรียบง่าย และทำงานซ้ำๆ |
พัฒนายาก | พัฒนาง่ายกว่า |
client สามารถอ่านข้อมูลได้ง่ายกว่า | มีความซับซ้อน client อ่านข้อมูลได้ยากกว่า |
สำหรับ GraphQL จะมีความยืดหยุ่นมากกว่าในการ query และสามารถ manipulate ได้แบบ dynamic มากกว่า REST API ดังนั้น คุณจึงสนุกและเล่นกับมันได้ด้วยการปรับแต่ง object ตามที่ต้องการ นอกจากนั้น คุณยังสามารถทำ web socket ได้ในแบบที่ protocol อื่นทำไม่ได้อีกด้วย
เท่านั้นยังไม่พอ client ยังสามารถอ่านข้อมูลจาก GraphQL ได้ง่ายกว่า เนื่องจากตอนที่คุณกำหนด type นั้น คุณกำหนดด้วย Scalars ทำให้เมื่อคุณอยู่บน explorer หรือใช้งานแอปฯ มือถืออยู่ คุณก็สามารถกำหนด type ได้ ด้วยเหตุนี้ เมื่อมีใครมาใช้งาน GraphQL API ก็จะเกิด error ทันทีด้วยการบอกว่า type ไม่ตรงกัน ดังนั้น client จึงทำงานง่ายขึ้นนิดหน่อยด้วยการสร้าง documentation ซึ่งคุณสามารถเล่นกับมันได้เต็มที่
อย่างไรก็ตาม REST API ก็ยังไม่ถูกเลิกใช้ไปเสียทีเดียว เพราะการพัฒนานั้นสามารถทำได้ง่ายกว่า แต่มีความซับซ้อนกว่านิดหน่อยในการทำ integration เพราะว่าเรายังต้องสร้าง documentation อยู่ ซึ่งมันจะยังรับข้อมูลทั้งหมดแม้ว่าจะไม่ต้องการก็ตาม
ในวิดีโอข้างล่างนี้เป็นเนื้อหาของการบรรยายจากผู้เชี่ยวชาญเกี่ยวกับหลักการทำงานแบบ SOLID ใน .Net ต่อด้วย dependency injection และ GraphQL และยังได้บรรยายต่อเนื่องไปถึงประเด็นของอุปสรรคและวิธีการเอาชนะพวกมัน ซึ่งเนื้อหาเหล่านี้อาจเป็นประโยชน์สำหรับคุณ
แชร์เรื่องนี้
- FinTech (11)
- การพัฒนาซอฟต์แวร์ (10)
- Expert Spotlight (8)
- อาชีพการงาน (8)
- Cloud (5)
- InsurTech (5)
- Mixpanel (5)
- Agile (4)
- Digital Transformation (4)
- JavaScript (4)
- QA (4)
- Trend (4)
- การพัฒนาแอปพลิเคชัน iOS (4)
- Android Developer (3)
- Azure (3)
- Banking (3)
- CSR (3)
- Hybrid App (3)
- IoT (3)
- Product-Centric Mindset (3)
- Seven Peaks Insights (3)
- Thought Leadership (3)
- การพัฒนาแอปฯ Android (3)
- การออกแบบ UX (3)
- บริษัท (3)
- เทคโนโลยีการเงินและการธนาคาร (3)
- .NET (2)
- AI (2)
- Cross-Platform Application (2)
- Data (2)
- Kotlin (2)
- Native App (2)
- ReactJS (2)
- digital marketing (2)
- การพัฒนาแอปฯ (2)
- งาน Product Owner (2)
- 5g (1)
- Android (1)
- AndroidX Biometric (1)
- Azure OpenAI Service (1)
- Biometrics (1)
- CI/CD (1)
- Customer Data Platform (1)
- Data and Analytics (1)
- Design Thinking (1)
- DevOps (1)
- Digital Healthcare (1)
- Digital ID (1)
- Digital Landscape (1)
- Digital Product (1)
- Digital Product Development (1)
- E-payment (1)
- E-wallet (1)
- Financial Inclusion (1)
- GraphQL (1)
- IT Outsourcing (1)
- MVP (1)
- MVVM (1)
- Metaverse (1)
- Morphosis (1)
- Node.js (1)
- Partner (1)
- Platform Engineering (1)
- Recruitment (1)
- SCB (1)
- SEO (1)
- Scrum Master (1)
- Software Engineer (1)
- Software Tester (1)
- Stripe (1)
- Swift (1)
- SwiftUI (1)
- Tech Meetup (1)
- Turnkey (1)
- UI (1)
- UX (1)
- UX Design (1)
- UX writing (1)
- Web-Debugging Tool (1)
- customer centric (1)
- iOS17 (1)
- waterfall (1)
- การจ้างงาน (1)
- การพัฒนาด้วย RabbitMQ (1)
- การพัฒนาระบบคลาวด์ (1)
- การออกแบบ Decorator Pattern (1)
- การใช้งาน C# (1)
- งาน Product Manager (1)
- งาน platform enginerring (1)
- ทำ Context API (1)
- ฟินเทค (1)
- ระบบการชำระเงิน (1)
- สร้าง brand loyalty (1)
- อีคอมเมิร์ซ (1)
- เขียนโค้ด React (1)
- เทคโนโลยี React (1)
- เพิ่ม conversion (1)
- เฟรมเวิร์ก (1)
- แดชบอร์ด (1)
- สิงหาคม 2024 (1)
- กรกฎาคม 2024 (2)
- มีนาคม 2024 (5)
- กุมภาพันธ์ 2024 (5)
- มกราคม 2024 (14)
- ธันวาคม 2023 (4)
- พฤศจิกายน 2023 (9)
- ตุลาคม 2023 (12)
- กันยายน 2023 (7)
- กรกฎาคม 2023 (4)
- มิถุนายน 2023 (3)
- พฤษภาคม 2023 (3)
- เมษายน 2023 (1)
- มีนาคม 2023 (1)
- พฤศจิกายน 2022 (1)
- สิงหาคม 2022 (4)
- กรกฎาคม 2022 (1)
- มิถุนายน 2022 (4)
- เมษายน 2022 (6)
- มีนาคม 2022 (3)
- กุมภาพันธ์ 2022 (6)
- มกราคม 2022 (3)
- ธันวาคม 2021 (2)
- ตุลาคม 2021 (1)
- กันยายน 2021 (1)
- สิงหาคม 2021 (3)
- กรกฎาคม 2021 (1)
- มิถุนายน 2021 (2)
- พฤษภาคม 2021 (1)
- มีนาคม 2021 (4)
- กุมภาพันธ์ 2021 (4)
- ธันวาคม 2020 (4)
- พฤศจิกายน 2020 (1)
- มิถุนายน 2020 (1)
- เมษายน 2020 (1)