Kandungan

Memahami Dependency Injection

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.

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 constructornya 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.

Mengenai penulis

Ikhwan Hayat telah membina perisian secara professional sejak 2004. Kebanyakan masa dia bergelumang dalam platform .NET. Namun kadangkala tersesat pergi ke dunia PHP, Python, Ruby, Java, dan bahasa-bahasa pengaturcaraan lain. Sekarang berkerja sebagai pengaturcara secara freelance sepenuh masa.

Ikhwan Hayat has been developing softwares professionally since 2004. Main area of expertise is .NET, but often found getting lost in the world of PHP, Python, Ruby, Java, or some esoteric programming language. Available as a full-time freelance developer.

comments powered by Disqus