2014年6月8日 星期日

Feature-oriented Programming with Node.js

Standard
Feature-oriented Programming(FOP)這個名詞對很多人來說應該相當陌生,這是一個鮮少人討論的開發模式,尋找了一下,也找不太到中文的說明和翻譯,所以我就暫時稱它為『特色導向程式開發』。如果你有興趣,可以去搜尋它,或使用關鍵字『Feature-oriented Software Development』找到更多資源。也有一個以 FOP 概念所改良的 C++ 語言『FeatureC++』,可以參考。

因為筆者眼前專注於 Node.js 的發展,所以也用 Node.js 實作了一非常個簡單的 Framework 『wag.js』,讓我們可以在 JavaScript 語言的環境下,引用 FOP 的開發概念來設計軟體。你可以用 npm 直接安裝他:

npm install wag.js

之所以會一時興起研究 FOP,是因為多年以來,一直為了重覆開發所苦,很多相同的東西一直不停重覆做了又做,總覺得自己一直在浪費時間,所以想試著尋找個方法來改善眼前的窘境。最麻煩的問題在於,雖然早就應用了許多模組化方法,我們也能把很多功能事先做好打包好,但仍然像個有數不清節點的連連看遊戲,讓人花不少時間在上面編織程式軟體。重點是,當其中有絕大多數的過程是一樣的時候,總讓人做得很無力。

你可以想一想,當你想要寫一個新的專案時,你通常最大的阻力是什麼?又有多少繁雜的事,每次開新專案時都重覆在做?而最令人煩燥的是,因為專案類型的不同,有些微的差異,所以難以用一個標準自動化的方式去完成。

這也是一般人和工程師最大的差別,甚至是造成溝通不良。一般人總是想著是我要什麼功能、再增加哪些功能;工程師則是一直在想著,我要挑選使用哪些功能模組,又該怎麼從數不清的可能中,選一個可行的方法組裝他們?如果想成捏黏土來比喻,一般人會捏個大概形狀,再往上慢慢堆想要的東西,慢慢趨近成果,而工程師會捏好各個部位,再把他一一拼湊起來。

我想,當你能回憶起客戶或老闆跟你說:『這功能很簡單,你應該能馬上做好』,你應該就能更明白我所說的,一般人與工程師的思維差異。不過,事實上,工程師思維和一般人沒兩樣,也是把東西堆起來,只是,工程師所設計出來的各種部位的元件,總是很難拼湊在一起,必需要花些功夫。

到底,有沒有什麼辦法,可以減少各種元件和功能模組的拼湊困難呢?讓我們可以更專心的做一個有更多功能的產品?或是更快將產品捏出來呢?

經過一些試驗後發現,Feature-oriented Programming 是個很值得嘗試的開發模式,概念如其名特色功能導向(Feature-oriented),選擇需要或想實作的產品特色後,再做細節修正。你可以到 wag.js 的 examples 目錄找到一些範例,會更瞭解其概念。


Hello World

首先看 helloworld 這支範例程式:
var Wag = require('wag.js');
var wag = new Wag();

// Create an app with web engine
var app = wag.mixin('web');

// Run app
app.run(function() {
        app.engine.listen(8000);
});
這支程式執行後,會運行一支 Web Server 在 8000 port,若使用瀏覽器去開,則會得到一個 『Hello world!』的訊息。

程式的執行邏輯很簡單:建立一個 wag.js 的物件,然後用這個物件去建立一個 app,並為其引入 web 引擎,最後將這個 app 跑起來初始化,待初使化完成後就去跑 app 裡的 listen() ,開始監聽 8000 port。

比較特別要說明的部份,是在建立 app 時,便要使用 mixin() 選擇需要引入哪些引擎功能。因為大多數 Node.js 開發者對 Express 非常熟悉,且有高需求量,所以筆者已經設計了一個名為『web』的功能模組來包裝了 Express ,以便示範和供多數開發者使用。為了與 Node.js 本身的『模組』一詞區隔,在 wag.js 裡面便把它叫做引擎(Engine)。

更複雜的應用程式

若是要開發更複雜的應用程式,我們除了引入 wag.js 已經內建的 web 引擎外,還可以以 web 引擎為基礎,做擴充、修改或是引伸設計。接下來的另一個範例 guestbook (留言板)就可以說明:
var Wag = require('wag.js');

var app = new Wag();
app.addPath('engineDirs', __dirname + '/engines');
app.mixin([
    'web',
    'guestbook'
]);

app.run(function() {
   app.engine.listen();
});
和前一個 helloworld 相較之下,我們多指定一個路徑,讓 wag.js 去找到我們自定的引擎(Engine)載入。然後除了 web 引擎之外,我們也另外增加了一個自定的 guestbook 引擎去擴充 app 的功能,讓 app 可以顯示留言的內容。

如前文所提到,開發概念上,就是在一開始選擇並引入想要的功能,創造一個合成好的程式,最後再去執行和啟動這個程式。

wag.js 尋找並載入引擎的順序是:

  1. wag.js 內建的引擎
  2. 開發者指定的路徑


在這個範例中, wag.js 會載入三個引擎的程式碼然後合成:

  1. wag.js 內建的 web 引擎
  2. 開發者自定的 web 引擎
  3. 開發者自定的 guestbook 引擎

基於 web 引擎的擴充

你可以在 engines 裡面找到一個自定的 web 引擎,事實上,如果你比對了 wag.js 內建的 web 引擎後就會發現,我們並非是在重寫一個新的,而是繼承了 wag.js 內建的 web 引擎後,修改並加上了一些功能。

wag.js 內建原生的 web 引擎長下面這模樣:
var express = require('express');

var Web = module.exports = function() {
        var self = this;

        self.express = express();
        self.server = null;
};

Web.prototype.configure = function(callback) {
        var self = this;

        callback();
};

Web.prototype.routers = function(callback) {
        var self = this;

        self.express.get('/', function(req, res) {
                res.send('Hello! Wag Web!');
        });

        callback();
};

Web.prototype.listen = function(port) {
        var self = this;

        var _port = port || Web.Wag.settings.web.port;

        self.configure(function() {
                self.routers(function() {
                        self.server = self.express.listen(_port);
                });
        });
};

我們在 app 中自定的引擎:
var path = require('path');
var express = require('express');
var bodyParser = require('body-parser');

var Web = module.exports = function() {
        var self = this;

        Web.super_.call(this);
};

Web.prototype.configure = function(callback) {
        var self = this;

        self.express.set('view engine', 'jade');
        self.express.set('views', path.join(__dirname, '..', '..', 'views'));
        self.express.use(express.static(path.join(__dirname, '..', '..', 'public')));
        self.express.use(bodyParser());

        callback();
};

Web.prototype.routers = function(callback) {

        // Clear route settings
        callback();
};
當這兩支引擎合成後,會和下面的程式有相同功能:
var express = require('express');

var Web = module.exports = function() {
        var self = this;

        self.express = express();
        self.server = null;
};

Web.prototype.configure = function(callback) {
        var self = this;
        self.express.set('view engine', 'jade');
        self.express.set('views', path.join(__dirname, '..', '..', 'views'));
        self.express.use(express.static(path.join(__dirname, '..', '..', 'public')));
        self.express.use(bodyParser());
        callback();
};

Web.prototype.routers = function(callback) {
        var self = this;


        callback();
};

Web.prototype.listen = function(port) {
        var self = this;

        var _port = port || Web.Wag.settings.web.port;

        self.configure(function() {
                self.routers(function() {
                        self.server = self.express.listen(_port);
                });
        });
};

這支自定的 web 引擎程式,大概有三個部份要說明:


首先,運用 Web.super_.call(),讓引擎被初始化時,可以引用 wag.js 內建的建構子(Constructor),而不是直接取代 wag.js 內建的 web 引擎。

接著,取代原生的 configure 方法,為其加上 Jade 和 view 的支援,也讓 express 可以存取 public 路徑底下的靜態文件,也支援 body 的處理。

將原生的 routers 方法給取代並不做任何事,讓原生引擎中對應『/』路徑的 hello world 訊息失效。(我們會在稍後的 guestbook 引擎內設計新的路徑和頁面內容)

建立一個新的引擎,為應用程式增加功能

既然要做留言板,就少不了顯示留言和張貼留言的功能,為了增加這些功能,我們可以設計一個新的引擎『guestbook』來達成。

var Guestbook = module.exports = function() {
        var self = this;

        Guestbook.super_.call(this);

        self.articles = [
                {
                        title: 'What\'s Wag.js',
                        text: 'Wag.js is a feature-oriented based application framework.'
                },
                {
                        title: 'What\'s News',
                        text: 'Guestbook example was added.'
                }
        ];
};

Guestbook.prototype.routers = function(callback) {
        var self = this;

        function routeInit() {

                self.express.get('/', function(req, res) {
                        res.render('index', {
                                articles: self.articles
                        });
                });

                self.express.get('/post', function(req, res) {
                        res.render('post');
                });

                self.express.post('/post', function(req, res) {

                        if (!req.body.title || !req.body.text) {
                                res.end('Please enter title and message!');
                                return;
                        }

                        // Save to article database
                        self.articles.push({
                                title: req.body.title,
                                text: req.body.text
                        });

                        res.redirect('/');
                });

                callback();
        }

        Guestbook.super_.prototype.routers.apply(this, [ routeInit ]);
};

簡單來說,guestbook 引擎,也是繼承了之前的 web 引擎再做擴充,所以可以看到在一開始的建構子(Constructor)中,也用到了 super_.call()。然後我們定義了一個名為『articles』的 Member,做為存放留言使用。

為了要顯示網頁,想當然爾要定義路徑和將回傳的網頁內容,分別為『/』、『/post』。而你可以看到的 Guestbook.super_.prototype.routers.apply(),只是為了繼承之前 web 引擎的 routers 方法,避免覆概舊有的功能(雖然之前的 web 引擎內的 routers 已經沒有任何功能,所以要省略也可以)。

總結


p1 = j • f       -- program p1 has features j and f

在 FOP 的概念下,應用程式就是許多元件(在 Wag.js 之下是以引擎 Engine 為單位)所拼成的一個大集合體,如果你有 OOP (物件導向的概念)你可以想像它是一個不斷串接並繼承下去的物件,此外,引入的先後順續是有差別的,這會牽涉到相依性的問題。

FOP 能讓開發者如預期的慢慢把功能疊上去,如果設計得當,同一個功能可以拿到各種專案上去使用,只需要讓應用程式引入便馬上可以運作。如筆者最近正在嘗試用 wag.js 開發數個完全不同用途的網站專案,都會用到會員註冊和管理的功能。但藉由引擎這樣的設計,只需要將直接將功能搬移到不同專案中,就可以讓會員系統在不同專案中運作,而且具有擴充的彈性,而不需要為不同專案重新開發。

後記


wag.js 尚未完全實做 FOP 的所有概念,只是粗淺的起頭而已。

未來應該要讓引擎本身,也能像 app 那樣,可以用數個引擎合成出來。

若是 wag.js 這樣的開發概念可行,之後也許可以推出像 npm 這樣的線上模組系統也說不定。