Skip to main content

Inversion of Control และ Dependency Injection

Inversion of Control (IoC) คืออะไร

IoC เป็นทฤษฏีที่ว่าด้วย การลดความผูกมัด (dependency) กันในระหว่าง module เพื่อให้ application ของเราแก้ไข (maintain) ต่อเติม (extensible) หรือทดสอบ (test) ได้ง่ายขึ้น

ซึ่งเอาจริงๆ IoC เป็นอะไรที่ทำให้เราสับสนและงุนงงมากๆ ว่ามันคืออะไร หลายๆ คนจึงยกให้ว่า IoC คือ Dependency Injection (DI) ซึ่งจริงๆ มันก็ไม่ถูกซะทีเดียว 

Dependency คืออะไร

Dependency คือการผูกมัดที่เกิดขึ้นในระบบ เมื่อ module นึงมีการเรียกใช้อีก module นึงด้วยการอ้างอิง (reference) ตรงๆ

แล้วอะไรที่เรียกว่าการ อ้างอิง (Reference) แบบตรงๆ 


อย่างภาพ diagram ด้านบน class LogEngine มีการเรียกใช้ ConsoleLog โดยตรง ซึ่งมองผ่าน diagram อาจจะไม่เห็นภาพลองดู code กัน
public class ConsoleLog {

   public void openLog(){
       //do something to open log
   }

   public void log(String message){
       //do something to log
   }

   public void closeLog(){
       //do something to close log
   }

}

public class LogEngine {

   private ConsoleLog log;

   public LogEngine(){
       log = new ConsoleLog(); //this is dependency
   }

   public void log(String message){
       log.openLog();
       log.log(message);
       log.closeLog();
   }

}

จาก code จะเห็นว่าเรามี class LogEngine เป็นตัวกลางในการพ่น log ลง Console (ConsoleLog) เชื่อว่าหลายคนก็คงงงว่ามันมีการผูกมัดอะไรตรงไหน ข้อเสียมันคืออะไร ทำไมถึงต้องเป็นเรื่องที่ต้องกังวล

จุดที่มีการผูกมัด อยู่ที่ class LogEngine ที่มีการเรียกใช้ class ConsoleLog โดยตรง ทำให้เมื่อมีการสร้าง object ของ class LogEngine จะต้องสร้าง object ConsoleLog ด้วยเสมอ ตรงนี้คือส่วนที่ทำให้ 2 class ผูกติดกันอย่างแน่นหนา (Tightly Coupling)

 ข้อเสียการผูกมัดแบบนี้ จะทำให้การแก้ไขหรือต่อขยายในภายภาคหน้านั้นเป็นไปได้ยาก ยกตัวอย่างเช่นเราต้องการขยายระบบ log ให้สามารถบันทึกลงไฟล์ (FileLog) หรือ ฐานข้อมูล (DatabaseLog) เชื่อว่าหลายคนยังไม่เห็นภาพ เรามีตัวอย่างให้ดู


import java.io.*;

public class FileLog {

   private BufferedWriter logWriter;
   private String logPath;

   public FileLog(String logPath){
       this.logPath = logPath;
   }

   public void openLog() throws IOException{
       try {
           logWriter = new BufferedWriter(new FileWriter(logPath));
       }catch (IOException ex){
           System.err.printf("Can't initial log file in this path [%s] \n", logPath);
           throw ex;
       }
   }

   public void log(String message)throws IOException{
       logWriter.write(message);
       logWriter.newLine();
   }

   public void closeLog() throws IOException{
       logWriter.flush();
       logWriter.close();
   }
}
class FileLog ดูเหมือนจะไม่มีอะไรแปลกประหลาด แต่จะเกิดปัญหาเมื่ือนำไปใช้กับ class LogEngine ซึ่งเป็นตัวกลางในการจัดการ log อาจจะยังเห็นภาพไม่ชัด ลองไปดูในตัวอย่าง code ด้านล่าง
import java.io.IOException;

public class LogEngine {

   private ConsoleLog consoleLog;
   private FileLog fileLog;

   public LogEngine(){
       consoleLog = new ConsoleLog();
   }

   public LogEngine(String logPath){
       fileLog = new FileLog(logPath);
   }

   public void log(String message) throws IOException{
       if(consoleLog != null) {
           consoleLog.openLog();
           consoleLog.log(message);
           consoleLog.closeLog();
       }else if(fileLog != null){
           fileLog.openLog();
           fileLog.log(message);
           fileLog.closeLog();
       }
   }

}
จาก code ด้านบนจะเห็นว่าทุกครั้งที่มีการเพิ่มประเภทของ log เราต้องแก้ไข code ของ class LogEngine ที่เป็นเช่นนั้นเพราะว่าโครงสร้างของ log ที่เราสร้างมาตั้งแต่แรกนั้นมีการผูกมัด (dependency) ที่แน่นหนา ทำให้การแก้ไขหรือการขยับขยายเป็นไปได้ยาก

วิธีแก้ปัญหา

ตามหลักการของ D (Dependency Inversion) ในหลักการพัฒนา application ในรูปแบบ SOLID ของ Robert C. Martin ได้กล่าวว่า

High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.

แปลเป็นไทยแบบง่อยๆ ก็ได้ใจความประมาณว่า

module ในระดับสูงไม่ควรที่จะผูกมัดกับ module ในระดับล่าง ทั้งสอง module ควรจะมีความสัมพันธ์กันผ่าน abstractions
abstractions ไม่ควรจะมีความสัมพันธ์ที่ลงลึกไปถึงรายละเอียด แต่รายละเอียดจะไปผูกกับ abstractions เอง

จากหลักการด้านบนนั้นหมายความว่า การจะลดการผูกมัดระหว่าง module ต้องใช้ abstraction เข้ามาเป็นตัวกลางสำหรับการเรียกใช้ระหว่าง module


เชื่อว่าหลายคนยังคงงงอยู่ ทำไมต้องเอา abstraction มาคั่นกลาง ให้เราลองคิดตามกันดู สมมุติว่าเรามี class 2 ตัว X และ Y โดยที่ X นั้นเรียกใช้ Y ซึ่งมันก็เป็นไปตามกลไกปกติ ถ้าหาก X จะใช้อะไรบางสิ่งใน Y ไม่ว่าจะเป็น method หรือ properties ยังไงก็ต้องเรียก Y แต่มีคำถามว่า X จำเป็นรู้ถึงรายละเอียดในการทำงานของ Y สำหรับการเรียกใช้ Y หรือไม่ 

จริงๆ แล้ว X ไม่มีความจำเป็นต้องรู้ลงลึกไปถึงรายละเอียดของ Y ไม่ต้องรู้เลยว่า Y ทำอะไรยังไงถึงได้ผลลัพธ์ที่ X ต้องการออกมา ดังนั้นจึงมีแนวคิดที่ให้ abstraction เป็นตัวกลางระหว่าง X และ Y โดย abstraction นั้นจะมีรายละเอียดว่า ถ้า X ต้องการใช้ Y ต้องทำยังไง ต้องส่ง argument อะไร



ในรูปด้านบน Y นั้นได้ implement interface มาจาก I และ X ได้เรียกใช้ Y ผ่าน I โดยที่ X นั้นไม่รู้เลยว่ากำลังเรียกใช้ Y อยู่ แค่รู้ว่ากระทำผ่านตัวกลางอย่าง I

จากที่กล่าวไว้ด้านบนเราลองมาดูในตัวอย่างจริงๆ กันว่าจะมีหน้าตาเป็นยังไงดัง diagram ด้านล่างนี้


คำว่า abstractions ในความหมายข้างบนหมายถึง interface ในภาษา Java โดย class LogEngine จะไม่ได้เรียกใช้ class ConsoleLog หรือ FileLog ตรงๆ แต่จะเรียกใช้ผ่านตัวกลางอย่าง interface ILog แทน และนี่คือหน้าตาของ code เมื่อเราได้มีการปรับปรุง
public interface ILog {
   void openLog() throws Exception;
   void log(String message) throws Exception;
   void closeLog() throws Exception;
}
public class LogEngine {

   private ILog log;

   public LogEngine(){
       log = new ConsoleLog(); //dependency
   }

   public LogEngine(String logPath){
       log = new FileLog(logPath); //dependency
   }

   public void log(String message) throws Exception{
       log.openLog();
       log.log(message);
       log.closeLog();
   }
}
แต่ถึงเราจะออกแบบ class ของเราให้มีโครงสร้างที่ตามหลักการที่จะลดการผูกมัดในระบบแล้ว และทำให้ method log() ใน class LogEngine นั้นดูสะอาดขึ้น แต่ว่าเมื่อเรามาลองดู code กลับพบว่ายังมีการผูกมัดกันระหว่าง LogEngine และ ConsoleLog, FileLog ยังคงอยู่เหมือนเดิม ที่ยังเป็นเช่นนั้นเพราะเราเรียกใช้ผิดวิธีอยู่ เพราะการสร้าง object ConsoleLog และ FileLog ภายใน class ของ LogEngine เป็นจุดที่ทำให้เกิดการผูกมัดกัันในระบบ ดังนั้นการแก้ไขก็คือห้ามมีการสร้าง object ที่ต้องการใช้ขึ้นมาตรงๆ แต่ให้ทำการฉีด (inject) object ที่จะใช้เข้ามาแทน และวิธีนี้นี่เองที่เค้าเรียกว่า Dependency Injection (DI) 

Dependency Injection (DI)

DI เป็นเทคนิคที่ว่าด้วยการจัดหาหรือการเตรียม object ที่มีการผูกมัด (dependency object) ให้กับ object ที่มีการเรียกใช้ โดย object ที่เรียกใช้ไม่จำเป็นต้องสร้าง dependency object ขึ้นมาเอง เพราะ dependency object จะถูกฉีดเข้าไปให้เอง



DI ก็เป็นเพียง 1 ในวิธีที่ใช้แก้ปัญหาการผูกมัดกันภายในระบบตามหลักการของ IoC ซึ่งวิธีการแก้ปัญหานั้นมีหลายวิธี (ดังรูปด้านบน) แต่เราหยิบยก DI ขึ้นมากล่าวถึงเพียงอย่างเดียว เพราะหลักการของ DI นั้นเป็นที่นิยมและถูกนำไปสร้างเป็น library หรือ framework ในหลายๆ ภาษา โดยวิธีการของ DI นั้นยังแตกแยกย่อยออกไปอีก 3 วิธี

  • Constructor Injection 
  • Setter Injection 
  • Method Injection 

Constructor Injection

เป็นวิธีการฉีด dependency object ผ่านทาง constructor ของ class ที่จะเรียกใช้ dependency object โดย constructor จะรับ argument ที่เป็น interface ของ class ที่เป็น dependency object

 ตามข้อกำหนดด้านบน ในกรณีนี้เราจะต้องแก้ class LogEngine ให้มี constructor ที่มี argument เป็น interface ของ ILog ตาม code ด้านล่าง
public class LogEngine {

   private ILog log;

   public LogEngine(ILog log){
       this.log = log;
   }

   public void log(String message) throws Exception{
       log.openLog();
       log.log(message);
       log.closeLog();
   }
}
จาก code ด้านบนจะทำให้เรา class ของ LogEngine จะไม่ได้มีการสร้าง object ของ ILog โดยตรงแล้ว แต่จะรับมาจากภายนอกแทน ซึ่งวิธีนี้เป็นการฉีด object ผ่าน constructor (constructor injection)

ในภายภาคหน้าไม่ว่าเราจะมีการเพิ่มประเภท log อีกสักกี่ตัว class LogEngine เราก็แทบไม่ต้องแก้ไขเลย ยกเว้นจะมีการเปลี่ยนแปลงทางโครงสร้างของ interface ILog

ข้อควรระวังของ Constructor Injection

  • ถ้าหากจะใช้ Constructor Injection ต้องระวังอย่าสร้าง empty constructor ใน class นั้น 
  • วิธีนี้จะทำให้ dependency object จะไม่มีทางเปลี่ยนได้แล้ว เช่นถ้าเราฉีด object ของ ConsoleLog เข้ามา ก็จะไม่สามารถเปลี่ยนเป็น object ของ FileLog ได้อีกแล้ว เพราะถูกกกำหนดมาให้ตั้งแต่ตอนสร้าง object 


Setter Injection 

เป็นวิธีที่ฉีด dependency object ผ่านทาง setter method วิธีนี้ดีกว่าแบบแรกตรงที่ยืดหยุ่นกว่า constructor injection เพราะสามารถเปลี่ยน dependency object ได้ตลอดเวลาผ่านทาง setter method โดยที่ไม่ต้องสร้าง object ขึ้นมาใหม่
public class LogEngine {

   private ILog log;

   public void setLog(ILog log) {
       this.log = log;
   }

   public void log(String message) throws Exception{
       log.openLog();
       log.log(message);
       log.closeLog();
   }
}
Method Injection 

เป็นวิธีที่ฉีด dependency object ไปยัง method ที่มีการเรียกใช้โดยตรงเลย วิธีนี้เป็นวิธีที่ยืดหยุ่นที่สุด แต่มัันจะเป็นเรื่องน่ารำคาญเพราะต้องฉีด object เข้าไปทุกครั้งที่จะมีการเรียกใช้ method ที่ถูกทำการฉีดเข้าไป
public class LogEngine {
   public void log(String message, ILog log) throws Exception{
       log.openLog();
       log.log(message);
       log.closeLog();
   }
}

ใครเป็นคนสร้าง dependency object? 

หลายคนคงสงสัยว่าแล้วใครจะเป็นสร้าง dependency object แล้วใครเป็นผู้ฉีด object เข้าไป ซึ่งถ้าเราไม่ได้ใช้ library หรือ framework อย่าง Spring Framework, Salta หรือ Google Guice นักพัฒนาจะต้องเป็นคนสร้าง dependency object และเป็นคนฉีดเข้าไปเอง

 ถามว่าควรจะสร้าง dependency object ที่ไหน เพราะจากที่กล่าวไปทั้งหมด ถ้าหากเราสร้าง object ตรงๆ เท่ากับเป็นการสร้างการผูกมัด คำตอบคือควรจะสร้างที่ส่วนที่อยู่บนที่สุดที่เกี่ยวกับ module เป็นจุดที่สร้าง object เพราะว่ายังไงการสร้าง application ใดๆ ขึ้นมาก็ต้องมีส่วนที่จะร้อยเรียง module ย่อยๆ ขึ้นมาประกอบให้เป็น application อยู่แล้ว อย่างในตัวอย่างเราก็จะสร้าง dependency object ใน main method
...
public static void main(String args[]) throws Exception {
   //prepare dependency object
   ILog consoleLog = new ConsoleLog();

   LogEngine log = new LogEngine();
   log.setLog(consoleLog); //inject object
   log.log("TestLog");
}
...
สรุป

IoC คือหลักการที่ไม่ให้ object สร้าง dependency object ที่จะเรียกใช้เองโดยตรง เพื่อหลีกเลี่ยงการผูกมัดกันระหว่างทั้ง 2 object โดย dependency object จะถูกขึ้นมาจากภายนอก แล้วนำมาฉีดลงไปใน object ที่ต้องการจะใช้จากภายนอก

DI คือวิธีการที่ทำให้หลักการของ IoC นั้นเกิดขึ้นจริง

จะเห็นได้ว่าหลักการของ IoC เป็นตัวช่วยให้การพัฒนา application ของเรานั้นยืดหยุ่นขึ้นกว่าเดิม สามารถขยับขยายได้ง่ายขึ้น แต่ใช่ว่าหลักการของ IoC นั้นจะไม่มีข้อเสีย


  • ทำให้การอ่าน code นั้นยากขึ้น เพราะแทนที่เราจะเรียกใช้ตรงๆ แต่เรากลับเรียกใช้ผ่าน abstraction ยกตัวอย่างในกรณีของ class LogEngine หลังจากที่เปลี่ยนมาใช้ interface ตามหลักของ IoC จะพบว่าเวลาอ่าน code เราจะไม่รู้ว่า dependency object ที่เข้ามานั้นจะเป็นอะไรระหว่าง ConsoleLog หรือ FileLog 
  • เพิ่มภาระให้กับนักพัฒนา สำหรับการเตรียม dependency object สำหรับการฉีดเข้าไปยัง object ที่เรียกใช้ เพราะในการพัฒนา application ในระบบใหญ่ๆ นั้นไม่ได้มีเพียงแค่ module ย่อยๆ เพียง module เดียว อาจจะมีเป็นร้อยเป็นพัน module ซึ่งถ้านักพัฒนาเอา IoC มาใช้ นักพัฒนาก็ต้องเตรียมสิ่งเหล่านี้ด้วยตัวเอง 


นั่นแหละจึงเป็นเหตุผลว่าทำไม Spring ถึง learning curve สูงงงงงงงง

อ้างอิง

Comments

Popular posts from this blog

ลองเล่นและเรียนรู้พื้นฐานขั้นต้นของ Spring Framework

** สำหรับใครที่ไม่เคยเรียนรู้ในด้านของ Java EE หรือ J2EE อาจจะมึนงงกับศัพท์หน่อยครับ ทำไมต้อง Spring Spring เป็น framework ที่นิยมมากในการนำไปสร้างระบบในระดับ enterprise ในเริ่มแรกที่ Spring เกิดมา มีจุดมุ่งหมายเพื่อที่จะมาแทนที่มาตรฐานของ Java อย่าง J2EE (Java 2 Enterprise Edition) ที่มันทั้งหน่วงทั้งอืดและยุ่งยาก โดยเฉพาะในส่วนของ EJB (Enterprise Java Bean) ที่ถือว่าเป็นฝันร้ายของนักพัฒนา ทำให้กูรูสาย Java ในช่วงนั้นถึงกับแนะนำว่า ถ้าจำเป็นที่ต้องพัฒนาระบบด้วย J2EE จงอย่าใช้ EJB ถึงขั้นถึงกับมีหนังสือแนะแนวทางการพัฒนาระบบ J2EE โดยไม่ใช้ EJB อย่างไรก็ตามทาง Sun ผู้เป็นเจ้าของ Java ในสมัยนั้น ถึงกับต้องมาล้างระบบ J2EE ใหม่ในปี 2006 จัดการใน EJB ให้ใช้ง่ายขึ้น มีประสิทธิภาพมากขึ้น และมีการเปลี่ยนชื่อจาก J2EE เป็น Java EE (Java Enterprise Edition) เพื่อลบภาพอันเลวร้ายของเดิมให้หมด และได้มีการนำฟีเจอร์เด็ดๆ ของ open source framework หลายๆ ตัว อย่างเช่นแกนหลักของ Spring อย่าง IoC (Inversion of Control) หรือ OR Mapping (Object Relational Mapping) ที่เป็นที่นิยมอย่าง Hibernate แต่ก็ไ

ลองเล่น Lambda Expression ฟีเจอร์เด่นใน Java 8

ประวัติความเป็นมาของ Lambda expression Lambda expression ไม่ใช่สิ่งแปลกใหม่ในวงการ ภาษาโปรแกรม ( Programming Language ) เพราะ lambda มันเป็นแกนหลักของ การเขียนโปรแกรมเชิงฟังก์ชัน ( Functional Programming ) ซึ่งมีอายุมานานมากแล้ว แต่ Java เพิ่งนำเอาคุณสมบัตินี้เอามาใส่ลงในเวอร์ชัน 8 หากจะกล่าวถึงที่มาของ lambda คงต้องไปดูที่ถึงที่มาของ lambda calculus ซึ่งถูกสร้างขึ้นมาตั้งแต่ปี 1930 โดยนักคณิตศาสตร์ชาวอเมริกัน  Alonzo Church  เพื่อใช้ในการแก้โจทย์ปัญหาทางคณิตศาสตร์ที่มีความซับซ้อน ในบางครั้งสมการทางคณิตศาสตร์ที่ยาวไปอาจจะทำให้เกิดความซับซ้อนโดยใช่เหตุ lambda calculus จะทำการยุบบางส่วนของสมการนั้นออกมาเป็นฟังก์ชันย่อยๆ เพื่อทำให้สมการนั้นเข้าใจง่ายขึ้น ต่อมาหลักการของ lambda calculus ได้ถูกนำไปใช้ใน Turing Machine ซึ่งเป็นแบบจำลองในอุดมคติของ Alan Turing  ที่ต่อมากลายเป็นต้นแบบที่ถูกนำไปใช้ในการผลิต  Von Neumann Machine  ซึ่ง Von Neumann Machine ตัวนี้ได้กลายเป็นต้นแบบของคอมพิวเตอร์เครื่องแรกของโลกในเวลาต่อมา ท้ายที่สุดแนวคิดของ lambda calculus ก็ถูกนำมาแปลงเป็นภาษาโปรแกรมท

ลองเล่น SonarQube คลื่นโซนาร์ช่วยตรวจสอบคุณภาพของ code

SonarQube  คือเครื่องมือช่วยตรวจสอบคุณภาพของ source code ช่วยหาข้อบกพร่องใน source code ไม่ว่าจะเป็น Bug ที่น่าจะเกิดขึ้น ช่องโหว่ทางด้านความปลอดภัยหรือกลิ่นไม่ดีใน source code ของเรา (Code Smell) และ ช่วยตรวจสอบเราเขียน code ทดสอบครอบคลุมหรือดีแล้วยังยัง (code coverage) Code Smell ไม่ได้ใช้วัดว่า source code นี้สามารถทำงานได้ถูกต้อง มี bug หรือช่องโหว่หรือไม่ แต่ Code Smell ใช้วัดถึงคุณภาพของการออกแบบ เพื่อตรวจสอบว่า source code ที่เป็นอยู่ในปัจจุบันจะสามารถต่อเติม แก้ไขหรือทดสอบได้ง่ายหรือไม่ โดยหลักเกณฑ์ที่นำมาใช้วัดในส่วนของ Code Smell คือ ความซ้ำซ้อนของ code มี code แบบเดียวกันไปซ้ำกันในไฟล์ไหนบ้าง ตรวจสอบเงื่อนไขใน if ให้ ว่าเงื่อนไขตรงนี้มันมีโอกาสเป็นไปได้ไหม เพราะบางทีเงื่อนไขที่เราเขียนขึ้นมาเพื่อดักไว้ในบางครั้งมันแทบจะไม่มีโอกาสที่เวลามันทำงานแล้วเข้าเงื่อนไขในส่วนนั้น เป็นต้น สามารถไปอ่านรายละเอียดเพิ่มเติมได้ที่นี่ http://www.somkiat.cc/code-smell-internal-class/ นอกจาก SonarQube จะสามารถบอกถึงคุณภาพของ source code เราได้แล้ว ยังสามารถใช้ในการแจกแจงงานให