เกี่ยวกับ Spring Dependency Injection

Theerut Bunkhanphol
3 min readJan 10, 2020

--

Photo by Sara Bakhshi on Unsplash

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 กลางทาง ก็เลือกให้เหมาะสม

IntelliJ : optional + enter

ทั้งนี้ทั้งนั้น มันก็ไม่ได้เป็นปัญหาแบบ 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;
}

--

--

Theerut Bunkhanphol
Theerut Bunkhanphol

Written by Theerut Bunkhanphol

(ธีรุตม์ บุญคั้นผล) As a poor developer who likes coffee, planting, Rock music, Anime and Game

No responses yet