`
bufanliu
  • 浏览: 197367 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

一篇比较好演示AS的重构方法

    博客分类:
  • FLEX
阅读更多
[翻譯]重構讓世界更美好(Making the world better via refactoring - Intro)
這些文章翻譯自Sho Kuwamoto的ActionScript Refactoring 三部曲,經過原作者同意翻譯之。
本文原文連結在此

我非常喜歡 Ely的一個作品:DragTile 元件( demo )。 為了做出不同的效果,我想延伸它原有的功能行為,並且使其更具彈性。我第一個想到的就是:Refactoring( 譯註: 中文通常翻譯為:”重構”,為重新建構之意,筆者在此保留原文,免除翻譯的差異 )。 如果你不常或輩子從沒做過refactoring,那麼,且聽我一步步道來,很有趣的。


何謂Refactoring?
Refactoring :在不改變原有外部功能的前提下,以漸進地手法改寫程式碼的結構。”不改變原有功能“聽起來很怪,卻是精隨所在。
分成兩部份來看: coding and refactoring,coding階段時,我們加上了新的功能(functionality);refactoring階段,我們重新調整程式碼結構,同時確保功能運行依舊。請牢記”不改變原有功能”的前提,這會讓你在進行調整程式碼時,不會迷失方向,當新的程式碼運作功能與舊的一樣,就是一次成功的refactoring。

有Refactoring有彈性
通常來說,refactoring 要讓現有的程式碼更有彈性,有些時候,你要refactoring的目標很明顯;有些情形下,你必須在refactoring前好好地思考規劃該如何改寫程式碼。
在Ely的例子裡,我們想要讓畫面的排版(layout)方式更容易被改變,看看DragTile的原始碼,有些部份不論排版如何,都是一樣;有些部份隨著排版而變動,因為排版的演算法(algorithm)在不同案例下會有不同變化,因此我們可以把它抽離出來,獨立成另一個類別(Class)。

Tip 1:

Cleanly separate out the code that you think will need to change often into a separate class.
技巧一: 把你認為會常常變動的程式碼分離出來

我們用繼承的手法,將DragTile裡關於排版的程式碼分開,建立成如下的關係:


(圖1 繼承)
FlexibleContainer, 負責一般的行為,如: 過場動畫、滑鼠互動等。
DragTile, 負責特定的排版方式,運算物件的位置。
把這些工作分離開後,我們可以更容易地建立新的類別以達成新的排版方式(比如排成一個圓之類的),而其他程式碼:item renderer的溝通、動畫等等還可繼續沿用。

另一個手法:合成(Composition)

(圖2 合成)
FlexibleContainer是一個容器,它包括了許多子元件,子元件一般行為的程式碼都會在這裡。
TileLayout管理排版的helper Class, 任何排版相關的計算都是它的責任。

如圖所示,Container類別將排版的任務委派(Delegate)給TileLayout來處理,本手法有一些好處:

以”委派”的觀念實作,通常可以使物件行為得以動態改變。承上,我們可以抽換其中一個Container的排版而不需重新調整繼承關係(reparenting)。
將大型類別拆解成數個小類別的合成(非繼承),將有助於調整、擴充程式結構,如同各個擊破一般。相對繼承手法來看,如果我們把FlexibleContainer再分離成兩個類別:一個是輕量化的Container專門為下載效率設計;另一個處理快取(cache)、本地化(localization),那麼,DragTile該繼承哪一個類別呢? 這將是一個難題,然而,如果你採用了委派的作法,將排版的工作委派給Layout類別,你不會有這個困擾。
合成手法,通常可以讓系統更分工(decoupling)更具彈性。舉例來說,你正在做照片管理的模組,如果你用了最上面說的繼承手法,你很難建立一個專門排版的模組,相對地,用合成手法,排版的功能是可以依使用者需要而直接改變的。
概觀上述所言,有技巧如下:

Tip 2: Think hard before using inheritance. Composition is almost always a better way to separate out the flexible part of a class from the invariant part.
技巧二: 你真的要用繼承嗎? 請三思。合成往往是比較好的選擇。

現在我們有refactoring的基礎概念了,在下一篇我們將深入探討程式碼。

[翻譯]ActionScript重構三部曲之一(Advanced ActionScript Refactoring - Step 1)
這些文章翻譯自Sho Kuwamoto的ActionScript Refactoring 三部曲,經過原作者同意翻譯之。
本文原文連結在此

上一篇,我們探討了基本的知識,以及如何利用refactoring讓DragTile更彈性,如果妳還沒有看過,可以去瞥幾眼再回來,
回來了嗎? 那我們來看看一些程式碼吧。

Step 0 - 開始
原本的程式碼檔案請到Ely的部落格下載,或者你可以下載我的稍微修改版。

Step 1 - 抽離出FlexibleContainer類別
雖然我上一篇說過要用合成不用繼承,但是我還是先用繼承一下,這樣會比較容易建立出helper class。

Tip 3: Always refactor in small steps that leave the external behavior unchanged
切記以小部份地進行refactor,並且確保物件的行為不變

先建立一個父類別(superclass),開始看DragTile的原始碼,一個個方法(譯註:method通常譯為”方法”, 可是我喜歡翻成”函式”  )一個個屬性(property)地看,把合適的函式和屬性放到superclass裡頭。
我把那些看起來很一般的區域變數歸類到superclass裡頭, _items 陣列, renderers 陣列也是一樣,其他看起來專門為了排版的變數保留在DragTile裡。
因為大部分變數都是private,被移動到superclass之後,會造成很多編譯錯誤的訊息,我通常也會把相關函式的移入superclass。
在一個情況下(dragTargetIndex)我需要建立一個protected變數讓子類別可以取得父類別的資料,然而,這是正確的途徑嗎? 在這個例子裡,可能不是,”正確”的方式,應該是在drag操作時,把資料以參數的方式傳遞出去。 這提示了我們:

Tip 4: When refactoring, don’t try to make it “perfect”. Just strive to incrementally improve the code each time you touch it.
Refactoring時,不要想一次就達到完美,只要一次比一次好一點就可以了

建立一個protected變數是分開兩個class最快的方法,我們等會再來修改。
原本的api:


第一次refactoring的目標是將有關於排版的邏輯程式都移到一個類別裡,其他的類別盡亮都放在superclass裡,DragTile類別 應該越小越好,調整過一次的api:


第一次Refactor的原始碼


[翻譯]ActionScript重構三部曲之二(Advanced ActionScript Refactoring - Step 2)
這些文章翻譯自Sho Kuwamoto的ActionScript Refactoring 三部曲,經過原作者同意翻譯之。
本文原文連結在此。

在前一步創造了一個父系類別,由他來掌管排版的邏輯演算。現在我們換個做法,用『合成』的概念來做做看:我希望這個部分的Refactoring把DragTile的功能轉移到另一個Helper Class(譯註: Helper Class顧名思義為協助型類別,通常定義了一些大家會共用到的運算式、常數等),最後DragTile只約化成一個空殼。
現在看來,我們不用再擔心Container和Layout Manager之間要如何拉關係了,回頭來看一下第一步,我們決定類別工作的過程有點隨便...。
那麼,到底要移動哪些方法、屬性到Helper Class呢?

我們得先問問自己,主要的物件(Container)將如何與Helper Class溝通,先來看看DragTile的定義:


注意那些標示成綠點的屬性,看起來都是特定給DragTile用的,同時,最底下有三個方法看起來很"普通": measure(), findItemAt(), and generateLayout(),我們把這三支抽離出來寫成一個interface(譯註: 此處指 OOP 的interface, 並非 User Interface)專門針對排版運算。
由於IUIComponent 已經定義了measure()這個方法,沒有必要令ILayout再定義一次,反之,我決定新增一個方法叫做 getMeasuredSize() 來呼叫measure()。針對排版運算管理,我還新增了兩個方法: attach() 及 detach(),如此一來,LayoutManager 可透過這些方法將物件加上或清除,最後, ILayout長像這樣:


接下來,FlexibleContainer可以加上layout屬性了,並建立一個TileLayout類別來實作ILayout定義的方法。 我們如何由第一步演進至第二步的呢? 第一步,新的副類別FlexibleContainer出現了;第二步,不會再有DrageTile類別了。
依照Refactoring的慢慢、小部份修改的原則,可是很難做到,因此我導出另一個技巧:

Tip 5: If needed, build temporary scaffolding to make sure your code continues to “work” as you refactor.
Refactor時,可建立一個暫時的類別,先把很多很多工作委任給它,讓原有功能保持正常運作

本例中,Refactor到一半時,我同時運用了繼承和委派。換句話說,即使我已經規劃了TileLayout類別,我仍舊多寫了一些程式碼保留DragTile。



如果你不熟析refactor背後的奧妙,你會懷疑:這"暫時類別"是什麼鳥?! 然而只要你習慣了,你會愛上它的。往往一次大改比小改容易成功許多,暫時類別是讓你無後顧之憂地繼續一步步refactoring。
下階段,移出 DragTile的方法功能,我不會把整堆方法都移出,反之,我會運用以下原則把這些方法放進對應類別:

如果程式碼是"一般的"邏輯操作,交給FlexibleContainer
如果與Tile排版有關,放到TileLayout
這些原則大都沒問題,惟獨遇到Style(譯註: Flex Framework的CSS架構),由於TileLayout並非UIComponent,因此沒有內建的CSS操作,DragTile有定義一些CSS,如vGap, hGap,我們依然可以讓TileLayout具有Style的功能的,然而這必須大費功夫,所以我決定了,暫時關閉CSS style的運作:

Tip 6: When temporarily disabling functionality during a big refactor, be sure to do so in a way that preserves information, ideally through stub functions.
當進行Refactor而關閉了某些功能,必須安排個方法保存對應的資料

把CSS關掉的最快方法:註解掉程式碼,但很難確定這些程式碼如果移到他處,是否仍舊運作無誤? 有鑑於此,我建立一個getStyle()方法,只回傳NaN(譯註: No a Number) 如此這般,Ely的CSS依舊正確地存取數值,但不使用罷了,這個作法可以避開大部分因移動程式碼造成的編譯器錯誤。

最後我把DragTile所有的程式碼分離至兩個類別裡頭,DragTile最後只剩下:

PLAIN TEXTActionscript:
public class DragTile extends FlexibleContainer
{
    public function DragTile()
    {
        super();

        layout = new TileLayout();
    }
}

顯然地,我們現在可以不管DragTile了。
二部曲的程式碼

翻譯]ActionScript重構三部曲之三(Advanced ActionScript Refactoring - Step 3)
這些文章翻譯自Sho Kuwamoto的ActionScript Refactoring 三部曲,經過原作者同意翻譯之。
本文原文連結在此。

最後一關了,快破關了。
一開始的版本會變成這個模樣,排版方式可以動態改變了!
回想第二步,我們沒做什麼refactoring,只有把Styles關掉,然後移出DragTile的程式碼,再把Styles加回去,之後Layout都擁有自己的Style了。

為了確保排版可以動態改變,我新寫了一個排版類別: CircleLayout ,再整理一次CircleLayout和TileLayout,解析兩者共通的部份,抽離出另一個父類別: Layout ,現在類別圖如下:


我還"修正"了一些地方,比如Drag/Drop(譯註: 拖拉-放)一直令我有點疑惑,因此我用了一個神祕的方法改寫掉了(看你能不能找到!)
最後測試看看refactor之後,排版方式是否那麼容易地抽換,原本的DragTile有600多行程式碼,而新的CircleLayout只有100多行,裡面只做了相關的物理運算,沒有其他管理renderer 與 animator的行為。

PLAIN TEXTActionscript:
public class CircleLayout extends Layout
{
// ILayout interface
override public function getMeasuredSize():Point
{
    return getMaxSize();
}

override public function findItemAt(px:Number, py:Number, seamAligned:Boolean):Number
{
    // Can't execute this if we aren't attached to a container.
    if (!container || container.renderers.length == 0)
        return NaN;

    // Get the radius and center of the circle.
    var radius : Number = Math.min(unscaledContainerWidth, unscaledContainerHeight) / 2;
    var hCenter : Number = unscaledContainerWidth / 2;
    var vCenter : Number = unscaledContainerHeight / 2;

    var angle : Number = Math.atan2(py-vCenter, px-hCenter);
    if (angle <0)
        angle += 2 * Math.PI;

    // figure out the closest "item" by working backwards from the angle to the index, using floating point math.
    var result : Number = container.renderers.length * angle / (2 * Math.PI);

    // depending on whether this is seam aligned, do a ceil or round.
    result = (seamAligned) ? Math.ceil(result) : Math.round(result);

    // do a modulo op to make sure that this is within [0, length-1]. Modulo is the correct
    // operator in this case because this is a circle.
    result %= container.renderers.length;
    return result;
}

override public function generateLayout():void
{
    // Get the radius and center of the circle.
    var radius : Number = Math.min(unscaledContainerWidth, unscaledContainerHeight) / 2;
    var hCenter : Number = unscaledContainerWidth / 2;
    var vCenter : Number = unscaledContainerHeight / 2;

    // Find the max item size.
    var maxSize : Point = getMaxSize();
    var max : Number = Math.max(maxSize.x, maxSize.y);

    // Inset the radius by the max size.
    radius -= max;

    // Loop through the items and position them.
    var length : int = container.renderers.length;
    for (var idx:int = 0; idx <length; idx++)
    {
        var renderer:IUIComponent = container.renderers[idx];
        var target:LayoutTarget = animator.targetFor(renderer);//targets[idx];

        // evenly space each item over 2*pi radians.
        var angle : Number = (2 * Math.PI) * idx / length;

        // position items on a circle.
        target.scaleX = target.scaleY = 1;
        target.item = renderer;
        target.unscaledWidth = renderer.getExplicitOrMeasuredWidth();
        target.unscaledHeight = renderer.getExplicitOrMeasuredHeight();
        target.x = hCenter + radius * Math.cos(angle) - target.unscaledWidth/2;
        target.y = vCenter + radius * Math.sin(angle) - target.unscaledHeight/2;
        target.animate = true;
    }

    // If there is more than one item, and if there is a drag target, nudge the items next to the drag target
    if (length> 1 && container.dragTargetIndex>= 0 && container.dragTargetIndex <length)
    {
        // Find the items to the left and right of the target.
        var leftIndex : int = (container.dragTargetIndex + length - 1) % length;
        var rightIndex : int = (leftIndex + 1) % length;

        var leftTarget : LayoutTarget = animator.targetFor(container.renderers[leftIndex]);
        var rightTarget : LayoutTarget = animator.targetFor(container.renderers[rightIndex]);

        // exaggerate the difference between the two targets by a factor of maxSize/2.
        var dx : Number = rightTarget.x - leftTarget.x;
        var dy : Number = rightTarget.y - leftTarget.y;
        var distance : Number = Math.sqrt( dx*dx + dy*dy );

        leftTarget.x -= dx / distance * max/2;
        leftTarget.y -= dy / distance * max/2;
        rightTarget.x += dx / distance * max/2;
        rightTarget.y += dy / distance * max/2;
    }
}

protected function getMaxSize() : Point
{
    // Can't execute this if we aren't attached to a container.
    if (!container)
        return new Point(0, 0);

    // Find the max item size.
    var maxWidth : Number = 0;
    var maxHeight : Number = 0;

    if(container.renderers.length> 0)
    {
        for(var i:int=0;i<container.renderers.length;i++)
        {
            var itemRenderer:IUIComponent = container.renderers[i];
            maxWidth = Math.ceil(Math.max(maxWidth,itemRenderer.getExplicitOrMeasuredWidth()));
            maxHeight = Math.ceil(Math.max(maxHeight,itemRenderer.getExplicitOrMeasuredHeight()));
        }
    }

    return new Point(maxWidth, maxHeight);
}

可以再改良嗎? 當然可以。layout與container仍舊留有一些連結,可能可以移除掉的,我先做到這裡就好,找機會再繼續Refactor吧。

Refactor最終版






分享到:
评论
1 楼 matt.u 2009-05-16  
好像有点深奥。

相关推荐

Global site tag (gtag.js) - Google Analytics