天天看點

一篇比較好示範AS的重構方法

[翻譯]重構讓世界更美好(Making the world better via refactoring - Intro)

這些文章翻譯自Sho Kuwamoto的ActionScript Refactoring 三部曲,經過原作者同意翻譯之。

[url=http://kuwamoto.org/2007/05/15/making-the-world-better-via-refactoring-intro/]本文原文連結在此[/url]。

我非常喜歡 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裡關於排版的程式碼分開,建立成如下的關係:

[img]http://blog.shiue.net/wp-content/uploads/2008/01/dragtileinherit.png[/img]

(圖1 繼承)

FlexibleContainer, 負責一般的行為,如: 過場動畫、滑鼠互動等。

DragTile, 負責特定的排版方式,運算物件的位置。

把這些工作分離開後,我們可以更容易地建立新的類別以達成新的排版方式(比如排成一個圓之類的),而其他程式碼:item renderer的溝通、動畫等等還可繼續沿用。

另一個手法:合成(Composition)

[img]http://blog.shiue.net/wp-content/uploads/2008/01/dragtilecomposition.png[/img]

(圖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 三部曲,經過原作者同意翻譯之。

[url=http://kuwamoto.org/2007/05/15/making-the-world-better-via-refactoring-step-1/]本文原文連結在此[/url]。

上一篇,我們探討了基本的知識,以及如何利用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:

[img]http://blog.shiue.net/wp-content/uploads/2008/01/step0_api.png[/img]

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

[img]http://blog.shiue.net/wp-content/uploads/2008/01/step1_api.png[/img]

[url=http://examples.kuwamoto.org/DragTile/DragTile_step1.zip]第一次Refactor的原始碼[/url]

[翻譯]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的定義:

[img]http://blog.shiue.net/wp-content/uploads/2008/01/step1_dragtile_api.png[/img]

注意那些標示成綠點的屬性,看起來都是特定給DragTile用的,同時,最底下有三個方法看起來很"普通": measure(), findItemAt(), and generateLayout(),我們把這三支抽離出來寫成一個interface(譯註: 此處指 OOP 的interface, 並非 User Interface)專門針對排版運算。

由於IUIComponent 已經定義了measure()這個方法,沒有必要令ILayout再定義一次,反之,我決定新增一個方法叫做 getMeasuredSize() 來呼叫measure()。針對排版運算管理,我還新增了兩個方法: attach() 及 detach(),如此一來,LayoutManager 可透過這些方法將物件加上或清除,最後, ILayout長像這樣:

[img]http://blog.shiue.net/wp-content/uploads/2008/01/step2_ilayout_api.png[/img]

接下來,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。

[img]http://blog.shiue.net/wp-content/uploads/2008/01/scaffolding.png[/img]

如果你不熟析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了。

[url=http://examples.kuwamoto.org/DragTile/DragTile_step2.zip]二部曲的程式碼[/url]

翻譯]ActionScript重構三部曲之三(Advanced ActionScript Refactoring - Step 3)

這些文章翻譯自Sho Kuwamoto的ActionScript Refactoring 三部曲,經過原作者同意翻譯之。

本文原文連結在此。

最後一關了,快破關了。

一開始的版本會變成這個模樣,排版方式可以動態改變了!

回想第二步,我們沒做什麼refactoring,隻有把Styles關掉,然後移出DragTile的程式碼,再把Styles加回去,之後Layout都擁有自己的Style了。

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

[img]http://blog.shiue.net/wp-content/uploads/2008/01/layoutinheritance.png[/img]

我還"修正"了一些地方,比如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吧。

[url=http://examples.kuwamoto.org/DragTile/DragTile_step3.zip]Refactor最終版[/url]