Basic Spring Part 1 - Spring framework & Dependency Injection

Basic Spring Part 1 - Spring framework & Dependency Injection

ผู้หญิงคนนึงเคยบอกว่าชอบฤดูใบไม้ผลิเพราะมันสวยงามให้ความรู้สึกได้เริ่มต้นใหม่

ผมเขียนเกี่ยวกับ Spring ไว้หลายตอน คุณสามารถกด Link ด้านล่างเพื่ออ่านที่เกี่ยวกับ Spring ตอนต่างๆได้เลย

รอบนี้มาเขียนเรื่องเกี่ยวกับ Spring Framework ซึ่งน่าจะลากยาวได้หลายตอนอยู่ โดยจะพูดแต่เรื่องพื้นฐาน แล้วก็ทดลองใช้งานเบื้องต้น ซึ่งน่าจะเขียนไปเรื่อยๆจนเขียนต่อ Database ทำ Web application server ได้เลย (แบบง่ายๆ) โดยที่เขียนเกี่ยวกับเรื่องนี้เพราะมีน้องในบริษัทหลายคนกำลังจะใช้ Spring framework ก็เลยถือโอกาสเขียน Blog ไปด้วยเลย เผื่อใครต้องเขียน Spring framework จะได้มาร่วมมั่ว ร่วมลองไปด้วยกัน

Spring framwork คืออะไร

Spring framework คือ framework นึงในภาษา Java Framework คือโครงหรือรูปแบบในการเขียน Program โดยหากเราทำตามรูปแบบที่เขากำหนดให้จะทำให้เราสามารถพัฒนางานของเราได้เร็วขึ้น เช่น ถ้าเราจะเขียน Web application server เราอาจะต้องเขียนขารับ Http request โดยหากเรา Implement เองทั้งหมด เราต้อง Implement ตั้งแต่แกะ Http request แล้วก็ Parser เก็บข้อมูลส่วน Header แยกส่วน Body อาจจะต้องด้วยว่าเป็น Method อะไร จากนั้นจะต้องไป Map กับ Endpoint ไหนอีก แค่พูดมาแค่นี้ก็เหนื่อยละ ซึ่งงานพวกนี้มันต้องทำอยู่แล้ว เขาเลยสร้าง Framework ขึ้นมาเพื่อจัดการสิ่งนี้ แล้วเราไปเขียน Code ตามโครงสร้างของเขาเขาจะจัดการส่วนที่ทำซ้ำให้เรียบร้อย จากนั้นส่งตัว Header กับ Body ที่ใช้งานง่ายๆมาให้ ซึ่งการทำแบบนี้ทำให้เราพัฒนา Application ได้ไว แล้วลงรายละเอียดเฉพาะส่วนที่เป็น Business Logic อย่างเดียว โดยตัว Spring framework นั้นมีส่วนช่วยเรามากมายไม่ว่าจะส่วนรับ Http request ส่วนติดต่อ Database ส่วน Security

DI : Dependency Injection

หากดูที่รูปจะเห็นส่วน Spring core ที่เป็นส่วน DI ซึ่งส่วนนี้เป็น Concept เกี่ยวกับการเขียน Program โดยถ้าเข้าใจ Concept นี้แล้วจะเข้าใจ Anotation บางตัวเลย อ่า พอมาถึงต้องอธิบายจะอธิบายยังไงดีล่ะ งั้นลองดู Code ละกัน

1
2
3
4
5
6
7
8
9
10
11
12

public class SummaryService {
public Double summaryAccount(AccountService accountService) {

List<Account> accounts = accountService.getAllAccount();
Double sum = 0;
for (Account account : accounts) {
sum += account.money;
}
return sum;
}
}

จาก Code ไม่มีอะไรมากก็คือทำการรวมเงินครับโดยใช้ตัว AccountService ที่ส่งเข้ามาใน Method : summaryAccount เพื่อดึงค่า accounts ทั้งหมดออกมาจากนั้น For Loop รวมค่าแล้ว Return ออกไป

คราวนี้ลองดูตรง AccountService ที่ส่งเข้ามาครับจะเห็นว่า Method : summaryAccount นั้นจะทำงานได้นั้นต้องมี AccountService ถ้าไม่มีจะทำงานไม่ได้ ดังนั้น AccountService มันเลยเป็น Dependency

ตอนนี้เราได้คำว่า Dependency แล้ว คำต่อไปคือ Injection ก็คือการฉีดเข้าไปเหมือนดังภาพเลย

Dependency Injection ก็คือการฉีดสิ่งที่เป็น Dependency เข้าไป ถ้าในตัวอย่าง คือการฉีด AccountService เข้าไปใน Method : summaryAccount นั่นเอง

Dependency Injection นั้นมีหลายระดับครับ ตามความยืดหยุ่น โดยมีคนเคยจัดไว้ https://medium.com/@samueleresca/dependency-injection-overview-31b757a8dd51 มี 3 ระดับ

  • Constuctor Injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class SummaryService {

private AccountService accountService;

private SummaryService(AccountService accountService) {
this.accountService = accountService;
}

public Double summaryAccount() {

List<Account> accounts = this.accountService.getAllAccount();
Double sum = 0;
for (Account account : accounts) {
sum += account.money;
}
return sum;
}
}

// Main class file

public class Test {
public static void main() {
AccountService accountService = new AccountService();
SummaryService summaryService = new SummaryService(accountService);
System.out.println(summaryService.summaryAccount()) ;
}
}

จะเห็นว่า AccountService นั้นจะถูก Inject ผ่าน Constructor เท่านั้น ดังนั้นมันจะไม่สามารถเปลี่ยน Account service ได้อีกหลังจากสร้าง Object ได้อีกแล้ว ซึ่งมีข้อดีคือ Object นี้เราจะรู้ว่ามันมี Account service อะไรอยู่ข้างใน สามารถ Track การทำงานของมันได้ไม่ลำบาก (ถ้ารู้ว่าอะไรใส่เข้าไปในตอนสร้างก็จะรู้ว่าตัวที่ทำงานคือตัวไหน) แต่มันก็มีข้อเสียคือมันเปลี่ยนแปลงไม่ได้แล้ว

  • Getter Setter Injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class SummaryService {

private AccountService accountService;

public Double summaryAccount() {

List<Account> accounts = this.accountService.getAllAccount();
Double sum = 0;
for (Account account : accounts) {
sum += account.money;
}
return sum;
}

public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}

public AccountService getAccountService() {
return this.accountService;
}
}

// Main class file

public class Test {
public static void main() {
AccountService accountService = new AccountService();


SummaryService summaryService = new SummaryService();

summaryService.setAccountService(accountService);
System.out.println(summaryService.summaryAccount());

}
}

อันนี้จะยืดหยุ่นกว่า Constructor นิดนึงคือสามารถเปลี่ยนแปลงได้โดยใช้ Setter ทำการ set : AccountService เข้าไป โดยสามารถเปลี่ยนได้ตามต้องการ แต่ก็ยากระดับนึงคือต้องสั่ง Setter ทุกครั้ง และอาจมีปัญหาในเรื่อง Concurrent ตอน Setter พร้อมกันหลายๆ Thread

  • Method Injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SummaryService {
public Double summaryAccount(AccountService accountService) {

List<Account> accounts = accountService.getAllAccount();
Double sum = 0;
for (Account account : accounts) {
sum += account.money;
}
return sum;
}
}

// Main class file

public class Test {
public static void main() {
AccountService accountService = new AccountService();

SummaryService summaryService = new SummaryService();

System.out.println(summaryService.summaryAccount(accountService));
}
}

แบบนี้จะยืดหยุ่นสุดคือการส่งเข้ามาเป็นตัวแปรทำให้ แต่ก็แลกมาด้วยการ Track SummaryService เรียกใช้ AccountService ไหน

แล้วทำไมต้องทำ Dependency Injection ล่ะ

จาก Code ด้านบนจะเห็นวิธีการทำ DI ไปแล้วแต่คำถามคือทำไปทำไม ทำไปเพื่ออะไร จริงๆผมควรจะพูดเรื่องนี้ก่อน DI แต่เขียนแบบนี้ก่อนก็ดีอีกแบบเพราะมันทำให้คนสงสัยพอมารู้คำตอบจะได้เชื่อมโยงได้ แตกต่างจากการเล่าแบบตรงไปตรงมาที่อาจจะผ่านมาแล้วผ่านไปเลย

ลองดู Code ด้านล่างนี้ที่เป็น Code ที่ดึงเอาทำการ summary ค่า ของ SummaryService

1
2
3
4
5
6
7
8
9
10
11
public class SummaryService {
public Double summaryAccount(AccountService accountService) {

List<Account> accounts = accountService.getAllAccount();
Double sum = 0;
for (Account account : accounts) {
sum += account.money;
}
return sum;
}
}

ลองนึกภาพตามว่า AccountService คือ Service ที่ไปเอาข้อมูล Account ออกมาใช่ไหมครับ ถ้าวันนึงข้อมูล Account เก็บไว้ Database เราก็ต้องเขียน AccountService ด้วยการเขียนเชื่อมต่อ Database ซึ่งเขียน code หน้าตาประมาณนี้ แล้วมันก็ต้องเปลี่ยนตัว class Test ที่มี main method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AccountService {

private SQLConnection sqlConnection;

public AccountService(SQLConnection sqlConnection) {
this.sqlConnection = sqlConnection;
}

public List<Account> getAllAccount() {
return sqlConnection.query("SELECT * From account");
}
}

// Main class file

public class Test {
public static void main() {
AccountService accountService = new AccountService(new SQLConnection("path/to/db"));

// Create summary service
SummaryService summaryService = new SummaryService();


// use account service 1
System.out.println(summaryService.summaryAccount(accountService));
}
}

แล้วอยู่มาวันนึงเราแก้ไปเก็บข้อมูลที่ File เราก็ต้องแก้ Code เป็น

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AccountService {

// other variable
private String filePath;

public List<Account> getAllAccount() {
return MyAppUtil.fileToListAccount(new File(this.filePath));
}
}

// Main class file

public class Test {
public static void main() {
AccountService accountService = new AccountService("/path/to/account/file"));

// Create summary service
SummaryService summaryService = new SummaryService();


// use account service 1
System.out.println(summaryService.summaryAccount(accountService));
}
}

จากตัวอย่างจะเห็นว่าเราไม่ได้แก้ Code ส่วนที่ SummaryService เลยมีแต่ Code ส่วน AccountService กับ Class Test ที่เป็นส่วนเรียก ซึ่งนี่แหละคือข้อดีของทำ Dependency injection เพราะตัว dependency นั้นมีการเปลี่ยนแปลงกระบวนการสร้างอะไรก็ตามแต่ แต่ class ที่ใช้ Dependency ผ่านการใช้วิธี Injection จะไม่ได้รับผลกระทบเลยเพราะตัว Class ที่ใช้ไม่รู้ว่า class depency นั้นสร้างยังไง ต้องต่อ database หรือ ใช้ file แต่ถ้าเราเขียน SummaryService แบบนี้

1
2
3
4
5
6
7
8
9
10
11
12
public class SummaryService {
public Double summaryAccount() {
AccountService accountService = new AccountService(new SQLConnection("/path/to/database"));

List<Account> accounts = accountService.getAllAccount();
Double sum = 0;
for (Account account : accounts) {
sum += account.money;
}
return sum;
}
}

ถ้ามีการเปลี่ยนตัว AccountService เป็น File เราก็ต้องแก้ตัว SummaryService เป็น

1
2
3
4
5
6
7
8
9
10
11
12
public class SummaryService {
public Double summaryAccount() {
AccountService accountService = new AccountService("Nqxxquehqdkogfq");

List<Account> accounts = accountService.getAllAccount();
Double sum = 0;
for (Account account : accounts) {
sum += account.money;
}
return sum;
}
}

จากตัวอย่างจะเห็นว่าเราต้องแก้ SummaryService อันนี้เป็นตัวอย่างแบบแง่ดีนะครับว่าเรารู้ค่าพวก pathToFile ถ้าโชคร้ายว่า pathToFile มันต้องไปหาที่อื่นอีก บอกเลยว่าต้องแก้ตรง SummaryService จะต้องแก้เยอะกว่านี้อีก คราวนี้จะเห็นประโยชน์ของทำ Dependency Injection แล้วนะครับ

สรุป

ตอนนี้เราได้รู้แล้วว่า Spring framework คืออะไร แล้วก็เข้าใจ Concept : Dependency injection ที่ต่อจากนี้เราจะได้ใช้มันไปเรื่อยๆตลอดการใช้ Spring framework ในตอนหน้าเราจะเริ่มสร้าง Project : Spring Framework แล้วก็ลองใช้ Spring framework กัน

เพลงประกอบการเขียน Code

เพลงที่โผล่เข้ามาในหัวทุกทีที่เจอผู้หญิงที่ชอบแล้วก็ไปจบที่ “ขอเชื่อในความรักสักหน่อย แม้รักนั้นอาจทำให้ช้ำใจ” ซึ่งหลายคนอาจบอกว่าเฮ้ยจบสวยแน่นอน แต่จากการเชื่อในความรักสักหน่อยนี่ช้ำใจทุกที จนตอนนี้ไม่ค่อยกล้าเชื่อในความรัก (ความรู้สึกอีกแล้ว)