Thursday, May 3, 2012

Dependency Injection & Inversion Of Control

Dalam kesempatan ini gue pengen ngebahas tentang dependency injection dan inversion of control karena gue sempet melihat di milis ada yang masih belum mengerti bedanya. Bahkan ada yang mengira bahwa DI atau IOC itu berbeda dengan Object Oriented Programming.

Di milis bahkan ada yang sudah menjawab dan jawaban tersebut, meskipun menurut gue belum lengkap, sudah menuju ke arah yang benar.

Thread lengkapnya bisa dibaca di milis.

Gue kutip jawaban dari mas Herloc disini:

Intinya sih semua dependency dari kelas tersebut dilakukan diluar, trus dimasukkan (inject) ke kelas tersebut melalui constructor atau setter (setModel misalnya)... Lebih ribet? Yup, namun kalo mau ganti2 implementasi dari dependency jauh lebih enak (database vs dummy)...

Contoh yang diberikan:
Cara biasa tanpa Dependency Injection
class Person_Model_Database
{
  function findAll()
  {
    $sql = 'SELECT * FROM t_people';
    
    return $db->queryAll($sql);
  }
}

class Person_Service
{
  private $model;
  
  function __construct()
  {
    $this->model = new Person_Model_Database;
  }
  
  function findAll()
  {
    // hak akses, dst
    
    return $this->model->findAll();
  }
}

$service = new Person_Service;
$people = $service->findAll();

Cara Dependency Injection
interface Person_Model_Interface
{
  function findAll();
}

class Person_Model_Database
  implements Person_Model_Interface
{
  function findAll()
  {
    $sql = 'SELECT * FROM t_people';
    
    return $db->queryAll($sql);
  }
}

class Person_Model_Dummy
  implements Person_Model_Interface
{
  function findAll()
  {
    return array('Gw', 'Lo', 'End');
  }
}

class Person_Service
{
  private $model;
  
  function __construct(Person_Model_Interface $model)
  {
    $this->model = $model;
  }
  
  function findAll()
  {
    // hak akses, dst
    
    return $this->model->findAll();
  }
}

// ambil data dari database
$dbModel = new Person_Model_Database;
$service = new Person_Service($dbModel);
$people = $service->findAll();

// ambil data bohongan dari array, gara2 yg bikin DB belom kelar2
$dummyModel = new Person_Model_Dummy;
$service = new Person_Service($dummyModel);
$people = $service->findAll();

menurut gue contoh mas herloc itu adalah jawaban yang tepat untuk Inversion of Control. tapi belum dependency injection.

gue ambil contoh kita punya 2 Service ya..
namespace app\service
  
  class Applicants{
    
    private $person;
    function __construct(model\interfaces\Person $person)
      
    {
      $this->person = $person;
    }
  }

namespace app\service
  
  class ClassRoom{
    
    private $person;
    
    private $room;
    
    function __construct(model\interfaces\Person $person,
                         model\interfaces\Room $room)
      
    {
      $this->person = $person;
      $this->room = $room;
    }
  }

maka untuk menciptakan 2 kelas tersebut kita harus menciptakan 2 kelas konkrit person dan room dimana nantinya kita masukkan ke konstruktornya Applicants dan ClassRoom
$personModel= new model\Person;
$roomModel = new model\Room;
$applicantsService= new app\service\Applicants($personModel);
$classRoomService = new app\service\ClassRoom($personModel. $roomModel)

fine.. ini baru dua.. gimana kalo dalam satu bisnis logik lo butuh 5 service sekaligus, dimana masing2 service bisa memiliki 3 model atau lebih yg berbeda? bakal pegel nyiptain modelnya satu2 dan masukin ke service class.

disini kita bisa menggunakan Factory Pattern atau sebangsanya
namespace util;

class Factory{
  
  public static function createApplicants(){
    $personModel= new model\Person;
    return new app\service\Applicants($personModel);
  }
  
  public static function createClassRoom(){
    $personModel= new model\Person;
    $roomModel = new model\Room;
    return new app\service\Applicants($personModel, $roomModel);
  }
  
}

jadi kita tinggal:
$applicantsService = util\Factory::createApplicants();
$classRoomService = util\Factory::createClassRoom();

Okay.. fine.. ini sudah mulai "dependecy injection".. kan kita punya class dimana kita bisa meng "inject" semua object yang kita mau.

Problem berikutnya adalah.. apabila kita punya 10 service yang butuh misalnya Person model, maka di factory class kita bisa kemungkinan memiliki 10 method dimana kita harus "inject" konkrit class Person 10 kali ke masing2 service tersebut. ini gak terlihat bakal mudah di maintain..

coba bayangkan, kalo kita punya satu line of code yang isinya kira2...

"untuk semua kelas yang memiliki dependency model\interfaces\Person, inject konkrit class model\Person"

dalam bentuk code mungkin bisa ditulis:
$container->bind('model\interfaces\Person')->to('model\Person');

kita gak butuh lagi inject satu2, kita gak butuh lagi Factory method seperti diatas. kita hanya punya satu Factory class dengan satu method "getInstance" (atau sejenisnya)
$applicantsService = util\Factory::getInstance("app\service\Applicants");
$classRoomService = util\Factory::getInstance("app\service\ClassRoom");

Problem berikutnya adalah.. (problem melulu... kapan beresnya...) gimana kalo model Person di Applicants gak sama dengan di ClassRoom? bukannya maka dari itu kita punya interface? :D misalnya Applicants akan menggunakan model\Person, dan ClassRoom menggunakan model\Student dimana Person dan Student sama2 implements model\interface\Person.

Statement binder diatas kan cuma berlaku "untuk semua kelas" yang memiliki dependency model\interfaces\Person.

untuk kasus khusus seperti ini solusinya bisa bermacam2. seperti misalnya menggunakan xml/yaml seperti symfony atau spring (java)
symfony yaml:
service.applicants:
  class: app\service\Applicants
    calls:
      - [ setPerson, [ @... ] ]
service.classRoom:
  class: app\service\ClassRoom
    calls:
      - [ setPerson, [ @... ] ]
      - [ setRoom, [ @... ] ]

spring xml:
<bean id="person" class="model.Person" />
<bean id="student" class="model.Student" />
<bean id="room" class="model.Room" />

<bean id="applicants" class="app.service.Applicants">
    <property name="person" ref="person" />
</bean>

<bean id="classRoom" class="app.service.ClassRoom">
    <property name="person" ref="student" />
    <property name="room" ref="room" />
</bean>

Apakah kita cuma bisa inject object? DI/IOC framework yang yahud gue rasa bisa inject semuanya.. dari primitive (int, string, bool, array) sampe object .. pokoknya apa yang dibutuhkan property sebuah kelas.. bisa kita inject. Ribet itu sebenernya hanya di awal waktu mempersiapkan semuanya.. kalo sudah masuk maintainance.. ini semua lebih terbaca dan lebih mudah refactoring nya.. belum lagi kalo kita mo Test Driven Development.. dengan membuat applikasi kita IOC.. kita sudah memotong2 setiap kelas dan method menjadi unit2 yang bisa kita test tanpa "dependency" dari kelas atau method lain.

Apa lagi yang framework ini bisa? framework yang yahud juga punya "scope" :D .. kalo di PHP gue bakal butuh "Request", "Singleton", dan "Session". di java ada lebih dari ini (misalnya Application scope). Dimana standard scope adalah "Request".

apa enaknya scope? bayangin coba, kalo kita harus maintain semua array atau isi $_SESSION.. makin gede aplikasi, makin bingung kita.. "username" udah kepake belum ya.. apa yang kepake itu "user" atau "userid".. eh tapi gue liat di debug tadi ada "id" apaan tuh ya? siapa yang pake?...

kita bisa saja membuat kelas model\Login yang kita deklarasikan scopenya sebagai session. jadi waktu di inject ke kelas lain, informasi yang ada dalam object model\Login tersebut akan ada/sama selama session belum expired. Dan apabila ada model lain yang juga dalam scope session misalnya model\Person. meskipun punya property yang kebetulan sama (name misalnya) kita gak perlu pusing kalo informasi tersebut tumpang tindih.. karena kita terimanya kan sudah berupa object.. dan meniympan dan membaca dari session sudah diatur sama frameworknya.

Singleton pun begitu.. kalo kita punya object DataAccess misalnya.. kita tentunya pengen sepanjang http request object ini cuma pegang satu koneksi ke database, tidak setiap kali kita panggil untuk diinject dia menciptakan object dan connection yang baru.

semoga memberikan pencerahan. :D

No comments: