[翻譯]重構讓世界更美好(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最終版