เกี่ยวกับ Spring Dependency Injection
Dependency Injection (DI) คืออะไร
เป็นเทคนิคนึงในการทำ loose coupling dependencyให้กันระหว่าง class อย่างเช่น A class ต้องการเรียก B class , ตัว B class จะถึงว่าเป็น dependency ของ A class. ส่วนนึงก็เพื่อแยกการทำงานให้เล็กลง และ เมื่อต้องการ fuction ที่ตัวเองไม่มีก็ทำการฉีด class อื่นเข้ามา การทำแบบนี้จะทำให้การทำ unit testing ง่ายขึ้นอีกด้วย เพราะเราจะฉีด mock/stub เข้าไปแทนได้เลย เราจึง focus ไปที่จุดที่ต้องการ test ได้อย่างเดียวเลย
Injection types
ของ spring boot ในทางปฏิบัติมันจะมีอยู่ 3 แบบหลักๆ
- Field-based dependency injection
- Constructor-based dependency injection
- Setter-based dependency injection
Note: ใน document มันมีให้ดูแค่ 2 แบบ
Field-based dependency injection
ผมเดาว่าเป็นท่าทั่วไปที่คนส่วนใหญ่น่าจะใช้กันคือการใช้ annotation @Autowired
ซึ่งก็จะค่อนข้างดูง่าย เรียบร้อยดี ลด boilerplate
public class ProfileController {
@Autowired
private ResponseFactory responseFactory;
@Autowired
private ProfileService profileService;}
Constructor-based dependency injection
ใช้เมื่อเราต้องการ dependencies ที่เป็น Required และต้องการไม่ให้มันเป็น null
วิธีนี้ ถ้าเทียบกับ แบบ Field-based ก็ไม่ได้ มีข้อดีแตกต่างไปกว่ากันมากนัก แต่ก็เกิดเป็น boilerplate ที่ต้องเขียนทุกครั้ง
public class ProfileController {
private final ResponseFactory responseFactory;
private final ProfileService profileService; @Autowired
public ProfileController(ResponseFactory responseFactory, ProfileService profileService) {
this.responseFactory = responseFactory;
this.profileService = profileService;
}
}
แต่ตั้งแต่ spring boot 1.4 ขึ้นไปเราไม่จำเป็นต้องใส่ @Autowired ก็ได้
public class ProfileController {
private final ResponseFactory responseFactory;
private final ProfileService profileService;
public ProfileController(ResponseFactory responseFactory, ProfileService profileService) {
this.responseFactory = responseFactory;
this.profileService = profileService;
}
}
Setter-based dependency injection
ใช้เมื่อเราต้องการ dependencies ที่เป็น Optional เราจะประกาศ setter method ของมา แล้วค่อยเอา @Autowired ใส่เข้าไป ท่าแบบนี้จะทำให้เรา ฉีด bean เข้าไปอีกครั้งได้
public class ProfileController {
private ResponseFactory responseFactory;
private ProfileService profileService; @Autowired
public void setResponseFactory(ResponseFactory responseFactory)
{
this.responseFactory = responseFactory;
} @Autowired
public void setProfileService(ProfileService profileService) {
this.profileService = profileService;
}
ปัญหาที่พบบ่อย
ปัญหาที่เราจะเจออย่างนึงจากการใช้ @Autowired คือ Autowired tower นั้นเอง(ตั้งชื่อเอง)
public class EmailController {
@Autowired
private OtpService otpService;
@Autowired
private TicketService ticketService;
@Autowired
private ResponseFactory responseFactory;
@Autowired
private EmailOtpService emailOtpService;
@Autowired
private ProfileService profileService;
@Autowired
private ContactService contactService;
@Autowired
private DeviceIdentityService deviceIdentityService;
@Autowired
private CounterService counterService;
}
ลองแก้ให้กลายเป็นแบบ constructure-base
public class EmailController {
private final OtpService otpService;
private final TicketService ticketService;
private final ResponseFactory responseFactory;
private final EmailOtpService emailOtpService;
private final ProfileService profileService;
private final ContactService contactService;
private final DeviceIdentityService deviceIdentityService;
private final CounterService counterService;
public EmailController(OtpService otpService, TicketService ticketService, ResponseFactory responseFactory, EmailOtpService emailOtpService, ProfileService profileService, ContactService contactService, DeviceIdentityService deviceIdentityService, CounterService counterService) {
this.otpService = otpService;
this.ticketService = ticketService;
this.responseFactory = responseFactory;
this.emailOtpService = emailOtpService;
this.profileService = profileService;
this.contactService = contactService;
this.deviceIdentityService = deviceIdentityService;
this.counterService = counterService;
}
}
ก็ยังดูยาวย้วยอยู่ดี ไม่ได้ช่วยอยู่ดี
วิธีแก้จริงๆที่ควรจะเป็น คือ การ refactor เพื่อแยกการทำงานกัน การที่มันพยายามเรียก service หลายตัว แปลว่ามันอาจจะกำลังพยายามทำงานหลายอย่างอยู่ อาจจะใช้ SOLID principle ในการอ้างอิงก็ได้ จะทำให้เราดูแล แก้ไข class ได้เฉพาะจุด ยืดหยุ่น และง่ายมากขึ้น แม้ว่าการใช้แบบ Field-based (Autowired) มันสะดวกดีแต่ก็มีจุดในการซ่อนปัญหาอยู่ เช่นเราอาจจะกันให้เป็น Immune object ไม่ได้ หรือเมื่อเป็น constructure-based มันจะเห็นชัดเลยว่ามันไม่ใช่เรื่องธรรมดาแล้ว ที่มี dependencies ถึง 8 ตัว และมันจะยังเด่นชัดขึ้นด้วยว่า dependency ตัวไหน เราให้เป็น required(constructure) ตัวไหนเป็น optional(setter)
ใน IDE บางตัวอย่าง intelliJ สำหรับ code ที่ใช้การ Autowired มันจะมีขึ้นเตือนว่า
Field injection is not recommended
Inspection info: Spring Team recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”.
ซึ่งมันเป็นสิ่งที่ spring เองก็แนะนำ ตาม link นี้ ในหัวข้อว่า “Constructor-based or setter-based DI?” สรุปคร่าวๆได้ว่า การที่เรารู้อยู่แล้วว่า class นั้นๆจะต้องใช้ constructor argument อะไรบ้าง การประกาศให้ชัดเจนแบบ constructor-based จะเป็นเรื่องที่เหมาะสมกว่า ส่วน setter-based ส่วนมากทีใช้กับ dependency แบบ optional ใน class แต่กลายเป็นว่า เวลามี class เรียกใช้มันต้องต้อง set เข้าไปทุกครั้งเพื่อหลีกเลี่ยง ไม่ให้พัง หรือใช้ในอีกวิธีคือ re-inject bean กลางทาง ก็เลือกให้เหมาะสม
ทั้งนี้ทั้งนั้น มันก็ไม่ได้เป็นปัญหาแบบ technical ที่เด่นชัดหรือ พิเศษอะไร แต่มันจะทำให้เราเข้าใจ code และ แบ่งแยกการทำงาน อาจจะมองว่าเป็นเรื่องของ practice ก็ได้
ผิดตรงไหนบอกกันได้นะ ผมเขียนตามความเข้าใจ
Bonus: Lombok @RequiredArgsConstructor
ด้วยท่านี้เราก็จะได้ เป็นแบบ constructure ที่ลด boilerplate ไม่ต้อง autowired โดยประกาศให้เป็น final ทีนี้เราก็จะได้ immune object แต่ code เราจะดูสะอาดอ่านง่ายขึ้น
@Controller
@RequiredArgsConstructor
public class ProfileController {
private final ResponseFactory responseFactory;
private final ProfileService profileService;}