Files: bac5418919612ea4d959063c289d20b7f09d7ae3 / _posts / 2013-03-22-memahami-dependency-injection.md
layout: post title: Memahami Dependency Injection date: 2013-03-22 author: ikhwanhayat
permalink: /memahami-dependency-injection.html
Dependency Injection adalah suatu teknik rekabentuk perisian untuk menjadikannya lebih modular dan flexible. Ia kadangkala juga disebut sebagai Dependency Inversion atau Inversion of Control (IoC). Maksud sebenar setiap satu frasa sedikit berbeza, namun untuk permulaan bolehlah dianggap semuanya membawa maksud yang hampir serupa.
<!--more-->
Apa Itu Dependency
Untuk memahami apa itu Dependency Injection (DI), kita perlu terlebih dahulu mengetahui apa yang dimaksudkan dengan dependency.
Dalam suatu sistem yang besar, perkara pertama yang perlu dilakukan ialah pecahkan ia kepada bahagian-bahagian yang lebih kecil supaya mudah untuk kita selesaikan dan yang lebih penting mudah untuk kita senggara (mantainable).
Katakan kita ingin membina suatu sistem online yang akan memaparkan cuaca bagi suatu tempat. Keperluan yang telah ditetapkan ialah sistem mesti mampu memberitahu pengguna cuaca di bandar mereka melalui alamat IP yang dikesan.
Jadi kita akan buatkan suatu class WeatherService
untuk tujuan ini. Katakan kita sudah mempunyai pangkalan data yang akan memberikan kita nama bandar berdasarkan IP. Ada pangkalan data lain pula yang diberikan pada kita oleh Jabatan Meteorologi yang menyenaraikan ramalan cuaca untuk satu-satu bandar.
Untuk memudahkan pemahaman, kita buatkan dulu ia sebagai applikasi konsol. Kita akan gunakan bahasa C# untuk contoh ini.
public class WeatherService
{
public void ShowForIp(string ip)
{
Console.WriteLine("Your IP is " + ip);
// Hubungi pangkalan data geolocation
// Dan larikan query untuk dapatkan bandar berdasarkan IP
var connGeo = new SqlConnection("server1...");
connGeo.Open();
var sqlCity = new SqlCommand("select top 1 city from IpCity where ip='" + ip + "'", connGeo);
var city = (string)sqlCity.ExecuteScalar();
Console.WriteLine("City: " + city);
connGeo.Close();
// Hubungi pula pangakalan data untuk cuaca
// Dan dapatkan cuaca terkini untuk bandar
var connWeather = new SqlConnection("server2...");
connWeather.Open();
var sqlWeather = new SqlCommand("select top 1 weather from CityWeather where city='" + city + "' and day=" + DateTime.Now.DayOfYear, connWeather);
var weather = (string)sqlWeather.ExecuteScalar();
Console.WriteLine("Weather: " + weather);
connWeather.Close();
}
}
// Panggil dari applikasi utama
var svc = new WeatherService();
svc.ShowForIp("10.10.10.10");
// Contoh output
Your IP is 10.10.10.10
City: Kuala Lumpur
Weather: Heavy Rain
Kita dapati class ini perlu melakukan beberapa perkara iaitu pertama ia menerima input, kemudian ia mencari bandar pengguna, lalu mencari cuaca, dan akhirnya memaparkannya.
Bila dilihat kembali, terlalu banyak kerja yang perlu dilakukan oleh class ini. Class yang baik ialah ianya fokus pada kerjanya sahaja. Bila kita kecilkan skop tugas suatu class, kita dapat menjadikan ia lebih "cohesive", tugasnya lebih fokus.
Tugas asasi class ini ialah menerima IP dan memaparkan cuaca bagi IP tersebut. Namun ia tidak dapat berfungsi jika ia tidak dapat mencari bandar untuk IP itu dan seterusnya mencari cuaca untuk bandar tersebut. Apa kata jika kita buatkan class lain untuk melakukan dua tugas tersebut untuknya. Class WeatherService
pula nanti hanya perlu gunakan class baru yang kita akan buat ini supaya objektifnya dapat dicapai.
public class CityFinder
{
public string FindFromIp(string ip)
{
// Hubungi pangkalan data geolocation
// Dan larikan query untuk dapatkan bandar berdasarkan IP
var connGeo = new SqlConnection("server1...");
connGeo.Open();
var sqlCity = new SqlCommand("select top 1 city from IpCity where ip='" + ip + "'", connGeo);
var city = (string)sqlCity.ExecuteScalar();
connGeo.Close();
return city;
}
}
public class WeatherFinder
{
public string FindForCity(string city)
{
// Hubungi pangakalan data untuk cuaca
// Dan dapatkan cuaca terkini untuk bandar
var connWeather = new SqlConnection("server2...");
connWeather.Open();
var sqlWeather = new SqlCommand("select top 1 weather from CityWeather where city='" + city + "' and day=" + DateTime.Now.DayOfYear, connWeather);
var weather = (string)sqlWeather.ExecuteScalar();
connWeather.Close();
return weather;
}
}
public class WeatherService
{
public void ShowForIp(string ip)
{
Console.WriteLine("Your IP is " + ip);
var city = new CityFinder().FindFromIp(ip);
Console.WriteLine("City: " + city);
var weather = new WeatherFinder().FindForCity(city);
Console.WriteLine("Weather: " + weather);
}
}
// Panggil dari applikasi utama
var svc = new WeatherService();
svc.ShowForIp("10.10.10.10");
Cuba lihat, class WeatherService
nampak lebih bersih bukan? Kurang berserabut apabila tugasnya telah dipecahkan kepada class lain. Baik, pada tahap ini anda dapat lihat bagaimana satu class besar dipecahkan kepada class lebih kecil. Aturcara yang baru ini boleh dikatakan lebih modular dari sebelumnya.
Namun, class WeatherService
ini bergantung kepada class CityFinder
dan WeatherFinder
untuk berfungsi. Untuk mencapai modularity, kita menghadapi satu masalah lain pula iaitu dependency (kebergantungan). Adanya dependency membuatkan perubahan sukar dilakukan, kerana perubahan pada satu tempat akan mempengaruhi tempat lain. Namun jika tiada dependency langsung maka class WeatherService
ini langsung tidak dapat berfungsi!
Mengurus Dependency
Kita perlukan cara untuk menguruskan dependency ini. Salah satu caranya ialah menggunakan teknik Dependency Injection.
Dalam class WeatherService
ini, dependency pada CityFinder dan WeatherFinder adalah kuat kerana ia perlu instantiate class-class ini sendiri sebelum menggunakannya. Jika kita ingin mengubahnya pada masa akan datang, kita perlu korek semula class WeatherService
ini dan lakukan perubahan di dalamnya.
Lebih baik jika tugas untuk instantiate class-class ini dilakukan oleh "orang lain". "Orang lain" ini kemudiannya akan memberikan instance class-class yang diperlukan kepada WeatherService
untuk digunakan. Mari kita lihat apa yang saya maksudkan.
// Anggapkan tiada perubahan pada class CityFinder dan WeatherFinder
public class WeatherService
{
private CityFinder cityFinder;
private WeatherFinder weatherFinder;
public WeatherService(CityFinder cityFinder, WeatherFinder weatherFinder)
{
this.cityFinder = cityFinder;
this.weatherFinder = weatherFinder;
}
public void ShowForIp(string ip)
{
Console.WriteLine("Your IP is " + ip);
var city = cityFinder.FindFromIp(ip);
Console.WriteLine("City: " + city);
var weather = weatherFinder.FindForCity(city);
Console.WriteLine("Weather: " + weather);
}
}
// Panggil dari applikasi utama
var cityFinder = new CityFinder();
var weatherFinder = new WeatherFinder();
var svc = new WeatherService(cityFinder, weatherFinder);
svc.ShowForIp("10.10.10.10");
Kita dapat lihat, applikasi utama yang perlu instantiate class CityFinder
dan WeatherFinder
, dan kemudiannya inject mereka ke dalam WeatherService
. Nah, inilah yang dinamakan Dependency Inversion (mengalihkan tugas mengurus dependency ke tempat lain) atau Dependency Injection (memasukkan dependency ke dalam class yang memerlukannya).
Tugas mengawal dependency biasanya diserahkan kepada kod yang berada lebih atas dalam hierarki panggilan kerana ia lebih mudah dikonfigurasikan.
Lebih Panjang dan Sukar?
Nampaknya ia hanya memanjangkan kod aturcara kita sahaja? Apa bagusnya begini? Untuk mendemonstrasikan kelebihannya, mari kita wujudkan satu situasi perubahan yang biasa berlaku dalam sistem perisian.
Katakan pada suatu hari, Jabatan Meteorologi telah meningkat taraf perkhidmatan IT mereka. Cuaca sekarang boleh diperolehi menggunakan web service yang telah mereka sediakan. Lebih tepat dan terkini. Kita ingin mengubahkan sistem kita supaya dapat menggunakan web service ini dan tidak perlu bersusah-payah mengambil data dari mereka setiap minggu :)
//
// Dalam C#, cara yang saya tunjukkan ini menggunakan interface. Dalam bahasa lain mungkin ia tidak diperlukan.
//
public interface IWeatherFinder
{
string FindForCity(string city);
}
//
// Ubah sedikit untuk class WeatherFinder asal supaya ia masih dapat digunakan
//
public class WeatherFinderFromDb : IWeatherFinder
{
public string FindForCity(string city)
{
// ... kod sama
}
}
//
// Class yang baru untuk mencari menggunakan web service
//
public class WeatherWebService : IWeatherFinder
{
public string FindForCity(string city)
{
// Hubungi web service
// Dapatkan cuaca untuk bandar yang diberi
// Anggaplah kod selengkapnya ada di sini :)
return weather;
}
}
public class WeatherService
{
private CityFinder cityFinder;
private IWeatherFinder weatherFinder; // gunakan interface, bukan concrete class lagi
public WeatherService(CityFinder cityFinder, IWeatherFinder weatherFinder)
{
this.cityFinder = cityFinder;
this.weatherFinder = weatherFinder;
}
public void ShowForIp(string ip)
{
Console.WriteLine("Your IP is " + ip);
var city = cityFinder.FindFromIp(ip);
Console.WriteLine("City: " + city);
var weather = weatherFinder.FindForCity(city);
Console.WriteLine("Weather: " + weather);
}
}
// Panggil dari applikasi utama
var cityFinder = new CityFinder();
var weatherFinder = new WeatherWebService(); // gunakan class yang baru
var svc = new WeatherService(cityFinder, weatherFinder);
svc.ShowForIp("10.10.10.10");
Kita dapat lihat bahawa hanya perubahan yang sedikit perlu dilakukan pada WeatherService
. Malah jika anda menggunakan interface dari awal (satu lagi prinsip yang baik untuk digunakan), maka perubahan langsung tidak dilakukan dalam class WeatherService
tersebut.
Melalui teknik ini juga, pengujian dapat dilakukan dengan lebih mudah. Katakan kita buatkan satu unit test untuk WeatherService
ini. Kita tidak mahu web service itu dipanggil setiap kali, kerana ia pastilah lambat. Kita anggapkan sahaja web service ini sudah terbukti berfungsi kerana kita telah lakukan satu lagi unit test untuknya ditempat lain.
Maka kita buatkan satu WeatherFinderStub
yang memulangkan cuaca yang sudah kita tentukan. Stub ini kemudiannya kita gunakan untuk menguji WeatherService
.
public class WeatherFinderStub : IWeatherFinder
{
public string FindForCity(string city)
{
return "Cloudy";
}
}
// Dalam unit test
void TestWeatherServiceCanReturnWeatherForIp()
{
var svc = new WeatherService(new CityFinder(), new WeatherFinderStub()); // gunakan stub
// Lakukan asserts
}
Dependency Injection Container
Depedency mungkin boleh jadi kompleks contohnya jika WeatherFinder
itu sendiri perlu bergantung pada value atau object lain. Namun dependency chain seperti ini sebenarnya adalah perkara biasa dalam sistem perisian. Agak sukar sebenarnya jika kita perlu susun sendiri semua dependency ini setiap kali ingin menggunakan class kita.
Katakan class CityFinder
telah kita ubah supaya constructor_nya menerima suatu ConnectionPool
yang menguruskan hubungan ke pangkalan data. Jadi CityFinder
tidak perlu tahu server mana yang perlu dihubungi. WeatherWebService
pula menerima URL untuk webservice itu supaya kita mudah melakukan konfigurasi. Ini bermakna _dependency kepada hubungan pangkalan data atau URL ditelah dilonggarkan dari class yang asal.
Situasi seperti ini dapat dipermudahkan dengan menggunakan framework-framework DI Container atau kadangkala disebut IoC Container. DI Container biasanya ada fungsi automatic injection untuk menguruskan dependency secara automatik. Contoh framework seperti ini yang ada di dalam dunia .NET adalah seperti Castle Windsor, Autofac, Ninject, dan sebagainya.
Container ini biasanya berfungsi seperti berikut:
// DiContainer adalah sebuah framework khayalan
var container = new DiContainer();
container.Register<IConnectionPool>().Using<ConnectionPool>()
.WithParamater("connString", "server1...");
container.Register<ICityFinder>().Using<CityFinder>();
container.Register<IWeatherFinder>().Using<WeatherWebService>()
.WithParameter("url", "http://webservice/...");
container.Register<IWeatherService>().Using<WeatherService>();
// Panggil dari applikasi utama
var svc = container.GetObject<IWeatherService>(); // Dapatkan WeatherService dari container
svc.ShowForIp("10.10.10.10");
Container akan mengambil tugas membina WeatherService
dan menyambungkan semua dependency nya supaya ia dapat hidup dan berfungsi. Kita tidak perlu menguruskannya sendiri.
Kesimpulan
Dependency Injection tidak akan dapat difahami jika kita tidak memahami apa itu dependency dan bagaimana ia boleh wujud. Setelah memahaminya, Dependency Injection dapat digunakan untuk mengurus dependency dan seterusnya membantu kita mencapai modularity dan flexibility yang diidam-idamkan dalam sistem kita. Gunakan juga DI Container untuk memudahkan tugas kita menghubungkan dependency antara object.
NOTA: Kod dalam artikel ini adalah khusus untuk memahami Dependency Injection. Ia mungkin mengabaikan aspek lain seperti keselamatan dan sebagainya.
Perbincangan mengenai artikel ini ada di sini.
Built with git-ssb-web