2014年4月14日 星期一

重構教程

重構教程


程式語言:Objective-C
IDE: Xcode 5

有關重構範例:
[來源] IOS7QRCodeDemo
[功能說明] 使用 iOS7 AVFoundation framework 編寫 QRCode 讀取功能。
[特色]不需依賴其他第三方框架

重構理由:
1. 可讀性/可維護性
2. 延展性 (封裝性/模組化能力/置換與轉換能力)
3. 安全性
4. 最佳化能力

重構步驟:

步驟一:將原始碼專案目錄建立 SCM Repository。這裡使用的是 Git。
理由:以利還原及追蹤。這是重要的第一步。

> cd ${QR Codes} 目錄
> git init .

步驟二:審視並調整群組結構
理由:更好的可讀性,加速閱讀及查找速度。

說明:
1. 和主流程相關的部份,我收納在 Main 群組,並以建議閱讀的順序排列。
2. 把 View 和 Model 分開收放。
3. 群組之間的順序也是以建議閱讀的順序排列。
4. 群組的分類方式,可依循的規範參考有以下數種選擇,我們可以擇一或混合使用:
  (1) 依照團隊慣例 (coding guideline)
  (2) 參考泛用性高的框架 (ex: Ruby on Rails)
  (3) 參考建置工具的標準建議 (ex: Maven)


步驟三:審視資訊隱藏
理由:沒必要放在 .h 檔中變成 public 資訊的程式內容,我把它們移至 .m 檔中。
(紅色部分表示「刪除」,綠色部分表示「加入」,下圖表示:將部份程式碼從 ViewController.h 移至 ViewController.m )


技巧:只要註解掉任一行宣告,IDE沒有報錯的話,就代表其沒有任何外部程式碼需要參考它,當然也就能放心進行隱藏。重點是:別做沒必要的曝光。此項技巧是我們使用 IDE 的重要理由之一。


步驟四:重構 _previewLayer.frame = _previewView.bounds

理由:這行程式碼的用意是讓 _previewLayer 的尺寸相等於 _previewView,我打算讓它能被更快看懂。例如,我希望重構結果是這樣:

[self makeSameSize:_previewView resizeView:_previewLayer];

說明:
1. 由於原式是無法進行方法抽取的,所以要分幾個程式進行重構。第一招是:加入區域變數!(紅色部份是原式,我改寫成綠色部份。然後編譯、測試。)


2.  然後進行方法的抽出。


3. 然後再把區域變數 inline 回去。


4. 可以看出來:42~43行的兩個區域變數已經不需要了,就拿掉它吧。



步驟五:協調單一函式內敘句的質量密度


理由:
1. 39, 40 行都是訊息傳送/方法呼叫,但是 42 ~ 53 行的程式碼是述句,在物理結構上就不一致。
2. 由上圖上紅色圓角矩形所示,viewDidLoad 內總共做了5件事,其中3件事是以述句的組合構成。若是把5件事都以同樣的密度陳述,用更直覺得方式表達會更好。

說明:
1. 首先,先把第3、第4件事,用抽出方法進行重構,結果如下:


先刻意留下第 61 行的第5件事,只把第 55 行及第 57 行的第3、第4件事抽出來後,停在這兒說明一件事。請看看第45行的註解,該註解在 registerNotificationForCameraOnOff 方法抽出後,顯示不太重要了!於是,我們可以將它進行移除。

這個動作將代出重構後程式碼的幾個象徵:
 (1) 每個 method 的行數均不多,而且每一行的執行目的,都有一個單一、難再切割的目的。
 (2) 幾乎可以拿掉大部份的註解:因為方法名稱已經足夠傳達程式碼的意圖了。

最後的結果如下:

這裡要注意部分是:為程式碼的閱讀者的習慣提出設想。

一般來說,我們對於文字的閱讀方向是:由上而下,由左至右。為了能讓程式碼的閱讀者有要順暢的閱讀體驗,把 viewDidLoad 方法提至最上方。原因很簡單:閱碼者應該會想知道 viewDidLoad 做了哪幾件事?然後視其需要,再決定要不要 drill down 讀下去。

然後,方法的呼叫順序與方法的擺放順序盡可能一致,有利於查找。

到了這個步驟完成,會發現重構過程式碼應該易讀、看起來乾淨,沒有不需要的註解(通常註解會用不同顏色區分,也會干擾閱讀感覺)。當然,美感方面有個人的差異,這點只能說重構者要 do my best。


步驟六:思考、追究並改善下列程式碼

理由:在 setupCaptureSession 方法中,有下列程式碼,

if (_captureSession)
        return;

這是什麼意思呢?更直覺的寫法應該是:

if (_captureSession!=nil)
        return;

那為什麼 setupCaptureSession 方法中的 _captureSession 何時會等於 nil 呢?
單看程式碼是找不出原因的。但是,若去思考 setupCaptureSession 的被呼叫位置 : viewDidLoad  方法內,大約就可以找到原因:是在防止出現 memory warning 發生時可能造成 _captureSession 不為 nil 而又重複執行一次 setupCaptureSession 的情況發生。

使用方法抽出,以 isMemoryWarningOccuredAndCaptureSessionCreatedAlready 命名之:

- (BOOL)isMemoryWarningOccuredAndCaptureSessionCreatedAlready
{
    return (_captureSession!=nil);
}

- (void)setupCaptureSession
{
    if ([self isMemoryWarningOccuredAndCaptureSessionCreatedAlready])
    {
        return;
    }

   //以下忽略
}

上面的程式碼中,雖然方法名字很長,不過重構者必須以假設:起碼,日後回來看程式碼的自己要能理解程式的真實動作及其意圖才行,而且長名在編譯後就會消失,所以用意圖明確的名稱絕對值得。此外,即使 if 或其他條件句中的述句只有一句,也最好完整加上大括號。在為數者眾、以及我自己團隊的 coding guideline 中,我都會加上此一要求。


步驟七:持續重構 setupCaptureSession method


可以看到第65行是很長的註解,67~72行是在偵測裝置是否擁有相機。若沒有就離開此 method。一樣,用方法抽出進行重構,抽出 isNoVideoCamera 方法:



步驟8:收攏整理 isMemoryWarningOccuredAndCaptureSessionCreatedAlready 和 isNoVideoCamera

理由:可以看出這兩個方法都是 setupCaptureSession 執行的前置條件,所以可以再進一步收攏,並整理好順序如下。

- (void)setupCaptureSession
{
    if ([self isMemoryWarningOccuredAndCaptureSessionCreatedAlready] ||
        [self isNoVideoCamera])
    {
        return;
    }
    
    _captureSession = [[AVCaptureSession alloc] init];
    
    //略
    
}

- (BOOL)isMemoryWarningOccuredAndCaptureSessionCreatedAlready
{
    return (_captureSession!=nil);
}

- (BOOL)isNoVideoCamera
{
    _videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    
    if (_videoDevice == nil) {
        NSLog(@"No video camera on this device!");
        return YES;
    }
    return NO;
}


步驟9:


理由:
觀察或實驗一下第64~65行,會發現:
1. 主要是為了建立、設定 _previewLayer。
2. 建立 _previewLayer 時需要傳入 _captureSession 。

其中,「需要傳入 _captureSession 」成為這兩行寫在 setupCaptureSession 方法中的唯一理由。但是 _previewLayer 本身的建立與設定 capture session 並不是絕對需要關聯在一起的同一件事。

方法:
1. 把 64,65行進行方法抽出,並且使其呼叫順序符合需求。(在 setupCaptureSession 方法後再進行呼叫)



步驟10:質疑 running property 的必要性。

- (void)stopRunning {
    if (!_running) return;
    [_captureSession stopRunning];
    _running = NO;
}

- (void)startRunning
{
    if (_running)
        return;
    [_captureSession startRunning];
    _metadataOutput.metadataObjectTypes = _metadataOutput.availableMetadataObjectTypes;
    _running = YES;
}

理由:
在上面程式碼中, 對 running 這個 property 存在的必要性,感到質疑,因為 captureSession 有一個 instance method : isRunning 應該是相同功能的方法。在單線程運行的 App 裡,像 running 這種 flag 變數的審查不太難。

說明:
1. 註解 running property 並檢視其影響範圍。
2. 以 [_captureSession isRunning] 取代 _running 並移除多餘的程式碼
3. 測試

結果如下:

#pragma mark camera methods

- (void)stopRunning {
    if (![_captureSession isRunning])
    {
        return;
    }
    [_captureSession stopRunning];
}

- (void)startRunning
{
    if ([_captureSession isRunning])
    {
        return;
    }
    [_captureSession startRunning];
    _metadataOutput.metadataObjectTypes = _metadataOutput.availableMetadataObjectTypes;
}

嗯,應該可以再進一步重構。結果如下:

#pragma mark - camera methods

- (void)captureSessionSwitchON:(BOOL)yes
{
    if (yes && ![_captureSession isRunning]) {
        [_captureSession startRunning];
    } else if ((!yes && [_captureSession isRunning])){
        [_captureSession stopRunning];
    }
}

當然,原本 stopRunning 及 startRunning 方法的引用也要跟著修改。


步驟11:質疑雙層 enumerateObjectsUsingBlock 的必要

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    NSMutableSet *foundBarcodes = [[NSMutableSet alloc] init];
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
         
         [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop) {
             NSLog(@"Metadata: %@", obj);
             if ([obj isKindOfClass:[AVMetadataMachineReadableCodeObject class]])
             {
                 AVMetadataMachineReadableCodeObject *code = (AVMetadataMachineReadableCodeObject*)[_previewLayer transformedMetadataObjectForMetadataObject:obj];
                 Barcode *barcode = [self processMetadataObject:code];
                 [foundBarcodes addObject:barcode];
             }
         }];
         
         dispatch_sync(dispatch_get_main_queue(), ^{
             ...(略)
             }];
             
             
        });
         
     }];

說明:

1. 原程式中在 captureOutput:didOutputMetadataObjects:fromConnection 中以雙層巢狀的 enumerateObjectsUsingBlock 呼叫寫成。可以嚐試把內層結構提出與外層結構處於同一層次,並觀察結果是否有異。經過實驗後,我決定把這個巢狀結構移除。

2. 然後將原本內層enumerateObjectsUsingBlock中的程式碼,以方法抽出進行重構。

- (void)findAndBuildBarcodes:(NSMutableSet *)foundBarcodes obj:(AVMetadataObject *)obj
{
    if ([obj isKindOfClass:[AVMetadataMachineReadableCodeObject class]])
    {
        AVMetadataMachineReadableCodeObject *code = (AVMetadataMachineReadableCodeObject*)[_previewLayer transformedMetadataObjectForMetadataObject:obj];
        Barcode *barcode = [self processMetadataObject:code];
        [foundBarcodes addObject:barcode];
    }
}

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    NSMutableSet *foundBarcodes = [[NSMutableSet alloc] init];
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
         [self findAndBuildBarcodes:foundBarcodes obj:obj];
         
         ...(略)    
             
        });
         
     }];
    
         ...(略) 
}

重構12:質疑 captureOutput:didOutputMetadataObjects 方法內,NSMutableSet *foundBarcodes 的合理範圍。

原程式:

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    NSMutableSet *foundBarcodes = [[NSMutableSet alloc] init];
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
              ...(略) 
    }];
    
         ...(略) 
}

決定縮小其範圍,並以 findAndBuildBarcodes 取代 findAndBuildBarcodes:obj。至少,看起來更明白 foundBarcodes 這個 set 只與和它真正有關係的程式碼在一起。(和「…略2」所忽略的程式碼無關)

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    
    [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop)
     {
         NSMutableSet *foundBarcodes = [self findAndBuildBarcodes:obj];
         
         dispatch_sync(dispatch_get_main_queue(), ^{
                 ...(略)
        });
         
     }];
    
    ...(略2)
}


重構13:抽出 removeAllPreviewLayers 及 drawNewPreviewLayers

原程式:

dispatch_sync(dispatch_get_main_queue(), ^{
             // Remove all old layers
             NSArray *allSublayers = [_previewView.layer.sublayers copy];
             [allSublayers enumerateObjectsUsingBlock:^(CALayer *layer, NSUInteger idx, BOOL *stop) {
                 if (layer != _previewLayer) {
                     [layer removeFromSuperlayer];
                 }
             }];
             
             // Add new layers
             [foundBarcodes enumerateObjectsUsingBlock:^(Barcode *barcode, BOOL *stop) {
                 CAShapeLayer *boundingBoxLayer = [CAShapeLayer new];
                 boundingBoxLayer.path = barcode.boundingBoxPath.CGPath;
                 boundingBoxLayer.lineWidth = 2.0f;
                 boundingBoxLayer.strokeColor = [UIColor greenColor].CGColor;
                 boundingBoxLayer.fillColor = [UIColor colorWithRed:0.0f green:1.0f blue:0.0f alpha:0.5f].CGColor;
                 [_previewView.layer addSublayer:boundingBoxLayer];
                 
                 CAShapeLayer *cornersPathLayer = [CAShapeLayer new];
                 cornersPathLayer.path = barcode.cornersPath.CGPath;
                 cornersPathLayer.lineWidth = 2.0f;
                 cornersPathLayer.strokeColor = [UIColor blueColor].CGColor;
                 cornersPathLayer.fillColor = [UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.5f].CGColor;
                 [_previewView.layer addSublayer:cornersPathLayer];

             }];

重構後:

現在可以一眼就看出:在 dispatch_sync 中,主要是在 main queue 中完成兩件工作。

dispatch_sync(dispatch_get_main_queue(), ^{
             [self removeAllPreviewLayers];
             [self drawNewPreviewLayers:foundBarcodes];

});

- (void)removeAllPreviewLayers
{
    NSArray *allSublayers = [_previewView.layer.sublayers copy];
    [allSublayers enumerateObjectsUsingBlock:^(CALayer *layer, NSUInteger idx, BOOL *stop) {
        if (layer != _previewLayer) {
            [layer removeFromSuperlayer];
        }
    }];
}

- (void)drawNewPreviewLayers:(NSMutableSet *)foundBarcodes
{
    [foundBarcodes enumerateObjectsUsingBlock:^(Barcode *barcode, BOOL *stop) {
        CAShapeLayer *boundingBoxLayer = [CAShapeLayer new];
        boundingBoxLayer.path = barcode.boundingBoxPath.CGPath;
        boundingBoxLayer.lineWidth = 2.0f;
        boundingBoxLayer.strokeColor = [UIColor greenColor].CGColor;
        boundingBoxLayer.fillColor = [UIColor colorWithRed:0.0f green:1.0f blue:0.0f alpha:0.5f].CGColor;
        [_previewView.layer addSublayer:boundingBoxLayer];
        
        CAShapeLayer *cornersPathLayer = [CAShapeLayer new];
        cornersPathLayer.path = barcode.cornersPath.CGPath;
        cornersPathLayer.lineWidth = 2.0f;
        cornersPathLayer.strokeColor = [UIColor blueColor].CGColor;
        cornersPathLayer.fillColor = [UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.5f].CGColor;
        [_previewView.layer addSublayer:cornersPathLayer];
    }];
}

其餘重構:大抵上依循前面步驟,對 creatPathToFirstCorner:code: 方法進行重構。

原程式:

- (Barcode *)processMetadataObject:(AVMetadataMachineReadableCodeObject*)code {
    
    // 1  Query the dictionary of Barcode objects to see if a Barcode with the same contents is already cached.
    Barcode *barcode = [self findOrCreateNewBarcode:code];
    
    // Create the path joining code's corners
    
    // 4 Instantiate cornersPath to store the path joining the four corners of the code.
    CGMutablePathRef cornersPath = CGPathCreateMutable();
    
    // 5 Convert the first corner coordinate to CGPoint instances using some CoreGraphics calls.
    CGPoint point;
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[0], &point);
    
    // 6 Begin the path at the corner defined in Step 5.
    CGPathMoveToPoint(cornersPath, nil, point.x, point.y);
    
    // 7 Loop through the other three corners, creating the path as you go.
    for (int i = 1; i < code.corners.count; i++) {
        CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[i], &point);
        CGPathAddLineToPoint(cornersPath, nil, point.x, point.y);
    }
    
    // 8 Close the path by joining the fourth point to the first point.
    CGPathCloseSubpath(cornersPath);
    
    // 9  Create a UIBezierPath object from cornersPath and store it in the Barcode object
    
    barcode.cornersPath = [UIBezierPath bezierPathWithCGPath:cornersPath];
    CGPathRelease(cornersPath);
    
    // Create the path for the code's bounding box
    
    // 10 Create the bounding box path using bezierPathWithRect:.
    barcode.boundingBoxPath = [UIBezierPath bezierPathWithRect:code.bounds];
    
    // 11  Finally, return the Barcode object.
    return barcode;

}

重構後:

- (Barcode *)processMetadataObject:(AVMetadataMachineReadableCodeObject*)code {
    
    Barcode *barcode = [self findOrCreateNewBarcode:code];
    
    CGMutablePathRef cornersPath = [self creatBarcodeCornerPathes:code];
    
    [self setupBarcodePreviewPath:cornersPath barcode:barcode code:code];
    
    return barcode;

}

- (Barcode *)findOrCreateNewBarcode:(AVMetadataMachineReadableCodeObject *)code {
    
    Barcode *barcode = _barcodes[code.stringValue];
    
    if (barcode == nil) {
        barcode = [Barcode new];
        _barcodes[code.stringValue] = barcode;
    }
    
    barcode.metadataObject = code;
    
    return barcode;
}

- (CGMutablePathRef)creatBarcodeCornerPathes:(AVMetadataMachineReadableCodeObject *)code {
    
    CGPoint point;
    CGMutablePathRef cornersPath = [self creatPathToFirstCorner:&point code:code];
    
    [self buildPathesWithAllCorners:point cornersPath:cornersPath code:code];
    
    CGPathCloseSubpath(cornersPath);
    
    return cornersPath;
}

- (CGMutablePathRef)creatPathToFirstCorner:(CGPoint *)point_p code:(AVMetadataMachineReadableCodeObject *)code {
    
    CGMutablePathRef cornersPath = CGPathCreateMutable();
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[0], &(*point_p));
    CGPathMoveToPoint(cornersPath, nil, point_p->x, point_p->y);
    
    return cornersPath;
}

- (void)buildPathesWithAllCorners:(CGPoint)point cornersPath:(CGMutablePathRef)cornersPath code:(AVMetadataMachineReadableCodeObject *)code {
    for (int i = 1; i < code.corners.count; i++) {
        CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[i], &point);
        CGPathAddLineToPoint(cornersPath, nil, point.x, point.y);
    }
}

- (void)setupBarcodePreviewPath:(CGMutablePathRef)cornersPath
                        barcode:(Barcode *)barcode
                           code:(AVMetadataMachineReadableCodeObject *)code {
    
    barcode.cornersPath = [UIBezierPath bezierPathWithCGPath:cornersPath];
    CGPathRelease(cornersPath);
    
    barcode.boundingBoxPath = [UIBezierPath bezierPathWithRect:code.bounds];
}


結語:

本範例主要在表達一些重構的觀念及技巧,其實在一個步驟內就還包含了數個小步驟,並不像本文中講的那麼簡潔。假若讀者有一起跟著操作,一定知道我在說什麼。

所有的程式碼均集中在 ViewController.m 中,所以這樣的重構結果,算是很基礎的。不過,這卻是重要的基礎。假設,未來有任何功能性需求的擴充,或是希望將這份程式碼延伸成樣版、模組,或是做為其他應用的基礎,相信妥善重構後的結果,一定能讓上述種種變更需求都顯得更優雅、更可行。至少,對於撰寫者而言,應該能保持較佳的工作心情及效率。

最後注意到的一點是:這個範例,正好是很不容易進行 TDD 的範例。至少目前我還沒找到可以把 UIImage 以 AVMetadataMachineReableCodeObject 進行解析的方式。不過儘管如此,在每一個步驟之間,重構者都必須得要不斷進行測試,並確定功能無損後才進行 commit,這個紀律是非常重要的。

2014年3月28日 星期五

Tweaks Framework

Tweaks::Facebook

Tweaks 是由 Facebook 三天前(2014.3.25)所發佈的一套用於協助 prototyping 的框架。

在進行 App 的視覺設計時,最準確的方式就是:將 App 佈署到行動裝置上,並且實際在各種場合下進行各種操作。

什麼叫「各種場合」?

例如:
一套支援計步健身的 App,其色彩設計上的規劃必須考慮使用者在大太陽之下能不能看得清楚?
以銀髮族為角色的 App,其字體大小的最小尺寸、以及不會破壞排版的最大尺寸為多少?
App 內的動畫播放速度要多快不會太擔誤使用者的時間?但是又能看得清楚、得到預想的效果?

什麼叫「各種操作」?

例如:
對於單手持握裝置的使用者而言,App 內提供的按鈕位置是否容易觸發?是否操作旅行的路徑最短?

上述需求,可能會引起的因應動作是:開發者得不斷在程式碼/設定檔中調整各項參數,然後再一次進行至行動裝置的佈署及執行。顯然,有點花時間,對吧?而且,決定視覺設計的人,也可能不是程式開發者本身,這樣一來,要確定哪一個參數合適的過程中的溝通成本就不低。

Tweaks 可以讓上述情境 smooth 一些。

我從 FBTweakExample 中的程式碼來進行解釋。

一、FBTweakValue

_rootViewController.view.backgroundColor = [UIColor colorWithRed:0.9
                                                        green:0.9
                                                         blue:0.9
                                                        alpha:1.0];

上面的程式碼設定了 root 這個視圖控制器所管理的視圖的背景色,以RGB & Alpha 值進行設定。
改寫成下方 statement :

_rootViewController.view.backgroundColor

[UIColor colorWithRed:FBTweakValue(@"Window", @"Color", @"Red", 0.9, 0.0, 1.0)       
                green:FBTweakValue(@"Window", @"Color", @"Green", 0.9, 0.0, 1.0)
                 blue:FBTweakValue(@"Window", @"Color", @"Blue", 0.9, 0.0, 1.0alpha:1.0];

這裡使用了 FBTweakValue 。是什麼意思呢?我們直接來看輸出:



第一個畫面是由 Tweaks 這個框架所提供的 (FBTweakViewController)。請注意看到第一個畫面中的「Window」row 和第二畫面中的「COLOR」session,請對應於上面程式碼中 FBTweakValue 的第一、第二個參數。而第二畫面中的 Red 就是對應第三個參數。我想大家也能猜出:第四個參數就是「預設值 0.9」。第五、第六個參數是最小值和最大值。

也就是說:只要我們將程式裡的某個值 (value) 代換成 FBTweakValue 這個 macro 的話,在 FBTweakViewController 啟動後就會把前三項參數產生為第一畫面列表中的一個列 (UITableViewCell)、第二畫面中的分段(Section) 及 分段 中的列。

以此例來說,提供了浮點數型別的三個數字作為第4~6個參數,FBTweakViewController 會自動生成 step 元件(兩個按鈕,一個減一個加)。

我們可以透過點按 step 元件來改變顏色的RGB值。

要注意的一點是:設定完之後,要把 App 先從背景移除後再開啟,該值才會生效。什麼?不能直接生效嗎?可以,不過得換個 macro 來用:FBTweakBind。


二、FBTweakBind

_label.text = @“Tweaks”;

上方的程式碼中,把標籤 _label 的文字設定為 Content。我們以下方的程式進行改寫:

  FBTweakBind(_label, text, @"Content", @"Text", @"String", @"Tweaks");

第一個參數是 UI 元件,第二個參數則是該元件的屬性第三 ~ 五個參數…直接看圖吧…,第六個參數則為預設值。


可以看出來,第三~五個參數是怎麼被產生出設定用的 UI 元件的。
這裡因為用的是 FBTweakBind 這個 macro ,所以值被變更的同時,回到 App 去看的話,就能馬上看到效果的變化。


三、FBTweakInline

上面提及的兩個 macro 有個共同之處:都是和某個 UI 元件的特定屬性相關。透過 FBTweakInline 可以用觀察者模式對某個設定項的值進行偵測,並於該值改變時調整 App 內任何的設定參數(不綁特定 UI 元件)。直接看實例吧:

_flipTweak = FBTweakInline(@"Window", @"Effects", @"Upside Down", NO);
  [_flipTweak addObserver:self];

- (void)tweakDidChange:(FBTweak *)tweak
{
  if (tweak == _flipTweak) {
    _window.layer.sublayerTransform = CATransform3DMakeScale(1.0, [_flipTweak.currentValue boolValue] ? -1.0 : 1.0, 1.0);
  }
}

  

畫面上的  EFFECTS -> Upside Down 預設值為 NO,並由程式碼的 host class 作為觀察者。只要 Upside Down 的值發者變化,則 call-back method : tweakDidChange 會被呼叫,然後就可以在該 method 內指定 App 要因應做何改變了(此範例是把螢幕轉180度,變成上下顛倒)。


以上就是 FBTweakExample 中和 FBTweaks 框架有關的示範了。


結語:

1. 可以用於 App 的設計階段。假設 coder 運用地夠純熟,大概不會太影響開發速度;UI designer 可以自行透過介面做各項參數的調整而不需要一再和 coder 反複針對單一參數進行溝通。
2. FBTweakExample 的寫法是為了容易說明,不過在程式碼中嵌入 FBTweak macro 時也會對程式碼 intention 的表達有影響:閱讀流暢度會受影響。要有更好的封裝:同時也得防止封裝影響偵錯。
3. 用於 Production … 嗯…持保留態度。
4. 可以看出這套框架應該能減少 UI design coder 之間的溝通成本,這個應該很合適以不斷修正體驗設計的開發團隊。對於 UI design program develop 流程分開且 one-step 執行 (non-iteration) 的團隊則不適用。






2014年3月18日 星期二

Objective-C Block 運用

這篇文章試圖用一個很簡單的範例來介紹:在 Objective-C 中怎麼利用 Block 進行 API 的設計。這篇文章的小小價值,在於不從 Block 的語法、特性切入,而是直接以 API 設計的思考做為起點來舖陳,希望閱讀者能盡快抓到 Block 用於設計的精要。

一、起點:以「書名」來找一本書

目前 API 還沒有任何一行 code 被產生。沒關係,我們先在 API 的 client  寫下欲設計的結果。

    for (NSString *bookName in @[@"Objective-C 真簡單", @"Objective-C 學不會"]) {
        [self printReportHeader:bookName];
        [BookFinder bookWithName: bookName          //以書名找書
                   actionIfFound: foundAction       //若有找到書,就做 foundAction
                actionIfNotFound: notFoundAction];  //若找不到書,就做 notFoundAction
    }

    - (void)printReportHeader:(NSString *)bookName
    {
       NSLog(@"======================================");
        NSLog(@"==> 欲尋找的書籍: %@", bookName);
    }

上面就是我想要的結果:一個 BookFinder 以 bookWithName:actionIfFound:actionIfNotFound 這個 Class Method 查找書籍。這個 Method 以書名(bookName)作為參數傳入,若書有找到,就執行 actionIfFound;反之,沒找到書,就執行 actionIfNotFound。

注意,上面我用「執行」作為及物動詞,而 actionIfFound 和 actionNotFound 為受詞。可見這兩個 action 不是單純的 object instance;事實上,它們是 block 。

繼續,我們進一步設計 actionIfFound 和 actionNotFound 這兩個 block 的工作內容。這兩個 block 是所謂的 in-line block :

    void (^foundAction)(Book *) = ^(Book *book) {
        NSLog(@"==> 書籍售價: %@", book.price);
    };
    
    void(^notFoundAction)(BookInfo *) = ^(BookInfo *bookInfo) {
        NSLog(@"==> 找不到的原因: %@", bookInfo.reason);
    };

這裡設計:若找到書,則把 Book 的 object 作為參數傳入 block ,我們就可以印出書籍的價格;若沒找到書,block 內應該能從 BookInfo 的 object 中取得找不到的原因並印出來。

然後我預想一下我要的輸出結果:

 ======================================
 ==> 欲尋找的書籍: Objective-C 真簡單
 ==> 書籍售價: 100
 ======================================
 ==> 欲尋找的書籍: Objective-C 學不會
 ==> 找不到的原因: 絕版

嗯,差不多了,開始把其他的程式碼長出來吧!

in BookFinder.h

宣告出我們在 Client 中使用的 Class Method :

+(void) bookWithName:(NSString *)name actionIfFound:(BookFindAction)actionIfFound actionIfNotFound:(BookNotFindAction)actionIfNotFound;

上面的 BookFindAction 和 BookNotFindAction 是自訂的 Block,使用 C 語言的 typedef 進行定義:

typedef void(^BookFindAction)(Book *);
typedef void(^BookNotFindAction)(BookInfo *);

從上式可知,這兩個 Block 都沒有回傳值 (void),傳入的參數則是遵照在 Client 端程式碼怎麼用它們來設計。記得,上面兩式得寫在 @interface BookFinder : NSObject 區段的上方才行哦!


in BookFinder.m

+(void) bookWithName:(NSString *)name actionIfFound:(BookFindAction)actionIfFound actionIfNotFound:(BookNotFindAction)actionIfNotFound
{
    Book *book = [BookFinder findByName: name];
    
    if (book != nil) {
        actionIfFound(book);
    } else {
        BookInfo *bookInfo = [BookInfo new];
        bookInfo.reason = @"絕版";
        actionIfNotFound(bookInfo);
    }
}

+(Book *)findByName:(NSString *)name
{
    if ([name isEqual:@"Objective-C 真簡單"]) {
        Book *book = [Book new];
        book.name = name;
        book.price = @100;
        return book;
    }
    
    return nil;

}

這裡的實作很簡單,透過一個簡單的比對,只要書名不是「Objective-C 真簡單」,就當那本書絕版。其餘的實作邏輯也是完全遵照一開始在 Client 端程式碼的設定。

至於 Book 和 BookInfo 只是兩個設定了 Property 的簡單類別,就不列出來佔版面了。

上面所謂「Client 端的程式碼」,若是寫在 Unit Test 中當然更好,不但能更佳的突顯、收納設計意圖,也能時時驗證。

這裡展示的是如何在對於 Block 最少量所知、而且尚未有任何程式碼前進行設計,並一步步把設計實現的思維及步驟,希望對於尚未使用 Block 在自己的程式裡進行設計的人能有所助益。


2014年3月10日 星期一

Java Spark Framework


簡介


Github URL: https://github.com/perwendel/spark

Spark 是以 Ruby Sinatra 為範本的 Java 版本,可以用很少的程式碼量快速建構 Web 系統。
舉個簡單的例子:

假設使用 http://主機名稱:埠號/hello 這樣的 URL 作為一項 Web 服務,其回傳 "Hello World!",其程式碼寫法為:

get(new Route("/hello"
   @Override
   public Object handle(Request request, Response response) {
      return "Hello World!";
   }

});

意思是:建立 Http Get 能進行存取的 /hello 服務。
基本上把這段程式放在 main() 中後啟動,就會啟動 spark 內搭的 jetty 完成程式的佈署及服務開啟。

安裝

1. 建立 Maven Project
2. 加入 dependency 設定:
<dependency>
        <groupId>com.sparkjava</groupId>
        <artifactId>spark-core</artifactId>
        <version>1.1.1</version>
    </dependency>
3. 可以試著執行 mvn clean 讓相依的 Jar 檔下載至 local 。

執行運作

 mvn exec:java -Dexec.mainClass="${完整.類別名稱}"

程式範例

1. 存取網址的一部分作為參數。例:http://主機名稱:埠號/users/Sherlock,這裡希望 Sherlock 作為參數,讓程式進行處理:

get(new Route("/users/:name") {
            @Override
            public Object handle(Request request, Response response) {
                return "Selected user: " + request.params(":name");
            }

        });

意思是:視 :name 為參數,在程式裡能透過 Request 物件的 params 這個 Map 物件取得。(:name為 key, Sherlock 為 value)

2. 存取網址中的 query string:

//取得所有 query 參數           
Set<String> queryParams = request.queryParams();

//取得指定名稱的參數值
request.queryParams("參數名稱");

//取得整串 query string

request.queryString();

3. 存取各個 scope 的 attribute 值:

//getter
request.attribute("attribute_name_to_read");

//setter
request.attribute("attribute_name", "attribute_value");

//取得 Session scope 的 attribute
Session session = request.session();
session.attribute("session_attribute_name");

4. 存取 header

//getter
request.headers("header_name");

//setter (response物件)
response.header("header_name", "header_value");

5. 存取 cookie

//getter
request.cookie("cookie_name");

//setter (response物件)
response.cookie("cookie_name", "cookie_value");

//指定 cookie 有效期限
final int MAX_AGE = 1000;
response.cookie("cookie_name", "cookie_value", MAX_AGE);

6. 實作 filter

//前置過濾器
before(new Filter("/hello") {
  @Override
  public void handle(Request request, Response response) {
    //implements ...
  }
});

//後置過濾器
after(new Filter("/hello") {
 @Override
  public void handle(Request request, Response response) {
    //implements ...
 }

});

7. View 的實作

Route 有不同的子類支援不同的輸出格式:
 1) TemplateViewRoute : 輸出 HTML 範本
 2) ResponseTransformerRoute


8. 變更 listen port

直接呼叫 setPort(埠號); 



這裡對於不同的 View 沒有舉太多例子,視需求再到網路上找找資訊即可。