บทความและข่าวสาร | Seven Peaks Insights

หลักการทำงานแบบ SOLID สำหรับ DI และ GraphQL ใน .Net

Basic SOLID Principles to Advanced DI and GraphQL in .Net World

ในบทความนี้เราจะสรุปสาระสำคัญจากงาน .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 เป็นรูปแบบการทำงานที่ช่วยให้การพัฒนาผลิตภัณฑ์ออกมามีคุณภาพสูงสุด โดยคอนเซปต์ทั้งหมดจะมีความเชื่อมโยงกัน เนื่องจากเมื่อเลือกใช้คอนเซปต์หนึ่ง ส่วนที่เหลือก็ต้องทำตามให้สอดคล้องไปพร้อมกัน

SOLID principles in .Net

 

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”

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


หลังจากที่เข้าใจหลักการทำงานแบบ SOLID ใน .Net แล้ว Massimo Dradi ผู้ซึ่งเป็น Senior Backend Developer ของเราจะช่วยให้คุณเอาตัวรอดจากปัญหาที่พบได้บ่อยจากโค้ดของแอปพลิเคชันที่ component พึ่งพากันมากเกินไป โดยเขาจะอธิบายคอนเซปต์หลายอย่างเกี่ยวกับ dependency injection และการสนับสนุนจาก .NET Core ซึ่งเป็นหนึ่งในเทคนิคที่ได้รับความนิยมมากที่สุดในการเขียนโค้ดให้สามารถดูแลได้ง่ายขึ้นหลังจากใช้งานไปแล้ว
 

Dependency Injection คืออะไร

dependency injection (DI) คือหลักการออกแบบที่นำไปใช้ในการ implement Inversion of Control(IoC)

รูปแบบของ dependency injection จะมีความเกี่ยวข้องกับคลาส 3 ประเภท ได้แก่

  1. client class

    client class (dependent class) คือคลาสที่ขึ้นอยู่กับ service class

  2. service classs

    service class (dependency) คือคลาสที่สร้าง service ให้กับ client class

  3. injector class

    injector class ทำหน้าที่ส่งต่อ object ของ service class ไปยัง client class

DI pattern

 

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 แทน 

constructor injection

 

ด้วยการแก้ไขง่ายๆ นี้ เราจะได้ประโยชน์มากมายจากการใช้งาน 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 เป็นเรื่องง่าย ดังต่อไปนี้

IoC-container
 
 

การใช้งาน constructor DI ใน .NET

ASP.NET Core มี IoC container พื้นฐานที่ built-in มาให้ในระบบเลย ซึ่งจะแสดงอยู่ในอินเทอร์เฟซของ IServiceProvider และ IServiceCollection ที่รองรับ constructor dependency injection แบบ default

จากนั้น คุณก็สามารถ register service ได้ ด้วยคำสั่งต่อไปนี้,

 
Register-services-in-.NET-built-in-DI-container
 

หมายเหตุ:

  1. Service lifetime จะเป็นแบบ transient
  2. IProductService จะเป็น service type ใช้สำหรับ resolve
  3. ProductService คือการ implement ที่ระบบมีให้
 

จากนั้นจึงทำการ resolve service ดังนี้

Resolve-services-in-.NET-built-in-DI-container
 

หมายเหตุ:

  1. พารามิเตอร์ของ service type ที่ทำการ register ไปแล้วจะถูกเพิ่มลงไปใน dependant class constructor
  2. เมื่อ 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

 

 
หลังจากที่คุณเข้าใจหลักการทำงานแบบ SOLID ใน .Net และ dependency Injection แล้ว สุดท้ายนี้ อยากให้คุณสนุกกับข้อมูลจาก Nicolas Pierson ที่เขาจะพูดถึงพื้นฐานของการใช้งาน GraphQL ใน .Net ซึ่งเป็น query language สำหรับ API และการประมวลผลคำสั่งฝั่งเซิร์ฟเวอร์สำหรับการสร้าง query โดยใช้ type system ที่คุณกำหนดให้กับข้อมูลของคุณ ถือว่าเป็นทางเลือกที่ทำงานได้ใกล้เคียงกับ REST API ดังนั้น มาดูกันเลยว่า GraphQL เป็นอย่างไร
 

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 ที่ใช้งานกันบ่อยที่สุด ดังนี้

  • Queries (อ่านข้อมูล)
 
GraphQL-operation-query
  • Mutation (เขียนข้อมูล)
 
GraphQL-operation-mutation-1
  • Subscription (อ่านข้อมูล)
GraphQL-operation-subscription

อุปสรรคที่เรากำลังพยายามผ่านมันไปให้ได้

  • 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 และยังได้บรรยายต่อเนื่องไปถึงประเด็นของอุปสรรคและวิธีการเอาชนะพวกมัน ซึ่งเนื้อหาเหล่านี้อาจเป็นประโยชน์สำหรับคุณ 

 
ร่วมงานกับบริษัทออกแบบและพัฒนาซอฟต์แวร์ชั้นนำของไทย
ดูตำแหน่งงานที่เปิดรับได้ที่นี่
ดูตำแหน่งงานที่เปิดรับ