Design Pattern Dalam PHP (Bagian I)
Design Pattern? Mungkin bagi yang pernah mendengarnya tentu familiar dengan bukunya Gang of Four “Design Patterns: Elements of Reusable Object-Oriented Software”. Lalu apa itu design pattern? Konsep sederhana dibalik design pattern adalah solusi untuk masalah umum yang didokumentasikan oleh seorang yang berpengalaman dalam pengembangan aplikasi. Umumnya masalah yang ada sering ditemui dalam beberapa proses pengembangan sehingga dinamai pola (pattern). Sedangkan desain yang digunakan untuk mengatasi pola masalah tersebut umumnya digunakan agar kode menjadi less-coupled (tidak terlalu mempengaruhi kode lainnya jika ada perubahan ke depannya), mudah dimantain, portabel, mudah dibaca, dapat diterapkan dalam tim yang dinamis dan untuk skala proyek yang kompleks. Design Pattern dapat digunakan dalam bahasa pemrograman apa saja, khususnya bahasa pemrograman yang OOP, tak terkecuali PHP. Ada banyak design pattern, saya akan bahas empat design pattern (Singleton, Registry, Adapter dan Observer) terlebih dahulu dan akan saya lanjutkan pattern lainnya di tulisan lainnya.
Singleton
Singleton digunakan jika hanya diperbolehkan satu obyek terinisialisasi dari sebuah class. Misalnya sebuah class database, dimana objek yang dibuat dari class tersebut akan menginisialisasi koneksi ke database server. Untuk memastikan hanya ada satu koneksi saja yang tercipta (tentunya agar menghemat pemakaian memori) maka dapat digunakan pendekatan singleton. Berikut pseudo code dari pengimplementasian singleton pattern:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <?php class SingletonDB { // digunakan untuk menyimpan obyek // dari SingletonDB. private static $__instance; // dengan membuat konstruktor private // maka penciptaan obyek tidak dapat dilakukan // di luar class. private function __construct($param = '') { echo "{$param}: Object diinisialisasi...\n"; } // untuk memungkin penciptaan satu objek saja // diperlukan method publik yang menginisialisasi // SingletonDB jika belum terinisialisasi lalu simpan // di properti class yang private. Selanjutnya obyek // tidak akan diinsialisasi lagi. public static function getInstance($param = '') { if ( is_null(self::$__instance) ) { self::$__instance = new SingletonDB($param); echo "{$param}: Object selesai diinisialisasi.\n"; } return self::$__instance; } } // sekarang kita test $db1 = SingletonDB::getInstance('DB1'); $db2 = SingletonDB::getInstance('DB2'); $db3 = SingletonDB::getInstance('DB3'); ?> |
Oke, save kode di atas dengan nama file singleton.php, lalu coba jalankan:
akeda@akeda-laptop:~/www/design_pattern$ php singleton.php DB1: Object diinisialisasi... DB1: Object selesai diinisialisasi. akeda@akeda-laptop:~/www/design_pattern$
Dapat dilihat $db2 dan $db3 tidak menginisialisasi obyek baru. Penggunaan singleton tidak hanya terbatas pada class database tapi dapat digunakan untuk situasi apapun yang membutuhkan hanya satu saja inisialisasi obyek.
Registry
Registry mempunyai konsep seperti phonebook di HP Anda. Untuk mencari no HP Budi, Anda tinggal ketik Budi maka dapatlah nomornya (dan juga informasi lainnya). Untuk memasukkan kontak baru, Anda tinggal masukkan kata kuncinya (dalam hal ini nama teman Anda) dan juga nilai yang ingin disimpan (dalam hal ini no HP teman Anda). Jika Anda familiar dengan setter dan getter, maka Anda akan mudah memahami Registry. Situasi serupa dapat diterapkan dalam konfigurasi sistem. Konfigurasi sistem meyimpan properti aplikasi seperti nama aplikasi, koneksi database, theme yang digunakan, dsb. Kita akan membuat sebuah class Configure yang digunakan untuk men-set dan men-get properti dari konfigurasi, berikut kode-nya:
<?php class Configure { private $__config; private static $__instance; private function __construct($config) { $this->__config = $config; } /** * Dapatkan objek dari Configure yang juga Singleton * @param array $config Array konfigurasi * @return object Obyek dari Configure */ public function getInstance($config) { if ( is_null(self::$__instance) ) { self::$__instance = new Configure($config); } return self::$__instance; } /** * Dapatkan nilai dari $key * @param mixed $key Key dari array konfigurasi yang ingin di * dapatkan nilainya * @return mixed */ public function get($key) { $val = null; if ( isset($this->__config[$key]) ) { $val = $this->__config[$key]; } return $val; } /** * Menset nilai $val dengan key $key ke konfigurasi * @param mixed $key * @param mixed $val * @return bool Selalu true */ public function set($key, $val) { $this->__config[$key] = $val; return TRUE; } } // test configure $conf_array = array( 'app_name' => 'Aplikasi Ku', 'theme' => 'biru_muda', 'debug' => FALSE ); $config = Configure::getInstance($conf_array); echo $config->get('app_name') . "\n"; echo $config->get('theme') . "\n"; $config->set('debug', TRUE); echo 'debug:' . $config->get('debug') . "\n"; ?>
Sekarang test jalankan:
akeda@akeda-laptop:~/www/design_pattern$ php registry.php Aplikasi Ku biru_muda debug:1
Pada contoh class Configure di atas dapat kita lihat bahwa kita juga menerapkan Singleton. Ini dilakukan agar data konfigurasi konsisten, jadi diperlukan hanya ada satu obyek dari Configure.
Adapter
Misalkan Anda sedang membangun sebuah sistem CMS dimana ada fitur plugin. Anda menyediakan sebuah interface plugin dimana para developer plugin harus mengikutinya. Di core CMS Anda memiliki PluginManager yang akan mengeksekusi setiap plugin. Dalam pengembangan ke depan Anda perlu mengubah core CMS dan PluginManager pun ikut berubah. Anda tentunya tidak ingin kan setiap plugin menjadi tidak berfungsi kembali? Nah karena tidak mungkin menyentuh semua plugin yang di buat oleh developer di luar pengembang inti, Anda dapat membuat sebuah Adapter disini untuk menjembatani API dari core sebelumnya untuk kompatibel dengan API yang baru. Ini merupakan cara yang banyak digunakan proyek open source yang merilis API baru dan ingin mempertahankan kompabilitas dengan API lama. Disaat API baru dirilis mereka akan melabelkan beberapa method sebagai usang (deprecated), barulah diversi stabil setelahnya mereka membuang method yang usang tersebut.
Oke contoh yang akan kita buat adalah PluginManager sederhana yang fungsinya mengeksekusi semua plugin yang ada di dalam direktori plugins. Konvensi penamaan file plugin adalah plugin_nama_plugin.php, maka di dalam nya perlu didefinisikan sebuah class dengan nama PluginNamaPlugin yang mengimplementasi interface PluginInterface. Untuk mempermudah mengikuti contoh gunakan struktur seperti ini:
plugins.php plugins\plugin_a.php plugins\plugin_b.php plugins\plugin_akeda_b.php
Buka file plugins.php yang masih kosong. Berikut adalah interface PluginManager yang akan diimplementasi oleh class BackendPluginManager di core plugin Anda :
1 2 3 4 | <?php interface PluginManager { public function executeAllPlugins(); } |
Dan class BackendPluginManager yang mengimplementasi PluginManager (masih dalam file plugins.php) :
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | class BackendPluginManager implements PluginManager { private $__author; private $__content; private $__plugins; public function __construct($author_info, $content) { $this->__author = $author_info; $this->__content = $content; $plugin_directory = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR; if ( empty($this->__plugins) ) { if ($handle = opendir($plugin_directory)) { while ( false !== ($file = readdir($handle)) ) { if ( $file != "." && $file != ".." && strstr($file, 'plugin_') ) { require_once $plugin_directory . $file; $plugin_name = substr($file, 7); $plugin_name = substr($plugin_name, 0, -4); $plugin_names = explode('_', $plugin_name); $plugin_name_class_name = 'Plugin'; foreach ( $plugin_names as $w ) { $plugin_name_class_name .= ucfirst($w); } if ( class_exists($plugin_name_class_name) ) { $this->__plugins[$plugin_name_class_name] = new $plugin_name_class_name; } } } closedir($handle); } } } public function executeAllPlugins() { foreach ($this->__plugins as $plugin) { echo $plugin->filterAuthorInfo($this->__author); echo $plugin->filterContent($this->__content); } } } |
Yang dilakukan BackendPluginManager saat method __construct (konstruktor) dipanggil adalah mencek semua file di direktori plugins dengan prefix nama file ‘plugin_’. Lalu cek di file tersebut apakah ada class plugin yang sesuai dengan konvensi penamaan plugin. Jika terdapat class tersebut maka inisialisasi menjadi obyek dan kumpulkan dalam properti array yang private. Method executeAllPlugins() akan mengeksekusi method filterAuthorInfo dan filterContent dari obyek plugin yang telah dikumpulkan di konstruktor. Untuk membuat plugin mengikuti arsitektur plugin core, Anda perlu mendefinisikan interface yang perlu diikuti oleh pengembang plugin. Berikut adalah interface PluginInterface (masih dalam file plugins.php):
interface PluginInterface { public function filterContent($content); public function filterAuthorInfo($author_info); }
Sedangkan berikut adalah salah satu contoh plugin dengan nama plugin ‘PluginA’ (berarti nama filenya adalah ‘plugin_a.php’) :
1 2 3 4 5 6 7 8 9 10 11 | <?php class PluginA implements PluginInterface { public function filterContent($content) { return "Plugin A - Content: {$content}\n"; } public function filterAuthorInfo($author_info) { return "Plugin A - Author : {$author_info}\n"; } } ?> |
Dalam contoh saya buat 3 contoh plugin, yaitu PluginA (plugins/plugin_a.php), PluginB (plugins/plugin_b.php) dan PluginAkedaB (plugins/plugin_akeda_b.php). Ok sekarang contoh implementasi-nya (dalam file plugins.php):
// Misalkan data ini telah ditarik dari db $content = "Ini adalah content yang di filter dari setiap plugin"; $author = "Akeda Bagus; admin@gedex.web.id"; // pseudo code di core CMS.. $plugin_manager = new BackendPluginManager($author, $content); $plugin_manager->executeAllPlugins(); ?>
Kita jalankan untuk mengetes semua plugin dan plugin manager nya bekerja seperti apa:
akeda@akeda-laptop:~/www/design_pattern$ php plugins.php Plugin Akeda B - Author : Akeda Bagus; admin@gedex.web.id Plugin Akeda B - Content: Ini adalah content yang di filter dari setiap plugin Plugin A - Author : Akeda Bagus; admin@gedex.web.id Plugin A - Content: Ini adalah content yang di filter dari setiap plugin Plugin B - Author : Akeda Bagus; admin@gedex.web.id Plugin B - Content: Ini adalah content yang di filter dari setiap plugin
Dalam pengembangan Anda perlu mengubah BackendPluginManager dan PluginInterface. Apa yang harus dilakukan sementara banyak plugin diluar kendali kita? Berikut adalah interface plugin yang baru:
interface NewPluginInterface { public function filterContent($content); public function filterAuthorEmail($author_email); public function filterAuthorName($author_name); }
Jika sebelumnya hanya ada method filterAuthorInfo($author_info), kini plugin perlu mengimplementasi method filterAuthorEmail($author_email) dan filterAuthorName($author_name). Di BackendPluginManager kini hanya menerima dua method tersebut. Berikut adalah class NewBackendPluginManager:
class NewBackendPluginManager extends BackendPluginManager { private $__author_email; private $__author_name; private $__plugins; public function __construct($author_info, $content) { parent::__construct($author_info, $content); $author = explode(';', $author_info); $this->__author_email = trim($author[0]); $this->__author_name = trim($author[1]); $this->__content = $content; $plugin_directory = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR; if ( empty($this->__plugins) ) { if ($handle = opendir($plugin_directory)) { while ( false !== ($file = readdir($handle)) ) { if ( $file != "." && $file != ".." && strstr($file, 'plugin_') ) { require_once $plugin_directory . $file; $plugin_name = substr($file, 7); $plugin_name = substr($plugin_name, 0, -4); $plugin_names = explode('_', $plugin_name); $plugin_name_class_name = 'Plugin'; foreach ( $plugin_names as $w ) { $plugin_name_class_name .= ucfirst($w); } if ( class_exists($plugin_name_class_name) ) { $this->__plugins[$plugin_name_class_name] = new $plugin_name_class_name; } } } closedir($handle); } } } public function executeAllPlugins() { foreach ($this->__plugins as $plugin) { $interfaces = class_implements($plugin); // jika interface yang digunakan bukan // interface yang baru if ( !array_key_exists('NewPluginInterface', $interfaces) ) { $plugin = new PluginAdapter($plugin); } echo $plugin->filterAuthorEmail($this->__author_email); echo $plugin->filterAuthorName($this->__author_name); echo $plugin->filterContent($this->__content); } } }
Anda menginginkan plugin baru mengikuti interface ini tapi plugin lama tetap dapat berjalan. Solusi mudah adalah dengan Adapter. Berikut adalah contoh Adapter yang menjadi jembatan antara plugin dan NewBackendPluginManager:
class PluginAdapter implements NewPluginInterface { private $__plugin; function __construct($plugin) { $this->__plugin = $plugin; } public function filterContent($content) { return $this->__plugin->filterContent($content); } public function filterAuthorEmail($author_email) { return $this->__plugin->filterAuthorInfo($author_email); } public function filterAuthorName($author_name) { return $this->__plugin->filterAuthorInfo($author_name); } }
Sekarang kita test plugin manager dan Adapter tersebut :
$plugin_manager = new NewBackendPluginManager($author, $content); $plugin_manager->executeAllPlugins(); ?>
Berikut hasilnya:
akeda@akeda-laptop:~/www/design_pattern$ php plugins.php Plugin A - Author : Akeda Bagus Plugin A - Author : admin@gedex.web.id Plugin A - Content: Ini adalah content yang di filter dari setiap plugin Plugin Akeda B - Author : Akeda Bagus Plugin Akeda B - Author : admin@gedex.web.id Plugin Akeda B - Content: Ini adalah content yang di filter dari setiap plugin Plugin B - Author : Akeda Bagus Plugin B - Author : admin@gedex.web.id Plugin B - Content: Ini adalah content yang di filter dari setiap plugin
Selain untuk menjadi jembatan antara API yang tidak saling kompatibel, Adapter juga dapat digunakan untuk jembatan antara model (tabel dalam bentuk objek) dan database. Karena banyaknya jenis database yang perlu disupport Anda dapat menggunakan pendekatan Adapter dalam hal ini.
Observer
Observer mempunyai konsep seperti notifikasi di Facebook dan notifikasi layanan SMS. Di facebook pada saat kita mengomentari posting di wall seseorang maka kita secara tidak langsung akan mendaftarkan diri kita menerima notifikasi jika ada komentar baru. Dalam layanan content di SMS, pada saat kita mendaftarkan diri kita ke suatu layanan via REG DAFTAR, maka kita akan mendapatkan notifikasi/balasan SMS jika ada suatu event berkaitan dengan layanan SMS tersebut. Design pattern Observer terdiri dari dua entitas, yaitu subyek yang di observasi dan observer yang mendaftarkan diri untuk diberitahu jika suatu event terjadi terhadap subyek yang diobservasi. Dalam kasus Facebook, wall posting seseorang disebut sebagai subyek yang di observasi, sedangkan observer-nya adalah teman yang berkomentar di wall posting tersebut. Kita akan ambil contoh class WallPost sebagai subyek yang diobservasi. Pertama kita buat dahulu interface ObservableSubject agar setiap class yang ingin terobservasi harus mengimplementasi interface ini:
1 2 3 4 5 6 | <?php interface ObservableSubject { public function reg($key, $obj); public function unreg($key); public function notify(); } |
Pada interace ObservableSubject, method reg($key, $obj) digunakan oleh observer yang tertarik mendapatkan notifikasi. Method unreg($key) digunakan jika observer sudah tidak tertarik lagi mendapatkan notifikasi. Sedangkan method notify() digunakan untuk mem-broadcast notifikasi ke semua observer yang telah mendaftar. Sekarang kita perlu membuat interface Observer untuk class yang tertarik menjadi observer:
8 9 10 | interface Observer { public function getNotification($wall_owner, $username, $act); } |
Berikut contoh class WallPost yang mengimplementasikan ObservableSubject:
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | class WallPost implements ObservableSubject { private $__owner; private $__username; private $__act; private $__observers = array(); public function __construct($owner) { $this->__owner = $owner; } public function reg($key, $obj) { $this->__observers[$key] = $obj; } public function unreg($key) { unset($this->__observers[$key]); } public function notify() { foreach ($this->__observers as $observer) { $observer->getNotification($this->__owner, $this->__username, $this->__act); } } public function fav($username) { $this->__username = $username; $this->__act = 'favorites'; $this->notify(); } public function comment($username) { $this->__username = $username; $this->__act = 'comments'; $this->notify(); } } |
Pada class WallPost terdapat method reg($key, $obj), unreg($key) dan notify() yang merupakan hasil kontrak dengan interface ObservableSubject. Semua obyek yang mendaftar melalui method reg($key, $obj) akan dikumpulkan dalam properti array $__observers. Method fav($username) dan comment($username) merupakan aksi ($__act) yang terjadi di WallPost. Jika aksi tersebut terjadi di WallPost, maka method notify() akan dipanggil untuk memberi notifikasi ke semua obervser yang telah terdaftar. Sekarang kita perlu membuat class yang mengimplementasikan Observer. Pada contoh saya akan buat tiga class dengan tujuan yang berbeda-beda, yaitu class UserNotification (untuk menotifikasi pengguna via dashboard), MailerNotification (untuk menotifikasi pengguna via email) dan SystemLog (untuk menotifikasi system logger). Berikut class-class tersebut:
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | class UserNotification implements Observer { private $__my_name; public function __construct($my_name) { $this->__my_name = $my_name; } public function getNotification($wall_owner, $username, $act) { echo "UserNotification: Hi {$this->__my_name}! {$username} {$act} on {$wall_owner}'s wall\n"; } } class MailerNotification implements Observer { private $__my_name; public function __construct($my_name) { $this->__my_name = $my_name; } public function getNotification($wall_owner, $username, $act) { echo "MailerNotification: Hi {$this->__my_name}! {$username} {$act} on {$wall_owner}'s wall\n"; } } class SystemLog implements Observer { public function getNotification($wall_owner, $username, $act) { echo "SystemLog: {$username} {$act} on {$wall_owner}'s wall\n"; } } |
Sekarang kita tes jalankan :
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | // inisialisasi subyek yang akan diobservasi $my_wall = new WallPost("Akeda Bagus"); // daftarkan notifikasi ke semua teman $my_friends = array('Budi', 'Eko', 'Paul'); foreach ( $my_friends as $friend ) { ${$friend . '_dashboard'} = new UserNotification($friend); ${$friend . '_mail'} = new MailerNotification($friend); $my_wall->reg($friend . '_dashboard', ${$friend . '_dashboard'}); $my_wall->reg($friend . '_mail', ${$friend . '_mail'}); } // notifikasikan juga sistem log $sys_log = new SystemLog(); $my_wall->reg('system_log', $sys_log); // Yuli memfavoritkan postingan di wall Akeda Bagus $my_wall->fav('Yuli'); // Jono mengomentari postingan di wall Akeda Bagus $my_wall->comment('Jono'); // Budi dan Eko tidak tertarik lagi mendapatkan notifikasi $my_wall->unreg('Budi_dashboard'); $my_wall->unreg('Budi_mail'); $my_wall->unreg('Eko_dashboard'); $my_wall->unreg('Eko_mail'); // John berkomentar, tapi kini hanya Paul dan SystemLog yang // akan menerima notifikasi $my_wall->comment('John'); |
Dan berikut adalah hasilnya:
akeda@akeda-laptop:~/www/design_pattern$ php observer.php UserNotification: Hi Budi! Yuli favorites on Akeda Bagus's wall MailerNotification: Hi Budi! Yuli favorites on Akeda Bagus's wall UserNotification: Hi Eko! Yuli favorites on Akeda Bagus's wall MailerNotification: Hi Eko! Yuli favorites on Akeda Bagus's wall UserNotification: Hi Paul! Yuli favorites on Akeda Bagus's wall MailerNotification: Hi Paul! Yuli favorites on Akeda Bagus's wall SystemLog: Yuli favorites on Akeda Bagus's wall UserNotification: Hi Budi! Jono comments on Akeda Bagus's wall MailerNotification: Hi Budi! Jono comments on Akeda Bagus's wall UserNotification: Hi Eko! Jono comments on Akeda Bagus's wall MailerNotification: Hi Eko! Jono comments on Akeda Bagus's wall UserNotification: Hi Paul! Jono comments on Akeda Bagus's wall MailerNotification: Hi Paul! Jono comments on Akeda Bagus's wall SystemLog: Jono comments on Akeda Bagus's wall UserNotification: Hi Paul! John comments on Akeda Bagus's wall MailerNotification: Hi Paul! John comments on Akeda Bagus's wall SystemLog: John comments on Akeda Bagus's wall
Kesimpulan
Kita sudah membahas tentang Singleton, Registry, Adapter dan Observer. Singleton digunakan untuk situasi dimana dibutuhkan satu obyek saja yang terinisialisasi dari sebuah class (seperti class Database). Registry diibaratkan seperti sebuah phonebook HP, dimana dibutuhkan sebuah class yang dapat menyimpan dan mendapatkan nilai berdasarkan kunci tertentu. Adapter digunakan sebagai jembatan antara dua buah API yang sudah tidak kompatibel atau jembatan antara API yang beragam. Dan yang terakhir adalah Observer yang mempunyai konsep seperti wall post di Facebook, dimana subyek yang menjadi sentral pemberi notifikasi ke obyek yang tertarik terhadap notifikasi subyek tersebut. Di tulisan lain, saya akan bahas design pattern lainnya. Happy Coding!
Referensi
- E. Gamma, R. Helm, R. Johnson, and J. Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, Reading, MA, 1995.
- Steve Holzner. Design Patterns. VTC Videos, 2007.
Quick Tips to Help You get Accepted on Google Summer of Code
Back to top
1 Ari, 02 Sep 2010 at 6:55 pm
nice post kk penjelasanya jelas banget jd lebih mengerti saya