Dependency Injection là mẫu hình thiết kế hiện đại, có mặt trong phần lớn các khung ứng dụng bằng những ngôn ngữ lập trình khác nhau hiện nay. AngularJS cũng không là ngoại lệ. Việc hiểu rõ về Dependency Injection, đặc biệt trong Javascript đóng một vai trò hết sức quan trọng trong việc nắm vững và sử dụng thành thạo AngularJS.

Nội dung trình bày

Khái lược về Dependency Injection

Trước khi tìm hiểu cách thức Dependency Injection được cài đặt và vận hành trong AngularJS như thế nào, chúng ta ôn lại sơ lược về khái niệm Dependency Injection và cách áp dụng mẫu hình thiết kế này trong Javascript.

Sự phụ thuộc giữa các đối tượng

Lập trình hướng đối tượng (OOP) là phương pháp chia tách dữ liệu và mã lệnh trong chương trình phần mềm thành các nhóm, sau đó đóng gói các nhóm này thành các khối cô lập gọi là đối tượng - tương ứng với các thực thể ngoài đời thực có liên quan đến chương trình phần mềm. Theo cách này, chương trình phần mềm có thể được hình dung như là một quần thể các đối tượng tương tác với nhau.

Chương trình là quần thể các đối tượng tương tác với nhau

Các đối tượng tương tác với nhau thông qua việc truyền mesage dưới dạng một lời gọi hàm. Đối tượng chứa hàm (tức là đối tượng sẽ nhận message) được gọi là callee object. Đối tượng gọi hàm (là đối tượng gửi message) được gọi là caller object.

Các đối tượng tương tác với nhau thông qua gọi hàm

Có 3 cách để caller object có thể tham chiếu đến được callee object để có thể thực hiện lời gọi hàm:

Cách 1. caller object có thể tạo ra callee object bên trong hàm khởi tạo của Caller, thông thường là áp dụng toán tử new đối với hàm khởi tạo của Callee.

function Callee(calleeName) {
    var myname = calleeName;
    this.receiveACall = function(name) {
        alert("Hello " + myname + ", This's " + name);
    }
}

function Caller(callerName) {
    var myname = callerName;
    var callee = new Callee("Viet");
    this.makeACall = function() {
        callee.receiveACall(myname);
    }
}

var caller = new Caller("Nam");
caller.makeACall();

Dễ nhận thấy mã lệnh sử dụng để xác định callee object gắn kèm với mã lệnh khởi tạo callee object, dẫn đến những chỉnh sửa thông số khởi tạo Callee sau này sẽ làm ảnh hướng đến Caller.

Cách 2. caller object có thể truy cập đến callee object khi cần, bằng cách tham chiếu đến một biến toàn cục.

function Callee(calleeName) {
    var myname = calleeName;
    this.receiveACall = function(name) {
        alert("Hello " + myname + ", This's " + name);
    }
}

function Caller(callerName) {
    var myname = callerName;
    this.makeACall = function() {
        a_global_variable.receiveACall(myname);
    }
}

var a_global_variable = new Callee("Viet");
var caller = new Caller("Nam");
caller.makeACall();

Theo cách này, hàm khởi tạo của Caller lại phụ thuộc vào biến toàn cục (là nhân tố thứ ba, nằm ngoài phạm vi caller objectcallee object). Điều này là “cấm kỵ” trong lập trình hướng đối tượng.

Cách 3. caller object được truyền tham chiếu trỏ đến callee object vào nơi mà nó cần, chẳng hạn vào hàm khởi tạo, hoặc thông qua phương thức setter.

function Callee(calleeName) {
    var myname = calleeName;
    this.receiveACall = function(name) {
        alert("Hello " + myname + ", This's " + name);
    }
}

function Caller(callerName, calleeRef) {
    var myname = callerName;
    var callee = calleeRef;
    
    this.setCallee = function(calleeRef) {
        callee = calleeRef;
    }
    
    this.makeACall = function() {
        callee.receiveACall(myname);
    }
}

var callee = new Callee("Viet");
var caller = new Caller("Nam", callee);
caller.makeACall();

Theo cách này, hàm khởi tạo Caller chỉ phụ thuộc vào hàm mà nó gọi (đây là điều không thể tránh khỏi được), chứ không còn phụ thuộc vào lệnh khởi tạo callee object. callee object được tạo ở bên ngoài và truyền vào caller object thông qua hàm khởi tạo. Đây chính là một trong những cách để đẩy mã lệnh phụ thuộc ra bên ngoài định nghĩa đối tượng.

Nhận xét chung: Hai cách đầu, thường được gọi là viết cứng mã lệnh (hard code) của đối tượng phụ thuộc (callee object) vào bên trong caller object. Điều này sẽ gây khó khăn cho việc chỉnh sửa những phần phụ thuộc: mã lệnh khởi tạo, tên lớp đối tượng, tên biến toàn cục, v.v. Ngoài ra, cũng rất khó khăn trong việc thực hiện Unit Test, là kỹ thuật đòi hỏi phải cô lập các thành phần phụ thuộc.

Vậy, Dependency Injection là gì?

Cài đặt Dependency Injection

Có rất nhiều cách để áp dụng Dependency Injection tùy thuộc vào đặc trưng của ngôn ngữ lập trình. Đối với Javascript, chúng ta có thể sử dụng Service Locator Injection hoặc Constructor Injection hoặc kết hợp cả 2 cách này để xây dựng Dependency Injection.

Những cách thức áp dụng Dependency Injection

Service Locator Injection

Service Locator là mẫu hình thiết kế được dùng để khởi tạo các Service và tra cứu các Service thông qua tên gọi của nó. Đoạn mã lệnh dưới đây khai báo hàm cấu tử của một đối tượng ServiceLocator.

function ServiceLocator() {
    this.services = {};
    
    this.get = function(name) {
        var svc = this.services[name];
        if (!svc) { 
            throw new Error('No service registered with name: ' + name);
        }
        return svc;
    };
    
    this.register = function(name, svc) {
        var existing = this.services[name];
        if (existing) { 
            throw new Error('Service[' + name + '] already registered');
        }
        this.services[name] = svc;
        this.services[name].$locator = this;
        return this;
    };
    
    this.unregister = function(name) {
        delete this.services[name];
        return this;
    };

    this.reset = function() {
        this.services = {};
        return this;
    };
}

Đoạn mã lệnh tiếp theo khai báo các hàm cấu tử của 2 lớp đối tượng: CallerCallee.

function Callee(myname) {
    this.myname = myname;
    this.receiveACall = function(name) {
        alert("Hello " + this.myname + ", This's " + name);
    }
}

function Caller(myname) {
    this.myname = myname;
    this.makeACall = function() {
        this.$locator.get("callee").receiveACall(this.myname);
    }
}

Cuối cùng là mã lệnh sử dụng $locator để đăng ký các đối tượng và truyền đối tượng callee object cho caller object sử dụng:

var $locator = new ServiceLocator();
$locator.register("callee", new Callee("Viet"));
$locator.register("caller", new Caller("Nam"));
$locator.get("caller").makeACall();

Với cách này, rõ ràng caller object chỉ còn phụ thuộc vào lệnh gọi hàm (cụ thể là hàm receiveACall()) của callee object. Lệnh khởi tạo đối tượng callee object đã “bị” đẩy ra bên ngoài.

Automated Dependency Injection

Ví dụ trong mục Cách 3 ở phần trên, minh họa cho kỹ thuật Constructor Injection trong đó callee object được truyền vào cho caller object dưới dạng tham số của hàm khởi tạo (Constructor) của Caller. Tuy nhiên, trong ví dụ đó, việc khởi tạo các đối tượng, truyền đối tượng vào hàm khởi tạo đều thực hiện “bằng tay”.

Phần này trình bày một ví dụ nhỏ, Chúng ta sẽ xem xét quá trình bơm truyền tự động trong ví dụ minh họa bên dưới.

var $injector = new function InjectorClass() {
    
    var dependencies = {};
    
    var retrieve = function(name) {
        var record = dependencies[name];
        if (record == null) {
            throw new Error('No service registered with name: '+name);
        }
        if (record['cache'] == null) convert(name);
        return record['cache'];
    };
    
    var convert = function(name) {
        var record = dependencies[name];
        if (record['type'] == 'service') {
            record['cache'] = instantiate(record['value']);
        } else {
            record['cache'] = record['value'];
        }
    };
    
    var instantiate = function(fn) {
        var wrapper = function(args) {
            return fn.apply(this, args);
        };
        wrapper.prototype = fn.prototype;
        return new wrapper(getDependencies(fn));
    };
    
    var getDependencies = function(fn) {
        var params = getParameters(fn);
        return params.map(function(value) {
            return retrieve(value);
        });
    };

    var getParameters = function(fn) {
        var args = fn.toString()
          .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg,'')
          .match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
          .split(/,/);
        if (args.length == 1 && args[0] == "") return [];
        return args;
    };

    this.registerObject = function(name, dependency) {
        dependencies[name] = {type: 'object', value: dependency};
        return this;
    };

    this.defineService = function(name, dependency) {
        dependencies[name] = {type: 'service', value: dependency};
        return this;
    };
    
    this.lookup = function(name) {
        return retrieve(name);
    }
    
    this.invoke = function(target) {
        target.apply(target, getDependencies(target));
    };

    return this;
}();

Khai báo các đối tượng và dịch vụ với $injector:

$injector.registerObject('greeting', {
    greet: function() { return 'Hello world!'; }
});

$injector.defineService('service1', function(fullname) {
    var self = fullname;
    this.sayHello = function(name) {alert('Hello '+name+". I'm "+self)}; 
}).registerObject('fullname', 'Computer');

$injector.defineService('service2', function() { 
    this.sayWellcome = function(name) {alert('Wellcome to ' + name)}; 
});

Gọi phương thức $injector.lookup() để sử dụng dịch vụ hoặc gọi phương thức $injector.invoke() để bơm truyền đối tượng và dịch vụ vào hàm:

$injector.lookup('service1').sayHello('Ubuntu');

$injector.invoke(function (greeting, service1, service2) {
    document.write(greeting.greet())
    document.write('<br/>');
    service1.sayHello('Pham Ngoc Hung');
    service2.sayWellcome('Vietnam');
});

Tài liệu tham khảo

Comments