Inversion of Control Containers(IoC) และ Dependency Injection Pattern(DI)

เกริ่นนำ

บทความนี้จะนำเสนอหลักการ ที่ใช้ในการประกอบองค์ประกอบ หรือ assemble objects/components ทุกสิ่งอย่างใน System หรือ World ของเรา ได้ในขณะ runtime หรือใน projects อื่นที่ต้องการกำหนดคุณลักษณะ(configuration)ที่แตกต่างกันได้ใน ขณะ run time

หลักการข้างต้นนิยมเรียกกันในชื่อแบบทั่วไปว่า Inversion of Control(IoC หรือ อธิบายแบบบ้านๆได้ว่า เอาอะไรที่ฉันต้องเกี่ยวข้องด้วย ไปจัดการยัดใส่ให้ฉันเอง ข้างนอกซะ) ซึ่งเป็น patterns ที่ใช้แก้ปัญหาข้างต้น โดยอาจจะใช้ชื่อ Dependency Injection(DI) Pattern แทนก็ได้ ไม่มีความสำคัญมากนักว่าจะใช้ชื่อไหน มันแตกต่างแค่ว่า IoC เป็นชื่อที่ทั่วไปกว่า คือมัน สามารถจะเอาไปอธิบายลักษณะการแก้ปัญหาที่มีลักษณะข้างต้นไม่เฉพาะแต่เรื่อง software components แต่ DI Pattern เป็นชื่อเฉพาะเรื่องของการประกอบ, configuration และ wiring objects หรือ components ให้กับ software ใดๆ

” As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection. ” – Martin Fowler –

Dependency Injection Pattern

โดยปกติ  Domain Model ใดๆใน World(หรือ System) ของเรา ก็จะมีพฤติกรรมของตนเอง ที่บางพฤติกรรมของมัน ขึ้นอยู่กับพฤติกรรมของ domain model อื่นๆ และในทางกลับกันมันก็จัดเตรียมพฤติกรรมหรือข้อมูล ไว้ให้บริการกับ domain model อื่นๆด้วยเช่นกันยกตัวอย่างเช่น เมื่อเราออกแบบและ พัฒนาด้วยวิธี Test-Driven Development(TDD) เริ่มต้นเราจำเป็นต้องมี domain models ไว้ส่วนหนึ่งเพื่อใช้เขียนเรื่องราว(story)จาก spec ความต้องการของเรา บ่อยครั้งที่เราใช้ stubs หรือ mocks model เพื่อสมมติ หรือจำลองพฤติกรรมตามเรื่องราวที่เขียนไว้ จึงทำให้ domain models ภายใต้การทดสอบของเรานี้เป็นอิสระจาก domain models อื่นๆ หรือ layer อื่นๆ ที่อยู่นอกเหนือจากเรื่องราวที่เราต้องการจำลองนี้ได้ โดยทำให้เรื่องราวจำลอง(mockup)จากความต้องการของเราทั้งหมดไม่ผิดเพี้ยนไปจากเดิม

อย่างไรก็ตามในขณะ runtime จริงๆ domain model ของเราก็ต้องการ implematation ที่แท้จริงจาก domain model อื่น ในทางกลับกัน domain model อื่น ก็ต้องการบริการที่แท้จริงจากเราด้วยเช่นกัน ดังนั้นทุกๆ domain model ใน World ของเราจำเป็นจะต้องถูกสร้างขึ้น และทำการ wiring พวกมันเข้าด้วยกัน แล้วทำงานได้อย่างถูกต้อง ซึ่งงานแบบนี้เราจึงต้องการ DI เข้ามาช่วยเพราะมันจะทำให้เราสามารถที่จะประกอบ และ configuration องค์ประกอบ หรือ domain model ของเราที่ทำงานได้และทดสอบเสร็จแล้ว ได้เลยแทบจะทันที

ผมจะเริ่มอธิบายขั้นตอนการสร้าง และเชื่อมความสัมพันธ์(wiring) ของ domain model แบบที่ให้เข้าใจได้ง่ายๆ

สมมติ เรากำลังออกแบบ และพัฒนาระบบ Payment นะครับ โดยมี Cashier เป็น Actor หลักตัวเดียว

public class Cashier

{

public decimal GetNetPay(decimal amt)

{

//ต้องการ FinancialCalculator เพื่อการคำนวณ

}

}

Code ข้างต้นเป็น model ของ Cashier ซึ่งมีหน้าที่คำนวณ NetPay โดยผ่าน method GetNetPay ซึ่งเมื่อเราวิเคราะห์ต่อพบว่า Cashier ใช้เครื่องมือ FinancialCalculator ช่วยคำนวณส่วนลดเพื่อคำนวณ NetPay โดย FinancialCalculator จะประกาศเป็นสมาชิกไว้ใน Cashier และสร้างมันที่ constructor ของ Cashier แบบง่าย

private FinancialCalculator _calculator;

public Cashier()

{

_calculator = new FinancialCalculator();

}

public class FinancialCalculator

{

public decimal GetDiscount(decimal amt)

{

return amt * 0.10m;//ลดให้ 10 %

}

}

และแก้ code คำนวณ NetPay

public decimal GetNetPay(decimal amt)

{

//ใช้ FinancialCalculator คำนวณ

return amt – _calculator.GetDiscount(amt);

}

Code ข้างต้นอธิบายได้ว่า เมื่อไรก็ตามที่เราต้องการคำนวณ NetPay ซึ่งหน้าที่คำนวณเป็นของ Cashier ซึ่งมันต้องการ FinancialCalculator ในการคำนวณ NetPay โดยทำการสร้างที่ constructor ของ Cashier และนี่คือ code ที่แสดงการพึ่งพิงกัน หรือกล่าวอีกแบบได้ว่า model Cashier เป็นผู้พึงพิง(dependent) และ model FinancialCalculator  เป็นผู้ถูกพึ่งพิง(dependency)

จากเรื่องราวทั้งหมด ผมได้พบว่า การคำนวณ discount นั้นต้องอ่านค่า discount rate ขึ้นมาคำนวณซึ่งขึ้นอยู่กับ mode ของ Cashier  นั่นคือถ้าอยู่ในลักษณะ Online จะอ่านค่า discount rate มาจากฐานข้อมูล และถ้ากรณีอยู่ในลักษณะ Offline จะอ่านค่ามาจาก memory ตัวเอง โดย default กำหนดให้ Cashier ใช้ FinancialCalculator เป็นลักษณะ Online โดยการสร้าง FinancialCalculator แบบใดจะขึ้นอยู่กับลักษณะของ  Cashier หรือการ config

เมื่อปลับ code แล้วจะได้

public class Cashier

{

private IFinancialCalculator _calculator;

public Cashier()

{

Type aType = typeof(OnlineFinancialCalculator);//Default

if (/*read configuration*/)

{

aType = typeof(OfflineFinancialCalculator);

}

_calculator = (IFinancialCalculator)Activator.CreateInstance(aType);

}

public decimal GetNetPay(decimal amt)

{

//ใช้ FinancialCalculator คำนวณ

return amt – _calculator.GetDiscount(amt);

}

}

public class OnlineFinancialCalculator : IFinancialCalculator

{

decimal _discountRate;

public OnlineFinancialCalculator()

{

//_discountRate = [Retrive Discount from Database system]

}

public decimal GetDiscount(decimal amt)

{

return amt * _discountRate;

}

}

public class OfflineFinancialCalculator : IFinancialCalculator

{

decimal _discountRate;

public OfflineFinancialCalculator()

{

_discountRate = 0.1m;//กำหนดค่าไว้ในตัวเอง

}

public decimal GetDiscount(decimal amt)

{

return amt * _discountRate;

}

}

public interface IFinancialCalculator

{

decimal GetDiscount(decimal amt);

}

นำ domain model ทั้งหมดเขียนเป็น class diagram ได้ ดังนี้

ซึ่งเมื่อพิจารณา class diagram แล้วพบว่า Cashier พึ่งพิง interface IFinancialCalculator และ model implementors 2 ตัวก็คือ OnlineFinancialCalculator และ OfflineFinancialCalculator ถ้าพิจารณานับกันง่ายๆ อาจกล่าวได้ว่าระบบนี้มี coupling เท่ากับ 3* และจะมีโอกาศเพิ่มขึ้นได้เรื่อยๆ หากว่ามีตัว implementor จาก interface IFinancialCalculator เพิ่มขึ้นเข้ามาในระบบอีกอย่างแน่นอน นั่นก็หมายความว่าระบบของเราจะมี coupling เพิ่มขึ้นเรื่อยๆ และมี config เพิ่มขึ้นด้วย ทำให้เรากลับไปแก้ไข code Cashier เพื่อให้ตอบรับกับ config ใหม่นี้ด้วย… เมื่อพิจารณาแล้ว คิดว่าไม่เป็นผลดีกับระบบของเราอย่างแน่นอนในอนาคต

ถ้าเราวิเคราะห์ระบบ Payment ของเราให้ดีแล้ว Cashier พึ่งพิง interface IFinancialCalculator เพียงเท่านั้นก็พอเพียงแล้ว แล้วเราจะทำอย่างไรดีละ ให้ accociation ที่การ <<create>> เพื่อ injects implementor หรือ service ทั้งสองนั้น ออกไปจาก class diagram ของเราให้ได้ เพื่อไม่ให้ coupling เพิ่มขึ้นในอนาคต และสามารถ config ได้โดยไม่ต้องแก้ไข code ใดๆเลย นั่นคือเราจะต้องมีตัวอะไรสักอย่างเข้ามาประกอบ Cashier เข้ากับ IFinancialCalculator ที่มีการ implement ตามที่เราต้องการแล้ว และนี่ก็คือแนวคิดของ Inversion of Control(IoC) ส่วนตัวอะไร ที่จะมาช่วยเราประกอบ และ wiring องค์ประกอบต่างๆ นั่นก็คือ IoC Containers นั่นเอง

สรุปเราก็น่าจะมีรูป class diagram ที่มี IoC Containers รวมอยู่ด้วย แบบนี้

อธิบาย model PaymentWorld ก็คือ class IoC Containers ที่จะเข้ามาช่วยเราประกอบ และ wiring Cashier กับ IFinancialCalculator ในระบบ Payment ของเรา และ IoC Container นี้จัดอยู่ในกลุ่มของ infrastructure components ดังนั้นมันจึงไม่ส่งผลกระทบกับ domain  logic ใดๆของเราเลย

Code ของ PaymentWorld

public class PaymentWorld

{

static IFinancialCalculator _calculator;//เครื่องมือ Financial Calculator ให้สร้างแค่ 1 ตัว

public static void Configure()

{

Type aType = typeof(OnlineFinancialCalculator);//Default

string mode = /*load configuration*/;

if (mode == “OnlineFinancialCalculator”)

{

aType = typeof(OfflineFinancialCalculator);

}

_calculator = (IFinancialCalculator)Activator.CreateInstance(aType);

}

public static Cashier CreateCashier()

{

Cashier cashier = new Cashier

{

Calculator = _calculator,

};

return cashier;

}

}

เรากลับขึ้นไปแก้ไข model Cashier เพื่อลด coupling ระหว่าง implementor class ทั้งสอง และกำหนดให้ _calculator เป็น property เพื่อให้ Containers สามารถกำหนด(หรือ inject) Calculator ให้ Cashier ได้

public class Cashier

{

public IFinancialCalculator Calculator { get; set; }

public Cashier() { }

public decimal GetNetPay(decimal amt)

{

//ใช้ FinancialCalculator คำนวณ

return amt – Calculator.GetDiscount(amt);

}

}

เมื่อ PaymentWorld ดำเนินการ Configure แล้วมันจะทำการ load config ขึ้นมา แล้วทำ injection หรือการ set องค์ประกอบต่างๆของระบบเราทั้งหมดเข้าด้วยกัน(การ inject dependency objects(service) ให้กับ dependent objects(client) ด้วยวิธีการนี้ จึงเรียกว่า Dependency Injection หรือ DI นั่นเอง) ซึ่งการ config มีแบบ code base configuration และ xml base configuration ทั้งนี้จะเป็นแบบไหนก็ขึ้นอยู่กับ Container และความถนัด DI ที่เราจะเลือกใช้(Spring.NET, MEF or Unity) บทความนี้ผมเขียน IoC Container ขึ้นมาเองแบบง่ายๆเพื่อแสดงให้เห็นแนวคิดของ IoC หรือ DI Patterns เท่านั้น

สุดท้ายนี้ ผมก็คิดว่าเมื่อคุณอ่านจนจบแล้วต้องงงๆ ใช่มั้ยครับ เหมือนกันครับเขียนเองก็ยังงงเหมือนกันไม่รู้จะอธิบายยังไง เพราะผมรู้สึกว่าแนวคิด IoC(พิจารณาแล้วมันคล้ายๆกับ Hollywood Principle) มันเขียนอธิบายทั้งยาก และยาวเลย เพราะต้องหาตัวอย่างประยุกต์มาอธิบายมัน แต่พอเอามันมาใช้งานจริงๆกลับอ่านเอกสารการใช้งานมันน้อยมาก และเข้าใจได้ง่ายกว่ามานั่งอ่านคำอธิบายยาวๆนี้ซะอีก

ฝากไว้ท้ายสุดว่า ยังไงก็ลองใส่ใจเรื่องของ IoC หรือ DI นี้ด้วยครับเพราะมันสำคัญมากกับระบบของคุณในระยะยาว เนื่องจากองค์ประกอบของระบบจะเพิ่มขึ้นไปเรื่อยๆ ตามความซับซ้อนของ software ประยุกต์ใดๆที่จะต้องปลับตัวไปตามเวลา ไปตามวิวัฒนาการของมัน ดังนั้นการประกอบองค์ประกอบ หรือการ wiring domain model หรือ objects ต่างๆเข้าด้วยกัน จึงเป็นอะไรที่ซับซ้อนขึ้นในแบบทวีคูณ ซึ่งการ concern เรื่อง Dependency Injection Pattern นี้จะช่วยคุณ โดยการ move logic การ assemble หรือ wiring domoain models ของเราทั้งหมดไปไว้ที่ DI container แทน จึงทำให้ domain model ของเรา concern เฉพาะ logic ของพวกมันอย่างเดียวเท่านั้น ซึ่งมันจะช่วยลดความซับซ้อน(entropy) ของระบบให้เราได้มากเลยทีเดียวครับ

ผมได้ทำตัวอย่าง DI ง่ายๆไว้แล้ว อ่านได้ที่ Dependency Injection ด้วย Spring.NET/Microsoft Unity… Actions!

ขอบคุณครับ 🙂

*ในบทความนี้ผมคำนวณ Coupling ของระบบ เท่ากับ “จำนวน accociation ของ class ที่มี accociation มากที่สุด”

แหล่งข้อมูล

Advertisements

#architecture, #design, #pattenrs