วันเสาร์ที่ 1 พฤศจิกายน พ.ศ. 2557

Dependency Injection ต่างจาก Dependency Inversion Principle อย่างไรนะ

Dependency Injection ต่างจาก Dependency Inversion Principle อย่างไรนะ
| October 29, 2014| Programming |5 Comments
Like
179
4
This page has been shared 6 times. View these Tweets.
image

บ่อยครั้งที่มักพบว่า คนส่วนใหญ่มักจะสับสนกันระหว่างคำว่า
Dependency Injection และ Dependency Inversion
ยิ่งพูดตัวย่อว่า “DI” แล้ว ยิ่งงงอีกว่า I ย่อมาจากอะไรกันแน่
ดังนั้นมาดูกันว่า
  • ทั้งสองคำมันคืออะไร
  • เหมือนหรือต่างกันอย่างไร
เพื่อไม่ให้เกิดความสับสน
ดังนั้นมาทำความเข้าใจโดยสังเขปกันหน่อย

เริ่มต้นด้วยทั้งสองคำนั้นมีเป้าหมายเหมือนกัน คือ

ขจัด code ที่ผูกมัดกันแบบแน่นๆ ออกไป
ช่วยลดความซับซ้อนของ code
ลดไม่ให้เกิด code แบบมั่วๆ ดังที่ชอบเรียกว่า Spaghetti code
ลดเวลาในการดูแลรักษา code ลงไป

Dependency Inversion Principle (DIP) คืออะไร

คือ D ใน SOLID  เป็นแนวปฎิบัติที่ดีสำหรับการออกแบบในโลกของ Object-Oriented
ซึ่งคุณ Robert C. Martin พูดไว้ในเอกสารเรื่อง DIP
กล่าวไว้ว่า
High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.
"
แปลแบบงงๆ จะได้ว่า
Module ที่อยู่ในลำดับที่สูงกว่าต้องไม่ขึ้นอยู่กับ Module ที่อยู๋ในระดับต่ำกว่า
แต่ทั้งคู่จะต้องขึ้นอยู่กับส่วนที่เรียกว่า Abstraction
ส่วนของ Abstraction นั้นจะไม่ขึ้นอยู่กับส่วนรายละเอียด
โดยที่รายละเอียดจะต้องขึ้นอยู่กับส่วน Abstraction
"
หัวใจคือ Abstraction นั่นเอง
แต่อ่านแล้วมันยังงงๆ มากมาย ดังนั้น มาดูตัวอย่างกันดีกว่า

ตัวอย่างการส่งข้อความแจ้งเรื่อง promotion ต่างๆ ผ่านทาง Email

แสดงการทำงานด้วย UML ดังรูป
Screen Shot 2557-10-28 at 3.50.54 PM

สามารถเขียน code ได้ดังนี้
12345
public class Email {
 
public void send() {
}
}
view rawEmail.java hosted with ❤ by GitHub
12345678910111213
public class Notification {
Email email;
public Notification() {
email = new Email();
}
public void notifyPromotion() {
email.send();
}
 
}
view rawNotification.java hosted with ❤ by GitHub
คำอธิบาย
จะพบว่า class Notification นั้นผูกติดกับ class Email เป็นอย่างมาก
นั่นคือ เมื่อทำการสร้าง object ของ class  Notification แล้ว
จะต้องสร้าง object ของ class Email ด้วยเสมอ
เรียกว่าทั้งสอง class มันผูกติดกันแน่นมากๆ ( Tightly coupling )
เป็นการละเมิดกฏ DIP อย่างแรง ส่งผลให้เมื่อทำการแก้ไขที่ class Email แล้ว
มันมีโอกาสที่จะทำให้การทำงานใน class Notification ทำงานผิดพลาดได้ง่าย
คิดว่า developer หลายๆ คนน่าจะพอจินตนาการได้นะ
และยิ่งระบบต้องการส่งข้อความไปยังช่องทางอื่นๆ ล่ะ
เช่น SMS, Line, Facebook message และ Twitter
  • จะทำอย่างไร ?
  • จะต้องทำการแก้ไขที่ class Notification เยอะเลยไหม ?
  • ยากหรือเปล่า ?
  • มันจะกระทบกับส่วนการทำงานอื่นๆ ที่เคยทำงานได้หรือไม่ ?

เราจะทำอย่างไรดี เพื่อไม่ให้ทั้งสอง class ผูกติดกัน ?

จากแนวคิดของ DIP ก็คือ ต้องสร้าง Abstraction Layer ขึ้นมาระหว่างสอง class
ซึ่งผมทำการสร้าง interface MessageService ขึ้นมา
ทำการออกแบบได้ดังรูป UML
Screen Shot 2557-10-28 at 5.49.33 PM

สามารถเขียน code ของ Abstraction Layer ได้ดังนี้
12345
public interface MessageService {
public void send();
 
}
view rawMessageService.java hosted with ❤ by GitHub
ทำการแก้ไข class Email ดังนี้
12345
public class Email implements MessageService {
 
public void send() {
}
}
view rawEmail2.java hosted with ❤ by GitHub
และแก้ไข class Notification ดังนี้
12345678910111213
public class Notification {
MessageService messageService;
public Notification() {
messageService = new Email();
}
public void notifyPromotion() {
messageService.send();
}
 
}
view rawNotification2.java hosted with ❤ by GitHub
คำอธิบาย
ทำการเพิ่ม interface ชื่อว่า MessageService ซึ่งเป็น Abstraction layer
เพื่อให้ class Notification ทำการส่งข้อความผ่าน method หรือ operation ที่อยู่ใน Abstraction Layer
ซึ่งลดการผูกมัดระหว่าง class Notification และ Email ลงไป
เป็นไปตามแนวคิดของ Dependency Inversion Principle นะ
มันก็ดูดีขึ้นนะ !!

แต่ว่ายังมีปัญหาอยู่ใช่ไหม ?

คำถาม
ตรงไหนล่ะ ?
คำตอบ
สังเกตไหมว่า มีการสร้าง object ของ class Email ใน class Notification ?
ซึ่งนั่นคือ ปัญหา เราต้องย้ายการสร้าง object ของ class Email ออกมาซะ
เพราะว่า code มันผูกติดเกินไป และ class Notification ก็ไม่ได้มีหน้าที่สร้าง object ของ class Email ด้วยนะ
คำถาม
แล้วย้ายออกมาอย่างไรล่ะ ?
คำตอบ
ก็ใช้แนวคิด Dependency Injection (DI) เข้ามาช่วยไงล่ะ
ถ้าถามว่ามีวิธีการอื่นๆ ไหม ตอบได้เลยว่ามี ดังรูป
Screen Shot 2557-10-28 at 5.01.26 PM

แล้ว Dependency Injection หรือ DI มันคืออะไรล่ะ ?

คือวิธีการเตรียมหรือส่ง object ที่ต้องการใช้งานเข้าไป
โดยไม่ต้องทำการสร้าง object นั้นขึ้นมาใช้เอง
ซึ่งมันช่วยลดการผูกมักภายใน code ของระบบ
จากตัวอย่างใน class Notification นั้นมีการสร้าง object ของ class Email อยู่
ดังนั้น สิ่งที่เราต้องการคือใน class Notification จะไม่มีการสร้าง object ของ class Email
แต่เราจะส่งเข้าไปให้เลย หรือ เรียกแบบทั่วไปว่า การฉีด หรือ inject object ของ class Email เข้าไป
โดยวิธีการ inject object เข้าไปนั้น มีอยู่ 3 แบบ คือ
  1. Constructor Injection
  2. Property Injection
  3. Method Injection
มาดูตัวอย่างการ inject ในแต่ละแบบกันดู เพื่อความเข้าใจมากยิ่งขึ้น

แบบที่ 1 Constructor Injection

เป็นรูปแบบที่ง่ายสุดๆ และมักจะถูกใช้งานกันมาก
โดยจะทำการส่ง object ที่ต้องการผ่านไปยัง constructor
ดังนั้นใน class Notification สามารถแก้ไขได้ดังนี้
12345678910111213
public class Notification {
private MessageService messageService;
public Notification(MessageService messageService) {
this.messageService = messageService;
}
public void notifyPromotion() {
messageService.send();
}
 
}
view rawNotification3.java hosted with ❤ by GitHub
ข้อดีของวิธีนี้คือ มันง่ายมาก
ลดงานที่ class Notification ต้องทำลงไป นั่นคือการสร้าง object
ทำให้ code ของ class Notification กับ Email ไม่ผูกติดกันครับ
ทำให้ดูแลรักษาง่ายขึ้นนะ ว่าไหม ?

แบบที่ 2 Property Injection

มันคือการสร้าง setter/getter method มาเพื่อกำหนดค่าของ object ที่เราต้องการใช้งาน
ใช้วิธีการนี้เมื่อ dependency object เหล่านั้น ไม่ใช่ตัวหลักในการทำงาน
ลองคิดดูว่า ถ้ามี dependency object  จำนวนมาก ถ้าจะใส่ใน constructor ทั้งหมด
ไม่น่าจะเหมาะสมเพราะว่าจะเกิด Code Smell ขึ้นมา นั่นคือจำนวน parameter ของ method เยอะเกินไป
ดังนั้นใน class Notification สามารถแก้ไขได้ดังนี้
123456789101112131415
public class Notification {
 
private MessageService messageService;
 
public void setMessageService(MessageService messageService) {
this.messageService = messageService;
}
 
public void notifyPromotion() {
if (this.messageService != null) {
messageService.send();
}
}
 
}
view rawNotification4.java hosted with ❤ by GitHub

แบบที่ 3 Method Injection

ทำการส่ง dependency object มายัง method ที่ทำงานเลย
ทำให้แต่ละ method มี parameter ที่แตกต่างกันตามความต้องการไปเลย
นั่นคือส่งเป็น parameter ดังนี้
1234567
public class Notification {
 
public void notifyPromotion(MessageService messageService) {
messageService.send();
}
 
}
view rawNotification5.java hosted with ❤ by GitHub

โดยสรุปแล้ว

มาถึงตรงนี้น่าจะพอทำให้เห็นว่า
Dependency Inversion Principle และ Dependency Injection มันแตกต่างกันตรงไหน
มันส่งเสริมมีเข้ามาช่วยเราแก้ไขปัญหาอะไร
โดยที่เป้าหมายของทั้งสอง คือ ต้องการ
  • ทำให้ code ไม่ผูกมัดกัน
  • ทำให้เราสามารถ reuse ส่วนการทำงานต่างได้ง่าย
  • ทำให้เราสามารถเพิ่มความสามารถต่างๆ เข้าไปได้ง่าย
การใช้ DI นั้นวิธีที่แนะนำ เพราะว่าง่ายที่สุดคือ Constrictor Injection
แล้วจึงนำอีกสองวิธีหลังมาใช้ เพื่อเสริมการทำงานต่อไป
เช่น object หลักให้ส่งมายัง constructor ส่วน object เสริมให้ใช้จาก 2 ตัวที่เหลือ เป็นต้น
แต่เชื่อเถอะว่า code ที่ developer ส่วนใหญ่เขียนขึ้นมามักจะผูกมัดกันแบบแน่นๆ
เพราะว่า มันง่าย สะดวก แต่ผลของมันก็น่าจะสะท้อนออกมาให้เห็นและแก้ไขกันอยู่โดยตลอด
แล้วเรายังจะทำแบบเดิมๆ กันอยู่อีกหรือ ?

Article by Somkiat Puisungnoen

To be Craftmanship

ไม่มีความคิดเห็น:

แสดงความคิดเห็น