# 模式篇:設計與架構
設計模式算是在OO中比較有趣的東西,特別是對于如我之類的用得不是很多的,雖然有時候也會用上,但是并不知道用的是怎樣的模式。之前了解了幾天的設計模式,實際上也就是將平常經常用到的一些東西進行了總結,如此而已,學習設計模式的另外一個重要的意義在于,我們使用了設計模式的時候我們會知道自己使用了,并且還會知道用了是怎樣的設計模式。
至于設計模式這個東西和有些東西一樣,是發現的而不是發明的,換句話說,我們可以將經常合到一起的幾種模式用一個新的模式來命名,它是復合模式,但是也可以用別的模式來命名。
設計模式算是簡化了我們在面向對象設計時候的諸多不足,這個在系統設計的初期有時候會有一定的作用,不過多數時候對于我來說,會用上他的時候,多半是在重構的時候,因為不是很熟悉。
### 觀察者模式
觀察者模式又叫做發布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。
觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。
一個軟件系統常常要求在某一個對象的狀態發生變化的時候,某些其它的對象做出相應的改變。做到這一點的設計方案有很多,但是為了使系統能夠易于復用,應該選擇低耦合度的設計方案。減少對象之間的耦合有利于系統的復用,但是同時設計師需要使這些低耦合度的對象之間能夠維持行動的協調一致,保證高度的協作(Collaboration)。觀察者模式是滿足這一要求的各種設計方案中最重要的一種。
簡單的來說,就是當我們監測到一個元素變化的時候,另外的元素依照此而改變。
### Ruby觀察者模式
Ruby中為實現Observer模式提供了名為observer的庫,observer庫提供了Observer模塊。 其API如下所示
方法名 | 功 能 ——-|—————– add_observer(observer) | 添加觀察者 delete_observer(observer) | 刪除特定觀察者 delete_observer | 刪除觀察者 count_observer | 觀察者的數目 change(state=true) | 設置更新標志為真 changed? | 檢查更新標志 notify_observer(*arg) | 通知更新,如果更新標志為真,調用觀察者帶參數arg的方法
#### Ruby觀察者簡單示例
這里要做的就是獲取一個json數據,將這個數據更新出來。
獲取json數據,同時解析。
~~~
require 'net/http'
require 'rubygems'
require 'json'
class GetData
attr_reader:res,:parsed
def initialize(uri)
uri=URI(uri)
@res=Net::HTTP.get(uri)
@parsed=JSON.parse(res)
end
def id
@parsed[0]["id"]
end
def sensors1
@parsed[0]["sensors1"].round(2)
end
def sensors2
@parsed[0]["sensors2"].round(2)
end
def temperature
@parsed[0]["temperature"].round(2)
end
def led1
@parsed[0]["led1"]
end
end
~~~
下面這個也就是重點,和觀察者相關的,就是被觀察者,由這個獲取數據。 通過changed ,同時用notify_observer方法告訴觀察者
~~~
require 'rubygems'
require 'thread'
require 'observer'
require 'getdata'
require 'ledstatus'
class Led
include Observable
attr_reader:data
def initialize
@uri='http://www.xianuniversity.com/athome/1'
end
def getdata
loop do
changed()
data=GetData.new(@uri)
changed
notify_observers(data.id,data.sensors1,data.sensors2,data.temperature,data.led1)
sleep 1
end
end
end
~~~
然后讓我們新建一個觀察者
~~~
class LedStatus
def update(arg,sensors1,sensors2,temperature,led1)
puts "id:#{arg},sensors1:#{sensors1},sensors2:#{sensors2},temperature:#{temperature},led1:#{led1}"
end
end
~~~
測試
~~~
require 'spec_helper'
describe LedStatus do
let(:ledstatus){LedStatus.new()}
describe "Observable" do
it "Should have a result" do
led=Led.new
led.add_observer(ledstatus)
led.getdata
end
end
end
~~~
測試結果如下所示
~~~
phodal@linux-dlkp:~/tw/observer> rake
/usr/bin/ruby1.9 -S rspec ./spec/getdata_spec.rb ./spec/ledstatus_spec.rb
id:1,sensors1:22.0,sensors2:11.0,temperature:10.0,led1:0
id:1,sensors1:22.0,sensors2:11.0,temperature:10.0,led1:1
id:1,sensors1:22.0,sensors2:11.0,temperature:10.0,led1:0
id:1,sensors1:22.0,sensors2:11.0,temperature:10.0,led1:1
id:1,sensors1:22.0,sensors2:11.0,temperature:10.0,led1:1
id:1,sensors1:22.0,sensors2:11.0,temperature:10.0,led1:1
~~~
使用Ruby自帶的Observer庫的優點是,讓我們可以簡化相互之間的依賴性。同時,也能簡化程序的結構,相比于自己寫observer的情況下。
### Node.js 簡單工廠模式
> 從設計模式的類型上來說,簡單工廠模式是屬于創建型模式,又叫做靜態工廠方法(Static Factory Method)模式,但不屬于23種GOF設計模式之一。簡單工廠模式是由一個工廠對象決定創建出哪一種產品類的實例。簡單工廠模式是工廠模式家族中最簡單實用的模式,可以理解為是不同工廠模式的一個特殊實現,學習了此模式可以為后面的很多中模式打下基礎。
當我發現我在代碼中重復寫了很多個if來判斷選擇那個數據庫的時候。于是,我就想著似乎這就可以用這個簡單工廠模式來實現SQLite3與MongoDB的選擇。
### MongoDB Helper與SQLite Helper類重復
對于我們的類來說是下面這樣子的:
~~~
function MongoDBHelper() {
'use strict';
return;
}
MongoDBHelper.deleteData = function (url, callback) {
'use strict';
...
};
MongoDBHelper.getData = function (url, callback) {
'use strict';
...
};
MongoDBHelper.postData = function (block, callback) {
'use strict';
...
};
MongoDBHelper.init = function () {
'use strict';
...
};
module.exports = MongoDBHelper;
~~~
然而,我們可以發現的是,對于我們的SQLiteHelper來說也是類似的
~~~
SQLiteHelper.init = function () {
'use strict';
...
};
SQLiteHelper.postData = function (block, callback) {
'use strict';
...
};
SQLiteHelper.deleteData = function (url, callback) {
'use strict';
...
};
SQLiteHelper.getData = function (url, db_callback) {
'use strict';
...
};
module.exports = SQLiteHelper;
~~~
想來想去覺得寫一個父類似乎是沒有多大意義的,于是用了簡單工廠模式來解決這個問題。
總之,就是我們可以用簡單工廠模式來做一個DB Factory,于是便有了
~~~
var MongoDBHelper = require("./mongodb_helper");
var SQLiteHelper = require("./sqlite_helper");
var config = require('../../iot').config;
function DB_Factory() {
'use strict';
return;
}
DB_Factory.prototype.DBClass = SQLiteHelper;
DB_Factory.prototype.selectDB = function () {
'use strict';
if (config.db === 'sqlite3') {
this.DBClass = SQLiteHelper;
} else if (config.db === "mongodb") {
this.DBClass = MongoDBHelper;
}
return this.DBClass;
};
module.exports = DB_Factory;
~~~
這樣我們在使用的時候,便可以:
~~~
var DB_Factory = require("./lib/database/db_factory");
var db_factory = new DB_Factory();
var database = db_factory.selectDB();
database.init();
~~~
由于是直接由配置中讀取進去的,這里的selectDB就不需要參數。
### Java Template Method(模板方法)
原本對于設計模式的寫作還不在當前的計劃中,然而因為在寫TWU作業的時候,覺得代碼寫得不好,于是慢慢試著一點點重構,重新看著設計模式。也開始記錄這一點點的方法,至少這些步驟是必要的。
### 從基本的App說起
對于一個基本的C/C++/Java/Python的Application來說,他只需要有一個Main函數就夠了。對于一個好一點的APP來說,他可能是下面的步驟,
~~~
main(){
init();
while(!condition()){
do();
}
}
~~~
上面的代碼是我在學51/AVR等各式嵌入式設備時,經常是按上面的寫法寫的,對于一個更符合人性的App來說他應該會有一個退出函數。
~~~
main(){
init();
while(!condition()){
do();
}
exit();
}
~~~
于是很幸運地我找到了這樣的一個例子。
過去看過Arduino的代碼,了解過他是如何工作的,對于一個Arduino的代碼來說,必要的兩個函數就是。
~~~
void setup() {
}
void loop() {
}
~~~
setup()函數相當于上面的init(),而loop()函數剛相當于上面的do()。似乎這就是我們想要的東西,看看Arduino目錄中的Arduino.h就會發現,如下的代碼(刪減部分代碼)
~~~
#include <Arduino.h>
int main(void)
{
init();
setup();
for (;;) {
loop();
if (serialEventRun) serialEventRun();
}
return 0;
}
~~~
代碼中的for(;;)看上去似乎比while(True)容易理解得多,這也就是為什么嵌入式中經常用到的是for(;;),從某種意義上來說兩者是等價的。再有不同的地方,就是gcc規定了,main()函數不能是void。so,兩者是差不多的。只是沒有,并沒有在上面看到模板方法,等等。我們在上面所做的事情,便是創建一個框架。
### Template Method
> **模板方法**: 在一方法中定義一個算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結構的情況下,重新定義算法中的某些步驟。
對于我來說,我就是在基本的App中遇到的情況是一樣的,在我的例子中,一開始我的代碼是這樣寫的。
~~~
public static void main(String[] args) throws IOException {
initLibrary();
while(!isQuit){
loop();
}
exit;
}
protected void initLibrary(); {
System.out.println(welcomeMessage);
}
protected void loop() {
String key = "";
Scanner sc = new Scanner(System.in);
key = sc.nextLine();
System.out.println(results);
if(key.equals("Quit")){
setQuit();
}
}
protected void exit() {
System.out.println("Quit Library");
}
~~~
只是這樣寫感覺很是別扭,看上去一點高大上的感覺,也木有。于是,打開書,找找靈感,就在《敏捷軟件開發》一書中找到了類似的案例。Template Method模式可以分離能用的算法和具體的上下文,而我們通用的算法便是。
~~~
main(){
init();
while(!condition()){
do();
}
exit();
}
~~~
看上去正好似乎我們當前的案例,于是便照貓畫虎地來了一遍。
### Template Method實戰
創建了一個名為App的抽象基類,
~~~
public abstract class App {
private boolean isQuit = false;
protected abstract void loop();
protected abstract void exit();
private boolean quit() {
return isQuit;
}
protected boolean setQuit() {
return isQuit = true;
}
protected abstract void init();
public void run(){
init();
while(!quit()){
loop();
}
exit();
}
}
~~~
而這個也和書中的一樣,是一個通用的主循環應用程序。從應用的run函數中,可以看到主循環。而所有的工作也都交付給抽象方法,于是我們的LibraryApp就變成了
~~~
public class LibraryApp extends App {
private static String welcomeMessage = "Welcome to Biblioteca library";
public static void main(String[] args) throws IOException {
(new LibraryApp()).run();
}
protected void init() {
System.out.println(welcomeMessage);
}
protected void loop() {
String key = "";
Scanner sc = new Scanner(System.in);
key = sc.nextLine();
if(key.equals("Quit")){
setQuit();
}
}
protected void exit() {
System.out.println("Quit Library");
}
}
~~~
然而,如書中所說`這是一個很好的用于示范TEMPLATE METHOD模式的例子,卻不是一個合適的例子。`
### Hadoop Pipe and Filters模式
繼續碼點關于架構設計的一些小心得。架構是什么東西并沒有那么重要,重要的是知道它存在過。我會面對不同的架構,有一些不同的想法。一個好的項目通常是存在一定的結構,就好像人們在建造房子的時候也都會有結構有一樣。
我們看不到的架構,并不意味著這個架構不存在。
### Unix Shell
最出名的Pipe便是Unix中的Shell
**管道(英語:Pipeline)是原始的軟件管道:即是一個由標準輸入輸出鏈接起來的進程集合,所以每一個進程的輸出(stdout)被直接作為下一個進程的輸入(stdin)。 每一個鏈接都由未命名管道實現。過濾程序經常被用于這種設置。**
所以對于這樣一個很好的操作便是,統計某種類型的文件的個數:
~~~
ls -alh dot | grep .dot | wc -l
~~~
在執行
~~~
ls -alh dot
~~~
的輸出便是下一個的輸入,直至最后一個輸出。
這個過程有點類似于工廠處理廢水,

pipe and filter
上圖是一個理想模型~~。
一個明顯地步驟是,水中的雜質越來越少。
### Pipe and Filter模式
**Pipe and Filter**適合于處理數據流的系統。每個步驟都封裝在一個過濾器組件中,數據通過相鄰過濾器之間的管道傳輸。
- **pipe**: 傳輸、緩沖數據。
- **filter**: 輸入、處理、輸出數據。
這個處理過程有點類似于我們對數據庫中數據的處理,不過可不會有這么多步驟。
### Fluent API
這個過程也有點類似于Fluent API、鏈式調用,只是這些都是DSL的一種方式。
流暢接口的初衷是構建可讀的API,畢竟代碼是寫給人看的。
類似的,簡單的看一下早先我們是通過方法級聯來操作DOM
~~~
var btn = document.createElement("BUTTON"); // Create a <button> element
var t = document.createTextNode("CLICK ME"); // Create a text node
btn.appendChild(t); // Append the text to <button>
document.body.appendChild(btn); // Append <button> to <body>
~~~
而用jQuery寫的話,便是這樣子
~~~
$('<span>').append("CLICK ME");
~~~
等等
于是回我們便可以創建一個簡單的示例來展示這個最簡單的DSL
~~~
Func = (function() {
this.add = function(){
console.log('1');
return this;
};
this.result = function(){
console.log('2');
return this;
};
return this;
});
var func = new Func();
func.add().result();
~~~
然而這看上去像是表達式生成器。
### DSL 表達式生成器
> 表達式生成器對象提供一組連貫接口,之后將連貫接口調用轉換為對底層命令-查詢API的調用。
這樣的API,我們可以在一些關于數據庫的API中看到:
~~~
var query =
SQL('select name, desc from widgets')
.WHERE('price < ', $(params.max_price), AND,
'clearance = ', $(params.clearance))
.ORDERBY('name asc');
~~~
鏈式調用有一個問題就是收尾,同上的代碼里面我們沒有收尾,這讓人很迷惑。。加上一個query和end似乎是一個不錯的結果。
### Pipe and Filter模式實戰
所以,這個模式實際上更適合處理數據,如用Hadoop處理數據的時候,我們會用類似于如下的方法來處理我們的數據:
~~~
A = FOREACH LOGS_BASE GENERATE ToDate(timestamp, 'dd/MMM/yyyy:HH:mm:ss Z') as date, ip, url,(int)status,(int)bytes,referrer,useragent;
B = GROUP A BY (timestamp);
C = FOREACH B GENERATE FLATTEN(group) as (timestamp), COUNT(A) as count;
D = ORDER C BY timestamp,count desc;
~~~
每一次都是在上一次處理完的結果后,再處理的。
### 其他
參考書目
- 《Head First 設計模式》
- 《設計模式》
- 《敏捷軟件開發 原則、模式與實踐》
- 《 面向模式的軟件架構:模式系統》
- 《Java應用架構設計》