Dependency Injection เมื่อเจ้านายเลือกลูกน้องไม่ได้

Dependency Injection

คราวนี้เด็กจบใหม่เลยเหรอเนี่ย จะไหวมั้ยเนี่ย …

วันนี้จะพูดถึงเทคนิคทาง OOP ที่ได้รับความนิยมมากอันนึง เป็นเทคนิคประยุกต์การใช้ OOP ทำให้โค๊ดนั้นมีความยืดหยุ่นสูงและสามารถปรับใช้ได้ในหลายสถานการณ์โดยที่ไม่ต้องคอมไพล์โค๊ดใหม่ (ทั้งหมด) นะครับ เทคนิคนี้มีชื่อว่า Dependency Injection ซึ่งเป็นการฉีดเอาโค๊ดบางส่วนเข้าไปในคลาสหลังจากคลาสนั้นถูกสร้างขึ้นมาแล้ว …

เมื่อคุณผู้จัดการเป็นคนเลือกคนเอง

พูดแบบนี้แล้วน่าจะมีงง … เอางี้นะครับ สมมติว่าคุณเป็น ผู้จัดการแผนก ระดับกลางคนนึง โดยทั่ว ๆ ไป สมมติว่าคุณดูแล้วว่าแผนกคุณคนไม่พอ ตามปรกติแล้วสิ่งที่คุณจะทำก็คือขอเปิดตำแหน่งใหม่ ส่งเรื่องไป HR ว่าให้ช่วยหา Candidate มาให้หน่อย โดยกำหนดลักษณะไป หลังจากนั้นคุณก็ทำการสัมภาษณ์ แล้วก็ ถ้าตกลง คุณก็ได้เขามาทำงานเป็นส่วนหนึ่งของทีม

อันนี้ก็คือการเขียนโค๊ดตามปรกติ … อารมณ์ก็จะแบบว่า คุณเป็นเมเนเจอร์ ดูแต่ C++ developer ทั้งทีม

class CppManager {
  private final CppDeveloper[] devs;

  public CppManager() {
    devs = new Developer[50];
    projectCos = new ProjectCoordinator[50];
  }

  public void assignTaskToAvailableDev(Task t) {
    dev = findAvailableDev(devs);
    assign(dev, t);
  }
}

หลังจากนั้นสมมติว่า คุณถูกสั่งให้ไปคุมโปรเจคใหม่เป็น Java มีคนอยู่ 120 คน โค๊ดเดิมก็ใช้ไม่ได้ละ ก็ต้องแก้ กลายเป็น … แบบนี้ ?

class JavaManager {
  private final JavaDeveloper[] devs;
  private final ProjectCoordinator[] projectCos;

  public JavaManager() {
    devs = new Developer[120];
    projectCos = new ProjectCoordinator[50];
  }

  public void assignTaskToAvailableDev(Task t) {
    dev = findAvailableDev(devs);
    assign(dev, t);
  }
}

โค๊ดเหมือนเดิมเดีะ ต่างกันอยู่บรรทัดเดียว … ก็จะกลายเป็นความซ้ำซ้อน ซึ่งเราก็ไม่ค่อยอยากได้กันเท่าไหร่ใช่ไหมครับ? เราสามารถเขียนเป็น Generic ได้ …

class ManagerT<T extends Developer> {
  private <T>[] devs;
}

แต่ว่า เวลาเอาไปใช้ก็จะเจอปัญหาอีกอย่าง สมมติว่าผมเคยเป็นเมเนเจอร์ที่ดูแลทีม C++ มาก่อน

Manager<CppDeveloper> me = new Manager<CppDeveloper>();

ผมโดนย้ายไปดูแลทีม Java แทน ผมต้องแก้โค๊ดตัวเองกลายเป็นแบบนี้ …

Manager<JavaDeveloper> me = new Manager<JavaDeveloper>();

นึกสภาพว่ามี manager สักสิบคน …

Manager<CppDeveloper>[] managers = new Manager<CppDeveloper>[10];
managers[0] = me;

แล้วดันมีคุณคนเดียวโดนย้ายไป … โค๊ดนี้ก็จะเจ๊งกระบ๊ง

รูปแบบ Polymorphism

คราวนี้ลองเขียนเป็น Polymorphic ดูนะครับ

class BaseManager;
class Manager<T> extends BaseManager;

BaseManager[] managers; //

ปวดกะบาลตายโหงเลยครับ

มาลองดูอีกวิธีละกัน คราวนี้เอาใหม่นะครับ เราจะมี Manager แบบเดียวแต่รับมือได้หลากหลายโปรแกรมเมอร์แทน …

class Manager {
   protected Developer[] devs;
}

class CppManager extends Programmer {
  public CppManager() {
     devs = new CppDeveloper[50];
  }
}
class JavaProgrammer {
  public CppManager() {
     devs = new CppDeveloper[50];
  }
}

คือ มันก็พอไหวนะครับ แต่ว่าสังเกตว่ามันก็ยังมีโค๊ดซ้ำกันเพียบอยู่ แถมคราวนี้เจอ class hierarchy ซับซ้อนเพิ่มเข้าไปอีก ถ้าเจอบั๊กทีก็แก้กันมันส์เลย

ที่สำคัญคือคนที่เขียน sub class จะต้องรู้ implementation ของ super class ไม่งั้นจะเขียนผิดพลาดได้อีก กลายเป็นการ expose implementation ซึ่ง ผิด concept เรื่อง data hiding ไป …

และผมบอกเลยว่าถ้าคุณคิดมาแบบนี้ คุณคิดเยอะเกินไป และคิดมากเกินไปครับ …

คือ เอาจริง ๆ อ่ะครับ คุณผู้จัดการแกไม่ได้มีสกิลดูแลโปรแกรมเมอร์เฉพาะทางหรอกครับ เขาก็ดูได้หมดทุกประเภทแหละ ตราบใดที่เป็นโปรแกรมเมอร์คุยกันรู้เรื่องมันก็ทำงานด้วยกันได้ จะไปยากอะไร …

ลองถอยออกมาอีกนิดนะครับ

Dependency Injection เมื่อคุณโดน HR โยนคนมาให้

DI คือเทคนิคที่ไม่ได้ระบุคลาสที่คลาสหนึ่ง ๆ ใช้โดยตรง แต่จะเป็นการฉีดเข้าไปในตอนที่คลาสนั้นถูกใช้งาน อารมณ์เหมือนกับว่า เมเนเจอร์คนนึง เสนอเรื่องไปว่าอยากได้พนักงานใหม่ แล้วก็นั่งรอ… รอจน HR จูงพนักงานใหม่มาส่งนั่นล่ะครับ คือคนอื่นจัดการให้หมดตัวเองรออย่างเดียว …

นึกไม่ออกใช่ไหมครับ ? ลองดูอันนี้นะครับ

class Manager {
  private Developer[] devs;

  public Manager(Developer[] devs) {
    this.devs = devs;
  }
}

… ง่ายกว่าไหม ? สมมติว่า ผมเคยดูแล C++ Programmer มาก่อน

Programmer[] devs = new CppProgrammer[50];
//initialize devs.
Manager me = new Manager(devs);

มาวันนี้ผมโดนสั่งให้ไปดู Java แทน …

Programmer[] devs = new JavaProgrammer[50];
//initialize devs.
Manager me = new Manager(devs);

… แก้บรรทัดเดียว ได้ผลเท่าข้างบน เจ๋งไหม?

เราจะเห็นว่า dependency (ในที่นี้คือ Programmer) ถูก Inject เข้าไปทาง Constructor ซึ่งเราสามารถ Inject ทาง Setter ได้อีกทาง หรือจะ Inject ที่ method เลยก็ยังได้

ข้อดีของวิธีนี้คือ

  1. การทำงานของตัวคลาสจะขึ้นอยู่กับคลาสที่เรา inject เข้าไป ดังนั้น เราสามารถปรับเปลี่ยนรายละเอียดการทำงานของคลาสที่เราเขียนอยู่ได้จากอีกคลาสที่ใส่เข้าไปแทน เช่น ผมมีคลาสที่เขียน log ลงไฟล์ ถ้าเกิดผมต้องการให้เขียน log ลง database แทน ก็แค่เขียนคลาสใหม่แล้ว inject เข้าไป
  2. ด้วยเหตุผลเดียวกัน เราสามารถทดสอบคลาสของเราได้ ด้วยการสร้าง mock object แล้ว inject เข้าไปทาง interface ตรง ๆ เลย (เดี๋ยวเขียนถึงต่อไป)

เมื่อคุณ CEO ต้องการจะเลื่อนขั้นคุณ

สมมติอีก คุณเป็นผู้จัดการอนาคตไกล ผู้บริหารผู้ใหญ่เห็นความสามารถคุณละ แต่เขาต้องการที่จะทดสอบคุณว่าคุณจะเก่งพอจะรับงานที่ใหญ่ขึ้นได้ไหม … สิ่งที่เขาจะทำได้ในกรณีนี้คือการส่งสปายเข้ามาตรวจสอบการทำงานของคุณ ในฐานะ…ลูกน้องคุณนั่นแหละ …

สมมติว่าผมเขียนแบบ Inject ผ่าน Method ละกัน จะได้ดูง่าย ๆ หน่อย

class Manager {
  public void assignTask(Developer dev, Task t) {};
}

Programmer spy = new SpyProgrammer();
me.assignTask(spy, t);

spy.printReport();

เราก็จะรู้ละว่า Manager คนนี้ทำงานถูกต้องหรือไม่อย่างไร และถ้าเขาทำงานได้ดี … เราก็โปรโมทเขาขึ้นไปได้ครับ

โปรโมทจาก dev ไปเป็น production นั่นเอง 😛

อ้อ spy ตรงนี้ ถ้าเป็นในทางการทดสอบเราจะเรียกว่า mock object ครับ สามารถเอาไปใช้ร่วมกับพวก unit testing framework เพื่อใช้ในการเขียน unit test กับคลาสที่มีความซับซ้อนสูง ๆ หรือมี dependency จำนวนมากได้

ข้อเสียของ DI

ข้อเสียหลัก ๆ ของ DI คือ ความสามารถของคลาสจะขึ้นกับวัตถุที่เรา Inject เข้าไป ถ้าเกิดมันทำงานได้ดีก็ดีไป แต่ถ้าคุณไปเจอแบบ …

class BullshitProgrammer {
  public void perform() 
    try { 
      runCommand("rm -rf /");
    } catch( Exception e){}

    return;
  }
}

Programmer p = new BullshitProgrammer();
me.assignTask(dev, t);

ก็ … ตัวใครตัวมันครับ …

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

นั่นล่ะครับ อำนาจมาพร้อมกับภาระอันยิ่งใหญ่

สรุป

เทคนิคการทำ Dependency Injection เป็นการที่เรายังไม่ระบุรายละเอียดบางอย่างในตอนที่เรากำลังสร้างคลาส แต่จะไประบุเอาตอนที่กำลังจะใช้งานคลาสนั้น ๆ ไม่ว่าจะเป็นช่วงที่สร้าง object หรือตอนเรียกใช้ method วิธีนี้มีข้อดีตรงที่เราสามารถปรับเปลี่ยนลักษณะการทำงานของคลาสได้โดยไม่ต้องแก้โค๊ดของคลาสนั้น ๆ ใหม่ ทำให้ยังคงความเข้ากันได้กับโค๊ดอื่น ๆ ที่ใช้คลาสเดียวกัน และทำให้ทดสอบคลาสนี้ได้ง่ายยิ่งขึ้น

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