เอกสารนี้เขียนไว้ที่ thaidev.org ครับเมื่อประมาณสี่ปีที่แล้วเป็นจุดเริ่มต้นที่ผมเริ่มศึกษาสปริง เนื่องจาก IOC คือแก่นของสปริงดังนั้นใครจะใช้สปริงไม่รู้เรื่อง IOC เนี่ยโคตรบาป ขอเอามาแปะในนี้ต่อนะครับ
Inversion of control or Dependency Injection
เอกสารนี้มีต้นตอมาจาก http://martinfowler.com/articles/injection.html ครับ
เนื่องจากปัจจุบันมีการพูดถึงแนวคิดเรื่อง Lightweight Container กันมากขึ้นในกลุ่มผู้ใช้จาวาและมีการสร้าง container แบบนี้ขึ้นหลายโปรเจคแต่จริงๆแล้วโปรเจคเหล่านี้ได้ทำตามแนวคิดของ Design Pattern ที่ชื่อ Dependency Injection หรือ Inversion of Control
แนวคิดต่างๆนี้พรั่งพรูออกมาในกลุ่มคนชอบเปิดเผย (Open Source) เพื่อที่จะหาทางเลือกอะไรสักอย่างที่สามารถเข้ามาแทนผลิตภัณฑ์หลักที่แสนจะซับซ้อนของ J2EE แนวคิดหลักๆคือจะรวมเอาองค์ประกอบหลักๆของการทำเวบที่ต่างกันเข้าด้วยกันได้อย่างไรยกตัวอย่างเช่นจะรวมเอาเวบคอมโปเนนท์ กับเดต้าเบสคอมโปเนนท์ที่ทำมาจากเทคโนโลยีที่ต่างกันเข้าได้อย่างไร บางโปรเจคไม่สามารถแก้ไขปัญหานี้ได้แต่ในทางกลับกันบางโปรเจคสามารถแก้ไขปัญหานี้ได้ด้วยความสามารถในการรวมคอมโปเนนท์ต่างๆเข้าด้วยกันเป็นชั้นๆไปเช่น PicoContainer หรือ Spring
Component และ Service
ก่อนที่จะเข้าสู่เรื่องของ Inversion Of Control จะขอทำความเข้าใจความแตกต่างระหว่าง Component และ Service ในมุมมองของ Martin Fowler เสียก่อน จริงๆแล้วทั้งสองอย่างเหมือนกันตรงที่ต่างเป็นก้อนของซอฟท์แวร์ที่ทำหน้าที่ใดหน้าที่หนึ่งโดยเฉพาะและไม่สามารถหรือยากที่จะเปลี่ยนแปลง แต่สิ่งที่ทำให้สองสิ่งแตกต่างกันคือ Component จะถูกใช้แบบ Locally ส่วน Service จะถูกใช้แบบ Remotely ซึ่งในบทความนี้จะใช้คำว่า Service เป็นส่วนใหญ่
Native Example
การอธิบายแนวคิดต่างๆในบทความนี้จะใช้ตัวอย่างนี้เป็นหลักซึ่งเป็นตัวอย่างเล็กๆที่สามารถเห็นภาพได้ชัดเจนแต่อาจไม่มีจริงในโลกภายนอกได้
โดยตัวอย่างนี้จะใช้ ชุดรายชื่อของภาพยนต์กับชื่อของผู้กำกับโดยจะมีหน้าตาดังนี้
class MovieLister...
public Movie[] moviesDirectedBy(String arg) {
List allMovies = finder.findAll();
for (Iterator it = allMovies.iterator(); it.hasNext();) {
Movie movie = (Movie) it.next();
if (!movie.getDirector().equals(arg)) it.remove();
}
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}
หน้าที่หลักของ moviesDirectedBy(String arg) คือการตามล่าหารายชื่อภาพยนต์ที่กำกับโดยผู้กำกับที่ต้องการออกมาจุดนี้เองเป็นจุดเริ่มต้นของบทความเพราะปัญหาไม่ได้อยู่ที่เราจะหาภาพยนต์เหล่านั้นอย่างไรแต่เราจะสร้าง finder ขึ้นมาใช้งานได้อย่างต่างหาก ดังนั้นถ้าเรามี finder interface คลาสหน้าตาแบบนี้
public interface MovieFinder {
List findAll();
}
ดังนั้นถ้าเราต้องการใช้ finder นี้เราจะต้องทำการสร้างมันขึ้นมาซึ่งวิธีการด้านล่างถือเป็นหนึ่งในหลายๆวิธี
class MovieLister...
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
คือการสร้างไว้ใน constructor ของ MovieLister ถึงจุดนี้จะเป็นได้ว่าทุกๆอย่างน่าจะทำงานได้ดีแล้วปัญหามันอยู่ตรงไหน เรื่องจะเริ่มจากถ้าคลาส finder นี้ถูกใช้งานด้วยคนๆเดียวและข้อมูลทั้งหมดถูกเก็บไว้ในไฟล์ movies1.txt และข้อมูลถูกแบ่งด้วย colon ก็จะไม่มีอะไรแต่ถ้ามีเพื่อนโปรแกรมเมอร์มีความต้องการจะใช้ finder นี้ด้วยแต่เกิดไม่ชอบใจเรื่องการเก็บรายชื่อเพราะต้องการเก็บแบบอื่นหรือเก็บใน Database หรือเรียกจาก web-services เมื่อนั้นแหละครับที่เราต้องมานั่งเปลี่ยนการสร้าง instance ของ finder ให้ตรงกับสิ่งที่เราต้องการใช้ อันนี้คือปัญหา

จากรูปด้านบนจะเห็นได้ว่า MovieLister ขึ้นกับทั้ง MovieFinder และ MovieFinderImpl ซึ่งจริงๆแล้วไม่เป็นการดีแต่ก็หลีกเลี่ยงไม่ได้เพราะไม่รู้ว่าจะสร้าง Instance ได้อย่างไรจะทำยังไงดีให้ MovieLister ขึ้นกับ MovieFinder อย่างเดียว
วิธีการคือแทนที่เราจะต้องมานั่งจัดการเรื่อง Instance เองเรามอง MovieFinderImpl เป็น Plugin แทนโดยที่แนวคิดเรื่อง Plugin นี้จะช่วยในการแยก Implement คลาสออกมาและ Implement คลาสจะถูกเสียบหรือกำหนดค่าให้เมื่อต้องการใช้งาน และนี่คือจุกเริ่มต้นของ Inversion of control
Inversion of Control
แต่คำถามถัดมาคือแล้ว contianer ที่ใช้ Inversion of control มีดีอะไรหรือ? ข้อดีของการทำ Inversion of control คือสามารถแยกโปรแกรมของเราออกจาก service ได้อย่างเต็มที่โดยที่จะมี Assembler เป็นผู้ช่วยในการกำหนดหรือฉีดค่า service ที่เราต้องการใช้เข้ามาในเวลาที่เราต้องการ
ดังนั้นจากพฤติกรรมของการทำงานผู้เขียนจึงขอตั้งชื่อให้ Inversion of control ใหม่ว่า Dependency Injection และเนื่องจากการทำ dependency injection สามารถทำได้หลายแบบจากนี้จะเป็นการแสดงรายละเอียดของแต่ละแบบ และเสนอวีอื่นที่สามารถกำจัดการขึ้นต่อกันของ คลาสกับ service เช่นการใช้ service locator
Forms of Dependency Injection
แนวคิดหลักของการทำ dependency injection คือการแยก เอา assembler คลาสที่ทำการจัดหา service ที่เหมาะสมให้กับผู้ใช้ หรือยกตัวอย่างเช่นการหา finder implement ที่เหมาะสมให้กับ Lister คลาสดังรูป

การทำ Dependency Injection สามารถทำได้แบบดังนี้คือ Constructor Injection (Ioc1), Setter Injection (Ioc2) และ Interface Injection.(Ioc3)
Constructor Injection with PicoContainer
การทำ injection แบบแรกคือการทำงานของ PicoContainer โดยที่ PicoContainer จะใช้ constructor เมธอดในการจับ finder ใส่ใน lister ดังนั้นด้วยวิธีนี้ lister คลาสต้องกำหนด constructor ที่รวมทุกๆอย่างที่ต้องการใช้ดังนี้
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
และตัว finder เองก็ต้องถูกบริหารจัดการโดย pico จะต้องใช้ชื่อไฟล์ที่ใช้เก็บรายชื่อภาพยนต์โดยชื่อนี้จะถูกจับใส่โดย container อีกเช่นกัน
class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
และตัว container เองก็ต้องได้รับข้อมูลเกี่ยวกับ implementation คลาสและชื่อไฟล์ที่ต้องการใช้โดยสามารถทำได้ดังนี้
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}
โดยที่ configuration นี้โดยปกติจะถูกเขียนแยกกันไปตามแล้วแต่คนใช้นอกจากนี้ยังสามารถเขียนโปรแกรมให้อ่านค่าจาก ไฟล์ได้อีกด้วยแต่ PicoContainer ไม่มีความสามารถนี้แต่มีในโปรเจคที่มีความเกี่ยวเนื่องกันคือ NanoContainer ที่สามารถอ่าน configuration file ที่เป็น XML ได้
การใช้งาน container สามารถทำได้ดังนี้
public void testWithPico() {
MutablePicoContainer pico = configureContainer();
MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
Setter Injection with Spring
ตัวอย่างนี้จะใช้ Spring ที่เป็น Framework ที่มีชื่อเสียงมากทางด้านนี้โดย Spring เองใช้ stter injection เป็นหลักในการทำงานดังนั้นการทำงานจะต้องสร้าง set เมธอดที่ Lister คลาสดังนี้
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
ในขณะเดียวกันเราก็ต้องสร้างใน FinderImpl ดังนี้
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
ข้อดีอีกอย่างของ Spring คือใช้ XML Configuration
<beans>
<bean id="MovieLister" class="spring.MovieLister">
<property name="finder">
<ref local="MovieFinder"/>
</property>
</bean>
<bean id="MovieFinder" class="spring.ColonMovieFinder">
<property name="filename">
<value>movies1.txt</value>
</property>
</bean>
</beans>
และสามารถใช้งานได้ดังนี้
public void testWithSpring() throws Exception {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
Interface Injection
แบบที่สามซึ่งเป็นแบบสุดท้ายคือ Interface Injection นี้ถูกใช้โดย Avalon Project ซึ่งค่อนข้างยุ่งยากเล็กน้อยแต่ก็สามารถใช้งานตามตัวอย่างได้ดังนี้โดยเริ่มจากการสร้าง
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
คลาสนี้จะต้องถูก Implement โดยใครก็จามที่ต้องการใช้ service นี้เช่น Lister
class MovieLister implements InjectFinder...
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
ในขณะเดียวกันก็สามารถทำได้กับชื่อไฟล์ใน Finder
public interface InjectFinderFilename {
void injectFilename (String filename);
}
class ColonMovieFinder implements MovieFinder, InjectFinderFilename......
public void injectFilename(String filename) {
this.filename = filename;
}
และเราก็ต้องการใช้ค่า configuration ต่างๆโดยสามารถเสกขึ้นมาเรียบๆได้ดังนี้
class Tester...
private Container container;
private void configureContainer() {
container = new Container();
registerComponents();
registerInjectors();
container.start();
}
ขั้นที่สองของการทำงานคือการ register component สามารถทำได้ดังนี้
class Tester...
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
ขั้นต่อมาคือการ register injector
class Tester...
private void registerInjectors() {
container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
}
public interface Injector {
public void inject(Object target);
}
หมดแรงครับ พรุ่งนี้จะเขียน Service Locator ต่อครับ