<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Posts on 網站製作學習誌</title>
		<link>https://jaceju.net/posts/</link>
		<description>Recent content in Posts on 網站製作學習誌</description>
		<generator>Hugo -- gohugo.io</generator>
		<language>zh-TW</language>
		<copyright>This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.</copyright>
		<lastBuildDate>Tue, 08 Sep 2020 12:13:15 +0800</lastBuildDate>
		<atom:link href="https://jaceju.net/posts/index.xml" rel="self" type="application/rss+xml" />
		
		<item>
			<title>用 Livewire 在 Laravel 應用裡實現無痛的前後端溝通</title>
			<link>https://jaceju.net/laravel-livewire/</link>
			<pubDate>Tue, 08 Sep 2020 12:13:15 +0800</pubDate>
			
			<guid>https://jaceju.net/laravel-livewire/</guid>
			<description>&lt;h2 id=&#34;緣起&#34;&gt;緣起&lt;/h2&gt;
&lt;p&gt;如果今天給網站開發者一個最簡單的題目：「如何在不重整頁面的情況下，讓前端介面可以取得後端資料的狀態？」我相信幾乎所有開發者都能實作出來，畢竟這個機制算是現代網站開發的基礎。&lt;/p&gt;
&lt;p&gt;這個題目的答案從概念上來說，就是很簡單的三個步驟：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;後端送出 HTML 給瀏覽器來呈現網頁。&lt;/li&gt;
&lt;li&gt;在頁面上用 AJAX 向後端 API 發起請求。&lt;/li&gt;
&lt;li&gt;在接收到 AJAX 的結果後，將結果呈現到對應的位置上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;當然不僅是取得結果，同樣的方式也可以用在把前端的資料送往後端的業務處理邏輯上。&lt;/p&gt;
&lt;p&gt;只不過這背後有很多麻煩事要處理，像是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;後端要建立對應前端操作的 API ，並且要顧及安全性 (例如防範 CSRF ) 。&lt;/li&gt;
&lt;li&gt;前端要透過 XHR 發起要求，並處理 API 各種狀態的結果。&lt;/li&gt;
&lt;li&gt;需要學習如何操作前端 DOM 元素，讓後端資訊可以綁在元素上或是透過元素的事件來取得元素上的資訊。&lt;/li&gt;
&lt;li&gt;前端要維護介面狀態，在重新整理頁面後要能後端資料狀態同步。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;諸如此類的基礎工夫還不少，雖然現代化的前後端框架或工具都幫我們處理掉了，但實戰上要把它們兜起來還是要花掉不少時間。&lt;/p&gt;
&lt;p&gt;有沒有什麼更好的方法，可以讓開發者可以做更少的事，而達到同樣的效果呢？&lt;/p&gt;</description>
			<content type="html"><![CDATA[<h2 id="緣起">緣起</h2>
<p>如果今天給網站開發者一個最簡單的題目：「如何在不重整頁面的情況下，讓前端介面可以取得後端資料的狀態？」我相信幾乎所有開發者都能實作出來，畢竟這個機制算是現代網站開發的基礎。</p>
<p>這個題目的答案從概念上來說，就是很簡單的三個步驟：</p>
<ul>
<li>後端送出 HTML 給瀏覽器來呈現網頁。</li>
<li>在頁面上用 AJAX 向後端 API 發起請求。</li>
<li>在接收到 AJAX 的結果後，將結果呈現到對應的位置上。</li>
</ul>
<p>當然不僅是取得結果，同樣的方式也可以用在把前端的資料送往後端的業務處理邏輯上。</p>
<p>只不過這背後有很多麻煩事要處理，像是：</p>
<ul>
<li>後端要建立對應前端操作的 API ，並且要顧及安全性 (例如防範 CSRF ) 。</li>
<li>前端要透過 XHR 發起要求，並處理 API 各種狀態的結果。</li>
<li>需要學習如何操作前端 DOM 元素，讓後端資訊可以綁在元素上或是透過元素的事件來取得元素上的資訊。</li>
<li>前端要維護介面狀態，在重新整理頁面後要能後端資料狀態同步。</li>
</ul>
<p>諸如此類的基礎工夫還不少，雖然現代化的前後端框架或工具都幫我們處理掉了，但實戰上要把它們兜起來還是要花掉不少時間。</p>
<p>有沒有什麼更好的方法，可以讓開發者可以做更少的事，而達到同樣的效果呢？</p>
<p><a href="https://laravel-livewire.com/">Livewire</a> 就是在這個概念下所產生的套件，目的就是為了減少開發者在前後端溝通時要花費的工夫。</p>
<p>這個套件很早我就在 <a href="https://laravel-news.com/laravel-livewire-1-0-0">Laravel News</a> 裡知道了，只是一直都沒有動力去試試。不過當 Laravel 官方釋出了 <a href="https://github.com/laravel/jetstream">Laravel Jetstream</a> 這個非常棒的服務平台骨架產生器時，我在研究的時候發現安裝 Jetsteam 的過程中，可以選擇 Livewire 或是 <a href="https://inertiajs.com/">Inertia.js</a> 。</p>

<div class="note">
<p>註： Inertia.js 的概念跟 Livewire 概念很像，也是主打不需要自行建立後端 API (但業務邏輯的撰寫還是必要的) ；不過它是從 SPA (Single Page Application) 的角度出發，著重在前端的開發上。在 IThome 鐵人賽上有<a href="https://ithelp.ithome.com.tw/users/20113602/ironman/3322">系列文</a>可以參考。</p>

</div>

<p>既然官方套件也開始用了，表示 Livewire 不會是曇花一現的技術；因此我就想好好地看看它是怎麼運作的，讓官方選擇它來當做套件的底層機制。</p>
<p>而要瞭解一個工具，最好的方法就是從實作開始。接著我會以官方文件的教學為主，簡單地分析 Livewire 的運作方式。</p>
<h2 id="初探-livewire">初探 Livewire</h2>
<p>由於 Livewire 是依附在 Laravel 應用程式上的機級，所以我們需要在本機建立一個 Laravel 應用程式。</p>
<p>首先我們安裝 Laravel Installer 或是將它升級至 4.0 ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ composer global require laravel/installer
</code></pre></div><p>接著建立並啟動一個 Laravel 的應用程式服務，這裡我用 Valet (Mac only) 來建立站台，其它環境請自行研究。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ laravel new livewire-demo
$ <span class="nb">cd</span> livewire-demo
$ valet link <span class="o">&amp;&amp;</span> valet secure
</code></pre></div><p>有了一個乾淨的 Laravel 應用程式環境，我們就可以來試玩一下 Livewire 了。</p>
<p>先安裝 livewire 套件：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ composer require livewire/livewire
</code></pre></div><p>我們看一下新增了什麼 routes ：</p>
<pre><code>$ php artisan route:list -c
+----------+----------------------------------+------------------------------------------------------+
| Method   | URI                              | Action                                               |
+----------+----------------------------------+------------------------------------------------------+
| ...      | ...                              | ...                                                  |
| GET|HEAD | livewire/livewire.js             | Livewire\Controllers\LivewireJavaScriptAssets@source |
| GET|HEAD | livewire/livewire.js.map         | Livewire\Controllers\LivewireJavaScriptAssets@maps   |
| POST     | livewire/message/{name}          | Livewire\Controllers\HttpConnectionHandler           |
| GET|HEAD | livewire/preview-file/{filename} | Livewire\Controllers\FilePreviewHandler@handle       |
| POST     | livewire/upload-file             | Livewire\Controllers\FileUploadHandler@handle        |
+----------+----------------------------------+------------------------------------------------------+
</code></pre><p>可以看到它提供了前端 assets 、供前端溝通用的訊息 API 、上傳檔案與檔案預覽的 API 。</p>
<p>不過這些 API 原則上知道就好，在實際開發時因為 Livewire 已經幫我們封裝這些 API 的操作，所以基本上不會看到它們。</p>
<p>然後我們要在 blade template (這裡為 <code>resources/views/welcome.blade.php</code> ) 上加入 Livewire 提供的 tag <code>&lt;livewire:styles /&gt;</code> 與 <code>&lt;livewire:scripts /&gt;</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;!DOCTYPE html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">&#34;utf-8&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>Laravel Livewire Demo<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
    <span class="c">&lt;!-- Styles --&gt;</span>
    <span class="p">&lt;</span><span class="nt">livewire:styles</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">livewire:scripts</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div>
<div class="note">
<p>註： Tag 是 Laravel 7 之後才支援的寫法，如果是 Laravel 6 以前的版本，必須用 <code>@livewireStyles</code> 及 <code>@livewireScripts</code> 。</p>

</div>

<p>接著打開頁面原始碼，我們可以看到 styles 和 scripts 分別被代換成以下 HTML ：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
    <span class="o">[</span><span class="nt">wire</span><span class="err">\</span><span class="p">:</span><span class="nd">loading</span><span class="o">],</span> <span class="o">[</span><span class="nt">wire</span><span class="err">\</span><span class="p">:</span><span class="nd">loading</span><span class="err">\</span><span class="p">.</span><span class="nc">delay</span><span class="o">]</span> <span class="p">{</span>
        <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="o">[</span><span class="nt">wire</span><span class="err">\</span><span class="p">:</span><span class="nd">offline</span><span class="o">]</span> <span class="p">{</span>
        <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="o">[</span><span class="nt">wire</span><span class="err">\</span><span class="p">:</span><span class="nd">dirty</span><span class="o">]</span><span class="p">:</span><span class="nd">not</span><span class="o">(</span><span class="nt">textarea</span><span class="o">)</span><span class="p">:</span><span class="nd">not</span><span class="o">(</span><span class="nt">input</span><span class="o">)</span><span class="p">:</span><span class="nd">not</span><span class="o">(</span><span class="nt">select</span><span class="o">)</span> <span class="p">{</span>
        <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span>
...
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;/livewire/livewire.js?id=d3352e4f7c3be3e22a1f&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="p">&gt;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nb">window</span><span class="p">.</span><span class="nx">livewire</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">warn</span><span class="p">(</span><span class="s1">&#39;Livewire: It looks like Livewire\&#39;s @livewireScripts JavaScript assets have already been loaded. Make sure you aren\&#39;t loading them twice.&#39;</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="nb">window</span><span class="p">.</span><span class="nx">livewire</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Livewire</span><span class="p">();</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">Livewire</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">livewire</span><span class="p">;</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">livewire_app_url</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">livewire_token</span> <span class="o">=</span> <span class="s1">&#39;syJQNxWSjopRJLvEKlImImypOHrzRLq7MJgArStk&#39;</span><span class="p">;</span>

    <span class="cm">/* Make Alpine wait until Livewire is finished rendering to do its thing. */</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">deferLoadingAlpine</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">callback</span><span class="p">)</span> <span class="p">{</span>
        <span class="nb">window</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">&#39;livewire:load&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="nx">callback</span><span class="p">();</span>
        <span class="p">});</span>
    <span class="p">};</span>

    <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s2">&#34;DOMContentLoaded&#34;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nb">window</span><span class="p">.</span><span class="nx">livewire</span><span class="p">.</span><span class="nx">start</span><span class="p">();</span>
    <span class="p">});</span>
<span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div><p>大致上就是 CSS 用來隱藏載入中或離線等狀態的 Livewire 元件，而 JS 除了定義跟後端溝通的連線資訊外，也用來監聽頁面上所有 Livewire 元件的事件；至於細節就不多解釋了，相信大家應該都看得懂。</p>
<p>接著來看看<a href="(https://laravel-livewire.com/docs/quickstart)">官方提供的例子</a>，這裡我們要新增一個手動計數器。</p>
<p>首先用以下指令來建立計數器的元件與樣版：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ php artisan make:livewire counter
 COMPONENT CREATED  🤙

CLASS: app/Http/Livewire/Counter.php
VIEW:  resources/views/livewire/counter.blade.php
</code></pre></div><p><code>app/Http/Livewire/Counter.php</code> 的內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">namespace</span> <span class="nx">App\Http\Livewire</span><span class="p">;</span>

<span class="k">use</span> <span class="nx">Livewire\Component</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">Counter</span> <span class="k">extends</span> <span class="nx">Component</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">render</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="nx">view</span><span class="p">(</span><span class="s1">&#39;livewire.counter&#39;</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>注意這裡的 <code>Livewire Component</code> 跟 Laravel 7 之後的 <a href="https://laravel.com/docs/7.x/blade#components">Blade Components</a> 的實作是不一樣的，雖然它們的用法基本上很像，但還是別搞混了。</p>
<p>再來看 <code>resources/views/livewire/counter.blade.php</code> ，它目前只有一個 <code>div</code> 標籤對：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
    {{-- 一句隨機挑選的俚語 --}}
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p>現在我們要讓這個計數器動起來了，第一步是在 <code>Counter</code> 類別補上計數器的暫存狀態與行為邏輯：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="c1">// ...
</span><span class="c1"></span>
<span class="k">class</span> <span class="nc">Counter</span> <span class="k">extends</span> <span class="nx">Component</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="nv">$count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">increment</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">count</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">decrement</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">count</span><span class="o">--</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// ...
</span><span class="c1"></span><span class="p">}</span>
</code></pre></div><p>下一步是修改 <code>counter.blade.php</code> 的內容：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;text-align: center&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">wire:click</span><span class="o">=</span><span class="s">&#34;increment&#34;</span><span class="p">&gt;</span>+<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>{{ $count }}<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">wire:click</span><span class="o">=</span><span class="s">&#34;decrement&#34;</span><span class="p">&gt;</span>-<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p>你會發現我們用 <code>wire:click</code> 去綁定後端 <code>Counter</code> 類別的 <code>increment</code> 和 <code>decrement</code> 這兩個方法，這就是 Livewire 主打的核心功能。</p>
<p>現在我們要來使用這個計數器元件了。修改 <code>resources/views/welcome.blade.php</code> ，在 <code>&lt;body&gt;</code> 後加入 <code>&lt;livewire:counter /&gt;</code> 這個 tag ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">...

<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">livewire:counter</span> <span class="p">/&gt;</span>

...
</code></pre></div><p>接著重整瀏覽器頁面，你應該會看到以下畫面：</p>
<p><img src="/resources/laravel-livewire/counter.png" alt=""></p>
<p>再看看頁面原始檔，你會發現 <code>counter.blade.php</code> 的 <code>div</code> 標籤多了一些屬性：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">wire:id</span><span class="o">=</span><span class="s">&#34;VhVG7h01POBNuWMvpaVx&#34;</span>
     <span class="na">wire:initial-data</span><span class="o">=</span><span class="s">&#34;{&amp;quot;fingerprint&amp;quot;:{&amp;quot;id&amp;quot;:&amp;quot;VhVG7h01POBNuWMvpaVx&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;counter&amp;quot;,&amp;quot;locale&amp;quot;:&amp;quot;en&amp;quot;},&amp;quot;effects&amp;quot;:{&amp;quot;listeners&amp;quot;:[],&amp;quot;path&amp;quot;:&amp;quot;https:\/\/livewire-demo.test&amp;quot;},&amp;quot;serverMemo&amp;quot;:{&amp;quot;children&amp;quot;:[],&amp;quot;errors&amp;quot;:[],&amp;quot;htmlHash&amp;quot;:&amp;quot;31258d01&amp;quot;,&amp;quot;data&amp;quot;:{&amp;quot;count&amp;quot;:0},&amp;quot;checksum&amp;quot;:&amp;quot;2ed61de3befc848b43c1cb84eb3bf9f2a65cae1586c1631bc4d49efa284ab3ac&amp;quot;}}&#34;</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;text-align: center&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">wire:click</span><span class="o">=</span><span class="s">&#34;increment&#34;</span><span class="p">&gt;</span>+<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>0<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">wire:click</span><span class="o">=</span><span class="s">&#34;decrement&#34;</span><span class="p">&gt;</span>-<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p><code>wire:id</code> 是元件的唯一識別，假設頁面有多個計數器， <code>wire:id</code> 能讓 Livewire 知道目前我們操作的是哪一個計數器。</p>
<p>再把屬性 <code>wire:initial-data</code> 裡的 <code>&amp;quot;</code> 替換成 <code>&quot;</code> 後再排版一下就可以得到：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="p">{</span>
  <span class="nt">&#34;fingerprint&#34;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;VhVG7h01POBNuWMvpaVx&#34;</span><span class="p">,</span>
    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;counter&#34;</span><span class="p">,</span>
    <span class="nt">&#34;locale&#34;</span><span class="p">:</span> <span class="s2">&#34;en&#34;</span>
  <span class="p">},</span>
  <span class="nt">&#34;effects&#34;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&#34;listeners&#34;</span><span class="p">:</span> <span class="p">[],</span>
    <span class="nt">&#34;path&#34;</span><span class="p">:</span> <span class="s2">&#34;https://livewire-demo.test&#34;</span>
  <span class="p">},</span>
  <span class="nt">&#34;serverMemo&#34;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&#34;children&#34;</span><span class="p">:</span> <span class="p">[],</span>
    <span class="nt">&#34;errors&#34;</span><span class="p">:</span> <span class="p">[],</span>
    <span class="nt">&#34;htmlHash&#34;</span><span class="p">:</span> <span class="s2">&#34;31258d01&#34;</span><span class="p">,</span>
    <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
      <span class="nt">&#34;count&#34;</span><span class="p">:</span> <span class="mi">0</span>
    <span class="p">},</span>
    <span class="nt">&#34;checksum&#34;</span><span class="p">:</span> <span class="s2">&#34;2ed61de3befc848b43c1cb84eb3bf9f2a65cae1586c1631bc4d49efa284ab3ac&#34;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>這就是 Livewire 用來讓前端可以跟後端溝通的資訊，而且其中也加上一些防止修改的措施，畢竟「<strong>不要相信用戶端來的所有資訊</strong>」是後端開發的重要觀念之一。</p>
<p>接下來打開瀏覽器的除錯工具，觀察跟 XHR 有關的網路連線。然後按一下頁面上的按鈕 <code>+</code> 來讓數字發生變化，這時候我們就會看到 Livewire 觸發了一個 XHR 的連線，它打到以下這個 API ：</p>
<pre><code>[POST] https://livewire-demo.test/livewire/message/counter
</code></pre><p>這就是我們上面看到那個指向 <code>livewire/message/{name}</code> 的 Laravel route 。</p>
<p>而它的 request payload 長這樣：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="p">{</span>
  <span class="nt">&#34;fingerprint&#34;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="s2">&#34;VhVG7h01POBNuWMvpaVx&#34;</span><span class="p">,</span>
    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;counter&#34;</span><span class="p">,</span>
    <span class="nt">&#34;locale&#34;</span><span class="p">:</span> <span class="s2">&#34;en&#34;</span>
  <span class="p">},</span>
  <span class="nt">&#34;serverMemo&#34;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&#34;children&#34;</span><span class="p">:</span> <span class="p">[],</span>
    <span class="nt">&#34;errors&#34;</span><span class="p">:</span> <span class="p">[],</span>
    <span class="nt">&#34;htmlHash&#34;</span><span class="p">:</span> <span class="s2">&#34;31258d01&#34;</span><span class="p">,</span>
    <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
      <span class="nt">&#34;count&#34;</span><span class="p">:</span> <span class="mi">0</span>
    <span class="p">},</span>
    <span class="nt">&#34;checksum&#34;</span><span class="p">:</span> <span class="s2">&#34;2ed61de3befc848b43c1cb84eb3bf9f2a65cae1586c1631bc4d49efa284ab3ac&#34;</span>
  <span class="p">},</span>
  <span class="nt">&#34;updates&#34;</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;callMethod&#34;</span><span class="p">,</span>
      <span class="nt">&#34;payload&#34;</span><span class="p">:</span> <span class="p">{</span>
        <span class="nt">&#34;method&#34;</span><span class="p">:</span> <span class="s2">&#34;increment&#34;</span><span class="p">,</span>
        <span class="nt">&#34;params&#34;</span><span class="p">:</span> <span class="p">[]</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div><p>可以發現到它丟了一個 <code>increment</code> 方法名稱給後端。也就是說 Livewire 並不是在前端進行運算，而是透過 XHR 傳遞方法名稱來要求後端的計數器元件對 <code>count</code> 值進行計算。</p>
<p>要特別注意，這時候 Livewire 是用<strong>前端的 <code>count</code> 值</strong>加上要呼叫的方法 <code>increment</code> 給後端的運算邏輯，來讓 <code>count</code> 值加一；如果不搭配存儲機制的話 (例如 Session / DB 等) ，後端並不會記住目前的計數器的 <code>count</code> 值，所以重新整理頁面後它就會還原回元件的初始值。</p>
<p>最後來看看回應的內容：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="p">{</span>
  <span class="nt">&#34;effects&#34;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&#34;html&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;div wire:id=\&#34;VhVG7h01POBNuWMvpaVx\&#34; style=\&#34;text-align: center\&#34;&gt;\n    &lt;button wire:click=\&#34;increment\&#34;&gt;+&lt;/button&gt;\n    &lt;h1&gt;1&lt;/h1&gt;\n    &lt;button wire:click=\&#34;decrement\&#34;&gt;-&lt;/button&gt;\n&lt;/div&gt;\n&#34;</span><span class="p">,</span>
    <span class="nt">&#34;dirty&#34;</span><span class="p">:</span> <span class="p">[</span>
      <span class="s2">&#34;count&#34;</span>
    <span class="p">]</span>
  <span class="p">},</span>
  <span class="nt">&#34;serverMemo&#34;</span><span class="p">:</span> <span class="p">{</span>
    <span class="nt">&#34;htmlHash&#34;</span><span class="p">:</span> <span class="s2">&#34;fbf6038e&#34;</span><span class="p">,</span>
    <span class="nt">&#34;data&#34;</span><span class="p">:</span> <span class="p">{</span>
      <span class="nt">&#34;count&#34;</span><span class="p">:</span> <span class="mi">1</span>
    <span class="p">},</span>
    <span class="nt">&#34;checksum&#34;</span><span class="p">:</span> <span class="s2">&#34;6685b4e7d45f9c4db431e5c267eed759eae1c950096ced91feccfc94cd109411&#34;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>在計數器的運算完成後，就會將 blade 樣版套用新變數結果後所產生的 HTML 內容回傳，並取代掉目前元件的 HTML 。</p>
<p>至此，大致上可以瞭解 Livewire 的運作流程其實跟我們平常做的事沒什麼差別，只不過 Livewire 封裝了背後的細節，使其更為自動化，讓開發者可以更專注在自己的業務邏輯上。當然如果大家對細節有興趣的話，可以參考官方文件或原始檔。</p>
<h2 id="優缺點分析">優缺點分析</h2>
<p>雖然還沒玩得很深，不過大致上可以想到 Livewire 有以下的優點：</p>
<ul>
<li>簡單的情境幾乎不需要寫 JavaScript 。</li>
<li>元件的狀態由後端管理，前端的工變少，適合對前端不那麼熟悉的後端開發者。</li>
</ul>
<p>只是就我的經驗來判斷，它也可能有以下的缺點：</p>
<ul>
<li>一些跟後端邏輯無關的複雜前端 UI 或機制，可能還是需要藉助其它 UI 框架或套件來協助。</li>
<li>複雜的 UI 元件切法和後端元件的搭配可能要花點時間理解。</li>
<li>因為前後端的界線變得模糊，會容易讓新手搞混前後端的運作機制。</li>
</ul>
<p>當然這也只是我淺嘗後的印象，也許再深入一點之後，可能又會有不同的見解。</p>
<h2 id="結論">結論</h2>
<p>不得不說 Livewire 真的是一個可以節省開發者不少力氣的有趣工具；當然它所應用的概念我想也不是新的，只不過對於傳統 Laravel 開發者來說，確實可能一下子難以接受。現階段如果要導入 Livewire ，我建議從比較功能比較簡單，情境沒有那麼複雜的新專案開始，去熟悉它的運作方式和缺點。</p>
<p>本文只是非常簡單地介紹了 Livewire 的概念和用法，我知道各位心中對它還有很多疑問，這些你都可以試著從官方文件和 issue 中找答案。至於更進一步的應用方式，當然就是推薦 Laravel 官方釋出的 <a href="https://github.com/laravel/jetstream">Laravel Jetstream</a> 。</p>
<h2 id="參考">參考</h2>
<ul>
<li><a href="https://laravel-livewire.com/docs/quickstart">Laravel Livewire::Quickstart</a></li>
<li><a href="https://medium.com/@avnshrathod/laravel-livewire-installation-and-demo-dfaf930f5f64">Laravel livewire installation and demo</a></li>
<li><a href="https://laravelarticle.com/laravel-livewire-crud-tutorial">Laravel Livewire CRUD tutorial</a></li>
</ul>]]></content>
		</item>
		
		<item>
			<title>我的 2019 年</title>
			<link>https://jaceju.net/my-2019/</link>
			<pubDate>Thu, 26 Dec 2019 16:59:15 +0800</pubDate>
			
			<guid>https://jaceju.net/my-2019/</guid>
			<description>又到了一年的尾聲了，年初回顧了去年的點點滴滴，趁著今年還有一點點時間，也應該好好來整理一下自己做了哪些事。 要把 2019 年我做的事總結成一句話的話，</description>
			<content type="html"><![CDATA[<p>又到了一年的尾聲了，年初回顧了<a href="/my-2018/">去年的點點滴滴</a>，趁著今年還有一點點時間，也應該好好來整理一下自己做了哪些事。</p>
<p>要把 2019 年我做的事總結成一句話的話，就是：</p>
<blockquote>
<p><strong>逐漸往合理的架構去改善，好讓接下來要做的事更快更容易。</strong></p>
</blockquote>
<p>當然我想這件事本來就是程式開發基本中的基本，只不過相較於其它技術上我比較著重這部份，所以就被我選做今年的總結了。</p>
<!-- raw HTML omitted -->
<h2 id="2019-年做了什麼">2019 年做了什麼</h2>
<p>還是先粗略地列一下自己今年做了哪些事：</p>
<ul>
<li>完善並發佈一起聽的監控後台。</li>
<li>某 Web 應用程式改版 (因為還沒完成，所以暫時不能公開是什麼。)</li>
<li>開發上面 Web 應用程式新版本所需要的 Web API ，同時也提供給其它 Web 服務使用。</li>
<li>重新學習前端開發。</li>
<li>編寫團隊用的開發文件。</li>
<li>寫了幾篇簡報，整理一些開發經驗和個人學習筆記。</li>
<li>只讀了幾本經典技術書籍，但非技術性質的書倒是看了不少。</li>
<li>破關了幾款遊戲。</li>
</ul>
<h2 id="今年有什麼體悟">今年有什麼體悟？</h2>
<p>在程式開發上的觀念大致就是那些老生常談的東西，今年在開發上也脫離不了它們。不過還是重點地整理一下一些體悟，也當做自我的成長紀錄。</p>
<h3 id="架構好對需求的反應就快">架構好對需求的反應就快</h3>
<p>老實說我自己再怎麼練寫 code 的速度，節省下來的時間也沒有比我把架構做好所節省的時間來得多。當然這不是說寫 code 的速度不重要，只不過前題是在面對產品需求時，應該是去思考：</p>
<blockquote>
<p><strong>先看清楚路該怎麼走比較省時，而不是一開始就想怎麼跑比較快。</strong></p>
</blockquote>
<p>我自己在程式架構的設計上大致上著重幾個點：</p>
<ul>
<li><strong>易於理解</strong>：架構不是設計來給自己自嗨的，而是要讓同事也能清楚它的脈絡。</li>
<li><strong>易於開發</strong>：當架構的抽象化夠明確時，很多時候我只需要添加測試和新類別，就可以很快滿足需求。</li>
<li><strong>善用特性</strong>：儘可能從程式語言的特性來設計架構，除了要好開發之外，更要避免無謂的效能浪費。</li>
</ul>
<h3 id="架構簡單才好改動">架構簡單才好改動</h3>
<p>早期在設計架構時，我們總是會把很多狀況考慮進來；結果在不斷地添加條件後，整個架構就變得越來越臃腫。但事實證明，這些考量並沒有那麼必要，而且還大大地削弱了後續架構的可調整性。</p>
<p>因此後來我在設計架構時一直把握這個原則：</p>
<blockquote>
<p><strong>除非需求很明確地一定要做，否則不要對系統做出過多的假設。</strong></p>
</blockquote>
<p>從這個原則裡，我延伸出幾個重點：</p>
<ul>
<li>先針對最關鍵的核心問題去設計，讓架構圍繞在產品的核心去往外延展，最後找到最合理而且簡單的設計。</li>
<li>不要一開始就試著套用設計模式，而是去發掘這個架構可以靠向哪個模式，或根本不適用任何模式。</li>
<li>將分解架構成模組並逐一抽象化；抽象化是避免對模組有太多假設，同時也定義出每個模組的職責與邊界。</li>
</ul>
<h3 id="code-review-就是改善的機會">Code Review 就是改善的機會</h3>
<p>雖然在設計架構時我會跟一起合作專案的同事用白板畫圖討論，但實際的程式碼寫出來出後，可能就跟當初想像的不太一樣了。所以在我設計好程式架構，且自己試著在上面開發新功能後，一定都會請同事幫忙 code review ；在 code review 的過程中，同事就會給我非常多架構如何改善的建議。</p>
<p>針對架構的 code review ，大致上有以下方向：</p>
<ul>
<li><strong>實作共識</strong>：每個人對語言特性的掌握不一定相同，因此可以從程式碼來確認大家對架構如何實作是否有正確的共識。</li>
<li><strong>需求導向</strong>：有時候達成目的的架構不會只有一種形式，所以請對需求已經有類似經驗的同事 code review 時就可以找到符合需求的形式。</li>
<li><strong>發現缺點</strong>：有時實作時似乎沒什麼問題的架構，當 code review 某些點後才發現會有預想之外的狀況。這時候就要迅速調整思維，重新討論出架構在面臨需求時的缺點。</li>
</ul>
<h2 id="簡報分享">簡報分享</h2>
<p>雖然沒有公開的演講，但為了不讓自己做簡報的能力退化，今年我還是生出幾個可以分享的簡報。</p>
<h3 id="專案改版紀錄我們的鳳凰專案">專案改版紀錄：我們的鳳凰專案</h3>
<p>今年花了一點時間把自己在某個舊專案改版的過程記錄下來，因為要碰到這類專案的機會雖然很多，但真正能成功的卻很少，原因在於政治因素的干擾以及沒有處理好過渡時期的轉換。</p>
<!-- raw HTML omitted -->
<h3 id="前端開發基礎-vuejs-入門">前端開發基礎： Vue.js 入門</h3>
<p>這份簡報其實是很早 Vue.js 1 的時候做的，不過因為今年有前端專案的需求，因此特地將它改成了 Vue.js 2 的版本。</p>
<!-- raw HTML omitted -->
<h3 id="個人能力學習-compiler-入門筆記">個人能力學習： Compiler 入門筆記</h3>
<p>在資訊領域打滾這麼久，老實說我還是覺得自己比不上那些科班出身的人；像是編譯器這種很基礎的知識，我一直沒能理解得很深入。為了內部分享時不再吃老本，花了一點時間把這方面的基礎從頭學了一遍；這才發現其實有很多平常看到的工具，它們的背後都是運用到裡面的原理。</p>
<!-- raw HTML omitted -->
<h2 id="總結">總結</h2>
<p>今年雖然是跨過不惑之年，但老實說過得有點迷惘，也沒什麼心情參與社群的活動；休閒時間大多花在閱讀上面，吸收一些跟技術無關的知識。生活並不是那麼精采，但平淡是福。</p>
<p>然後我還是要感謝我的同事們，如果沒有他們的智慧與溝通能力，我想有很多事情我自己是很難順利且漂亮地處理掉。</p>
<p>2020 年似乎會有很多變化，希望自己也能夠繼續平常心去面對。</p>
]]></content>
		</item>
		
		<item>
			<title>關於 PHP Traversing 的這檔事</title>
			<link>https://jaceju.net/all-about-php-traversing/</link>
			<pubDate>Tue, 24 Dec 2019 10:37:59 +0800</pubDate>
			
			<guid>https://jaceju.net/all-about-php-traversing/</guid>
			<description>foreach 大概是 PHP 程式中最常見的語法結構之後，本文將會介紹 foreach 的一些觀念，以及幾個跟它相關的 PHP 7 特色。 foreach 起手式 常見的 foreach 用法 一般我們最常看到 foreach 用在遍歷 (traversing)</description>
			<content type="html"><![CDATA[<p><code>foreach</code> 大概是 PHP 程式中最常見的語法結構之後，本文將會介紹 <code>foreach</code> 的一些觀念，以及幾個跟它相關的 PHP 7 特色。</p>
<!-- raw HTML omitted -->
<h2 id="foreach-起手式">foreach 起手式</h2>
<h3 id="常見的-foreach-用法">常見的 foreach 用法</h3>
<p>一般我們最常看到 <a href="https://www.php.net/manual/en/control-structures.foreach.php"><code>foreach</code></a> 用在遍歷 (traversing) 陣列裡的元素，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$arr = [1, 2, 3];

foreach ($arr as $num) {
    echo &#34;$num\n&#34;;
}
</code></pre></div><p>而如果想遍歷關連式陣列 (associative array) ，同時取得元素的鍵 (key) 與值 (value) ，可以用以下語法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$arr = [&#39;a&#39; =&gt; 1, &#39;b&#39; =&gt; 2, &#39;c&#39; =&gt; 3];

foreach ($arr as $key =&gt; $num) {
    echo &#34;$key =&gt; $num\n&#34;;
}
</code></pre></div><p>如果陣列元素本身也是陣列，我們稱為「巢狀陣列 (nested array) 」。在遍歷巢狀陣列時，我們可以用 <code>list(...)</code> 來解構每個陣列元素：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$arr = [
    [&#39;a&#39;, 1],
    [&#39;b&#39;, 2],
    [&#39;c&#39;, 3],
];

foreach ($arr as list($alpha, $num)) {
    echo &#34;$alpha =&gt; $num\n&#34;;
}
</code></pre></div><p>在 PHP 7.1 之後，你可以用方括號來取代 <code>list()</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">foreach ($arr as [$alpha, $num]) {
    echo &#34;$alpha =&gt; $num\n&#34;;
}
</code></pre></div><p>當然別忘了 PHP 7.1 之後， <code>list()</code> 可以用指定鍵的方式來取值：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$users = [
    [&#39;id&#39; =&gt; 1, &#39;name&#39; =&gt; &#39;Alice&#39;, &#39;age&#39; =&gt; 18],
    [&#39;id&#39; =&gt; 2, &#39;name&#39; =&gt; &#39;Bob&#39;, &#39;age&#39; =&gt; 24],
    [&#39;id&#39; =&gt; 3, &#39;name&#39; =&gt; &#39;Carl&#39;, &#39;age&#39; =&gt; 33]
];

foreach ($users as [&#39;name&#39; =&gt; $name, &#39;age&#39; =&gt; $age, &#39;id&#39; =&gt; $id]) {
    var_dump(&#34;$id $name: $age&#34;);
}
</code></pre></div><h3 id="如果沒有-foreach">如果沒有 foreach</h3>
<p>PHP 有提供幾個函式用來操作陣列裡的指標，以及取得指標指向的陣列元素；分別是 <a href="https://www.php.net/manual/en/function.reset.php"><code>reset</code></a> / <a href="https://www.php.net/manual/en/function.prev.php"><code>prev</code></a> / <a href="https://www.php.net/manual/en/function.next.php"><code>next</code></a> / <a href="https://www.php.net/manual/en/function.current.php"><code>current</code></a> / <a href="https://www.php.net/manual/en/function.end.php"><code>end</code></a> 。</p>
<p>你可以用 <a href="https://www.php.net/manual/en/control-structures.while.php"><code>while</code></a> 搭配以上的函式來遍歷陣列：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$arr = [null, 1, 2, false, null, 3, 4];

// 直接找到最後一個元素
// 這裡指標是指向最後一個元素
var_dump(end($arr));

// 因為指標已經跑到最後一個元素的位置
// 所以要重置指標
reset($arr);

// 遍歷陣列裡的元素
while (!is_null(key($arr))) {
    var_dump(current($arr));
    next($arr);
}

// 指標的位置已經沒有元素了
var_dump(current($arr));
</code></pre></div><p>另一個不用 <code>foreach</code> 的方法是使用 <code>array_walk</code> 這個函式：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$arr = [1, 2, 3];

array_walk($arr, function ($item) {
    echo &#34;$item &#34;;
});

$arr = [&#39;a&#39; =&gt; 1, &#39;b&#39; =&gt; 2, &#39;c&#39; =&gt; 3];

array_walk($arr, function ($item, $key) {
    echo &#34;$key =&gt; $item&#34;;
});
</code></pre></div><h3 id="用-foreach-來列舉物件屬性">用 foreach 來列舉物件屬性</h3>
<p><code>foreach</code> 也可以用來列舉 (listing) 物件的屬性，只要該物件不是屬於可遍歷的物件。</p>
<p>當你對一個物件實體用 <code>foreach</code> 來列舉屬性的話，你只能看到它的公開屬性：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class MyClass
{
    public $publicVar = &#39;public var&#39;;

    protected $protectedVar = &#39;protected var&#39;;

    private $privateVar = &#39;private var&#39;;
}

$class = new MyClass();

foreach ($class as $key =&gt; $value) {
    echo &#34;$key =&gt; $value\n&#34;;
}
</code></pre></div><p>但是如果你是在物件內部對 <code>$this</code> 做列舉屬性，那麼你可以看到這個物件所有的屬性：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class MyClass
{
    public $publicVar = &#39;public var&#39;;

    protected $protectedVar = &#39;protected var&#39;;

    private $privateVar = &#39;private var&#39;;

    public function iterateSelf()
    {
        foreach ($this as $key =&gt; $value) {
            print &#34;$key =&gt; $value\n&#34;;
        }
    }
}

$class = new MyClass();

$class-&gt;iterateSelf();
</code></pre></div><p>注意，之所以特意用「列舉屬性」將「遍歷元素」的概念區分開來，實在是因為 <code>foreach</code> 對 PHP 物件的操作真的很微妙。</p>
<p>一般來說，我們希望用 <code>foreach</code> 來遍歷集合的元素，而不是列舉物件的屬性；所以當物件屬於一個集合時，就需要讓物件所屬的類別實作一些特別的介面。</p>
<h2 id="用-foreach-來遍歷的介面">用 foreach 來遍歷的介面</h2>
<h3 id="traversable">Traversable</h3>
<p>如果你需要的是一個可以被遍歷的物件 (通常是集合物件) ，那麼你可以檢查它是不是屬於 <code>Traversable</code> 這個介面。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">if ($obj instanceof Traversable) {
    // ...
}
</code></pre></div><p>但是要注意 <code>Traversable</code> 的幾個特點：</p>
<ol>
<li>類別不能直接實作 <code>Traversable</code> 介面。</li>
<li>雖然陣列可以用 <code>foreach</code> 來遍歷，但它並不屬於 <code>Traversable</code> 介面。</li>
</ol>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// 不能直接實作
class MyExample implements Traversable {} // Error

// 陣列不屬於 Traversable
$arr = [1, 2, 3];
var_dump($arr instanceof Traversable); // false
</code></pre></div><p>再次強調：物件雖然可以用 <code>foreach</code> 來操作，但它不一定是 <code>Traversable</code> 。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// 物件可以 foreach 列舉屬性，但不一定是 Traversable
$obj = (object) [&#39;a&#39; =&gt; 1, &#39;b&#39; =&gt; 2, &#39;c&#39; =&gt; 3];
foreach ($obj as $key =&gt; $value) {
    echo &#34;$key =&gt; $value\n&#34;;
}
var_dump($obj instanceof Traversable); // false

class MyExample {}
$obj = new MyExample();
var_dump($obj instanceof Traversable); // false
</code></pre></div><h3 id="iterator-與-iteratoraggregate">Iterator 與 IteratorAggregate</h3>
<p>由於不能直接實作 <code>Traversable</code> 介面，官方建議應該改為實作 <code>Iterator</code> 或是 <code>IteratorAggregate</code> 這類的介面，它們都繼承自 <code>Traversable</code> 介面。</p>
<p><code>Iterator</code> 介面就如同前面介紹的 <code>next</code> 、 <code>current</code> 等函式一樣，提供了操作指標與取得指標所指向的元素等方法介面，以供類別來實作。</p>
<p>以下是一個很典型的範例：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class IteratorExample implements Iterator
{
    private $position = 0;

    private $data = [];

    public function __construct(array $data)
    {
        $this-&gt;position = 0;
        $this-&gt;data = $data;
    }

    public function rewind()
    {
        $this-&gt;position = 0;
    }

    public function current()
    {
        return $this-&gt;data[$this-&gt;position];
    }

    public function key()
    {
        return $this-&gt;position;
    }

    public function next()
    {
        ++$this-&gt;position;
    }

    public function valid()
    {
        return array_key_exists(
            $this-&gt;position,
            $this-&gt;data
        );
    }
}
</code></pre></div><p>我們可以用 <code>while</code> 敘述來遍歷 <code>Iterator</code> 物件裡的元素：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$it = new IteratorExample([&#39;a&#39;, null, &#39;b&#39;, &#39;c&#39;]);

$it-&gt;rewind();

while ($it-&gt;valid()) {
    $key = $it-&gt;key();
    var_dump($it-&gt;current());
    $it-&gt;next();
}
</code></pre></div><p>當然也可以用 <code>foreach</code> 來遍歷，因為這時所生成的物件實體已經屬於 <code>Traversable</code> 介面了：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">foreach ($it as $value) {
    var_dump($value);
}

var_dump($it instanceof Traversable); // true
</code></pre></div><p>實作 <code>Iterator</code> 介面的好處，就是可以依照自定義的邏輯來遍歷物件內的元素，這在某些特別的情境下很好用。</p>
<p>另一個更為簡便的介面是 <code>IteratorAggregate</code> ，它只需要實作 <code>getIterator</code> 這個方法就可以了， <code>getIterator</code> 方法必須回傳一個實作 <code>Iterator</code> 的物件。</p>
<p>PHP 內建了<a href="https://www.php.net/manual/en/spl.iterators.php">多種 Iterator 類別</a>讓開發者不需要從頭定義一個實作 <code>Iterator</code> 介面的類別，以下示範 <code>ArrayIterator</code> 這個類別如何跟 <code>IteratorAggregate</code> 介面搭配：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class IteratorAggregateExample implements IteratorAggregate
{
    private $data = [];

    public function __construct(array $data)
    {
        $this-&gt;data = $data;
    }

    public function getIterator()
    {
        return new ArrayIterator($this-&gt;data);
    }
}

$it = new IteratorAggregateExample([&#39;a&#39;, &#39;b&#39;, &#39;c&#39;]);

foreach ($it as $value) {
    echo &#34;$value\n&#34;;
}
</code></pre></div><p>用 <code>IteratorAggregate</code> 介面的好處是，你可以動態更換 <code>getIterator</code> 方法的回傳內容，而不必讓程式綁死在特定的 Iterator 類別上。</p>
<h3 id="iterable-型別">iterable 型別</h3>
<p><code>Traversable</code> 雖然可以用來判斷變數可否被遍歷，但它卻不適用在陣列變數上。因此 PHP 在 7.1 加入了一個偽型別 (pseudo-type) ： <a href="https://www.php.net/manual/en/language.types.iterable.php"><code>iterable</code></a> ，可以用在 <a href="https://www.php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration">Argument type declarations</a> (即 type hint) 及 <a href="https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration">Return type declarations</a> 上。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function getArr(): iterable
{
    return [&#39;a&#39; =&gt; 1, &#39;b&#39; =&gt; 2, &#39;c&#39; =&gt; 3];
}

function traverse(iterable $list)
{
    foreach ($list as $item) {
        echo &#34;$item &#34;;
    }
}

$arr = getArr();
$it = new ArrayIterator($arr);

var_dump(is_iterable($arr)); // true
var_dump(is_iterable($it));  // true

traverse($arr); // 1 2 3
traverse($it);  // 1 2 3
</code></pre></div><p>因此建議在程式中，可以用 <code>iterable</code> 型別來取代 <code>Traversable</code> 介面。</p>
<h2 id="如何在-foreach-時節省記憶體">如何在 foreach 時節省記憶體</h2>
<p>有時候要遍歷的對象，在生成後可能會佔用很大的記憶體空間，這可能會造成 PHP 執行時期的記憶體不足。以 <code>range</code> 為例：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function showMemUsageIf(bool $show = false): void
{
    if ($show) {
        echo round(memory_get_usage() / 1024 / 1024, 2) . &#39; MB&#39; . PHP_EOL;
    }
}

showMemUsageIf(true); // 15.42 MB
$a = range(1, 1000000);
foreach ($a as $num) { ... }
showMemUsageIf(true); // 47.43 MB
</code></pre></div><p>因此在 PHP 5.5 之後的版本，提供了 <a href="https://www.php.net/manual/en/class.generator.php">Generator</a> 這個類別，它可以協助我們在必要時才生成要處理的元素。</p>
<p>但是你不能直接用 <code>new</code> 來生成一個 <code>Generator</code> 類別的物件實體，取而代之的是 PHP 提供了 <code>yield</code> 這個新語法。</p>
<p><code>yield</code> 用途和 <code>return</code> 很類似，但 <code>yield</code> 只能放在函式或類別方法中，而包含了 <code>yield</code> 的函式或類別方法，其回傳值的型態都是 <code>Generator</code> 類別。</p>
<p>由於 <code>Generator</code> 類別實作了 <code>Iterator</code> 介面，所以可以用 <code>foreach</code> 來遍歷其物件實體。</p>
<p>先來看一個基本的 <code>yield</code> 用法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function gen()
{
    echo &#34;a: &#34;;
    yield 1;

    echo &#34;b: &#34;;
    yield 2;

    echo &#34;c: &#34;;
    yield 3;

    // 當然也可以放在迴圈裡
    foreach ([&#39;d&#39; =&gt; 4, &#39;e&#39; =&gt; 5] as $key =&gt; $num) {
        echo &#34;$key: &#34;;
        yield $num;
    }
}

foreach (gen() as $num) {
    echo &#34;$num &#34;;
}
</code></pre></div><p>可以看到當執行 <code>foreach</code> 的第一輪時， <code>gen()</code> 函式並不是一次跑完，而是會停在第一個 <code>yield</code> 上，並回傳 <code>yield</code> 後面的值。而第二輪則是從第一個 <code>yield</code> 後繼續執行，然後停在第二個 <code>yield</code> 。</p>
<p>由此可以看出，每當執行到 <code>yield</code> 時， <code>Generator</code> 就會保留目前的執行位置，並給出當下 <code>yield</code> 的結果，這在處理大量資料時就顯得非常有用了。</p>
<p>所以我們用 <code>Generator</code> 來重寫 <code>range</code> ，這個新函式我們命名為 <code>xrange</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function xrange($start, $limit, $step = 1) {
    if ($start <span class="err">&lt;</span> $limit) {
        if ($step <span class="err">&lt;</span>= 0) {
            throw new LogicException(&#39;Step must be +ve&#39;);
        }

        for ($i = $start; $i <span class="err">&lt;</span>= $limit; $i += $step) {
            yield $i;
        }
    } else {
        if ($step &gt;= 0) {
            throw new LogicException(&#39;Step must be -ve&#39;);
        }

        for ($i = $start; $i &gt;= $limit; $i += $step) {
            yield $i;
        }
    }
}

showMemUsageIf(true); // 15.42 MB
$a = xrange(1, 1000000);
foreach ($a as $num) { ... }
showMemUsageIf(true); // 15.42 MB
</code></pre></div><p>可以看到改用 <code>Generator</code> 後，記憶體的用量幾乎沒有什麼改變。</p>
<p>再舉一個例子，例如我們想要處理一個超大的 log 文字檔：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function readLog(string $file)
{
    $f = fopen($file, &#39;r&#39;);
    try {
        while ($line = fgets($f)) {
            yield $line;
        }
    } finally {
        fclose($f);
    }
}

foreach (readLog(&#34;access.log&#34;) as $line) {
    // echo $line;
}
</code></pre></div><p>透過這個方式，我們可以把每一行 log 的處理邏輯和讀檔邏輯分離開來，而且也不會佔用太多記憶體。</p>
<h3 id="generator-的特異功能">Generator 的特異功能</h3>
<p><code>Generator</code> 有幾個特別 <code>yield</code> 的用法，這裡特別介紹一下。</p>
<p><code>yield</code> 可以跟 <code>return</code> 一起使用，不過這時候必須用 <code>Generator::getReturn()</code> 來取得回傳值：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function getValues(): iterable
{
    yield &#39;value&#39;;
    return &#39;returnValue&#39;;
}

$values = getValues(); // $values 是一個 Generator
foreach ($values as $value) {
    var_dump($value);
}
echo $values-&gt;getReturn(); // &#39;returnValue&#39;
</code></pre></div><p><code>yield</code> 可以回傳鍵 (key) 與值 (value) ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function getMembers(): iterable
{
    yield &#39;a&#39; =&gt; 1;
    yield &#39;b&#39; =&gt; 2;
    yield &#39;c&#39; =&gt; 3;
}

foreach (getMembers() as $key =&gt; $value) {
    echo &#34;$key: $value\n&#34;;
}
</code></pre></div><p>巢狀的 Generator 可以用 <code>yield from</code> 來達成， <code>yield from</code> 後面要跟著一個 <code>iterable</code> 的值：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function one(): iterable
{
    yield 1;
}

function two_one(): iterable
{
    yield 2;
    yield from one();
}

function ten_to_seven(): iterable
{
    for ($i = 10; $i &gt;= 7; $i--) {
        yield $i;
    }
}

function count_down(): iterable
{
    yield from ten_to_seven();
    yield from [6, 5];
    yield from new ArrayIterator([4, 3]);
    yield from two_one();
}

foreach (count_down() as $num) {
    echo &#34;$num &#34;;
}
</code></pre></div><p>當然 <code>yield</code> 也可以用來回傳匿名函式：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function getFunctions()
{
    yield function ($num) {
        return $num;
    };

    yield function ($num) {
        return $num + 1;
    };

    yield function ($num) {
        return $num + 2;
    };
}

foreach (getFunctions() as $func) {
    var_dump($func(1));
}
</code></pre></div><p>但以下這個例子是錯誤的，因為 anonymous function 是一個 <code>Closure</code> 物件，不能接在 <code>yield from</code> 後面：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function getFunctions()
{
    yield from function ($num) {
        for ($i = 1; $i <span class="err">&lt;</span>= $num; $i++) {
            yield $i;
        }
    };
}

foreach (getFunctions() as $func) {
    // ...
}

// PHP Fatal error:  Uncaught Error: Can use &#34;yield from&#34; only with arrays and Traversables
</code></pre></div><p>直接 <code>yield</code> 就可以了：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function getFunctions()
{
    yield function ($num) {
        for ($i = 1; $i <span class="err">&lt;</span>= $num; $i++) {
            yield $i;
        }
    };

    yield function ($num) {
        for ($i = $num; $i &gt;= 1; $i--) {
            yield $i;
        }
    };
}

foreach (getFunctions() as $func) {
    foreach ($func(10) as $result) {
        var_dump($result);
    }
}
</code></pre></div><h2 id="在-phpunit-的-data-providers-中使用-yield">在 PHPUnit 的 Data Providers 中使用 yield</h2>
<p>在寫 PHPUnit 的測試案例時，我們通常會對某個單元的程式給出多組不同的測試資料，好驗證它的邏輯正確性；而這通常會透過 <a href="https://phpunit.readthedocs.io/en/8.5/writing-tests-for-phpunit.html#data-providers">Data Providers</a> 這個機制來完成，例如以下這個加法測試：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     *
     * @param int $a
     * @param int $b
     * @param int $expected
     */
    public function testAdd(int $a, int $b, int $expected)
    {
        $this-&gt;assertSame($expected, $a + $b);
    }

    public function additionProvider(): iterable
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 2],
        ];
    }
}
</code></pre></div><p>在上面的測試中， <code>additionProvider</code> 這個方法就是我們的 data-provider ，它必須回傳一個陣列 (這裡稱為 data set) 的陣列；而在測試案例 <code>testAdd</code> 這個方法上，我們要加入它的註解，並用 <code>@dataProvider</code> 來宣告我們的 data-provider 是 <code>additionProvider</code> 這個方法；而 <code>additionProvider</code> 的第二層陣列的項目 (即 data set 裡的每個元素) ，就會依序代入 <code>testAdd</code> 方法參數 <code>$a</code> 、 <code>$b</code> 、 <code>$expected</code> 裡。</p>
<p>不過很多剛用 data-provider 的朋友常會忘了要包第一層的陣列，導致測試錯誤；這裡我們可以改用 <code>yield</code> 來回傳每個 data set ，好避開這類的錯誤，同時也可以讓 data-provider 更加易讀 (雖然要多打幾個 <code>yield</code> 就是了) ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function additionProvider(): iterable
    {
        yield [0, 0, 0];
        yield [0, 1, 1];
        yield [1, 0, 1];
        yield [1, 1, 2];
    }
</code></pre></div><p>當然也可以用 <code>key =&gt; value</code> 的形式：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function additionProvider(): iterable
    {
        yield &#39;Set 1&#39; =&gt; [0, 0, 0];
        yield &#39;Set 2&#39; =&gt; [0, 1, 1];
        yield &#39;Set 3&#39; =&gt; [1, 0, 1];
        yield &#39;Set 4&#39; =&gt; [1, 1, 2];
    }
</code></pre></div><h2 id="參考">參考</h2>
<ul>
<li><a href="https://www.jianshu.com/p/86fefb0aacd9">php 之 Generator 生成器及 yield</a></li>
<li><a href="https://learnku.com/laravel/t/8704/using-yield-to-do-memory-optimization-in-php">在 PHP 中使用 <code>yield</code> 來做內存優化</a></li>
<li><a href="https://www.entropywins.wtf/blog/2017/10/09/yield-in-phpunit-data-providers/">Yield in PHPUnit data providers</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>《遺留系統重建實戰》導讀與心得</title>
			<link>https://jaceju.net/review-reengineering-legacy-software/</link>
			<pubDate>Wed, 06 Mar 2019 12:31:33 +0800</pubDate>
			
			<guid>https://jaceju.net/review-reengineering-legacy-software/</guid>
			<description>最近終於把《遺留系統重建實戰》看完了，有一種被深深震憾的感覺；你知道在看一本書時，能深刻地把自己的經驗投射在字裡行間是多爽的一件事嗎？每看一</description>
			<content type="html"><![CDATA[<p><img src="https://cf-assets2.tenlong.com.tw/products/images/000/111/651/original/51PgSbJUUNL.jpg?1525598072" alt="遺留系統重建實戰"></p>
<p>最近終於把<a href="https://www.tenlong.com.tw/products/9787115465856">《遺留系統重建實戰》</a>看完了，有一種被深深震憾的感覺；你知道在看一本書時，能深刻地把自己的經驗投射在字裡行間是多爽的一件事嗎？每看一段文字，就好像在講自己的故事，那種「作者你為什麼這麼懂！？」的感覺真的會讓人感動得掉淚！‬如果說<a href="https://www.tenlong.com.tw/products/9789864765867">《鳳凰專案》</a>是講組識變革和工作方法，那這本《遺留系統重建實戰》就是講專案重構與重建的方法與過程。</p>
<p>雖然它出版的時間只有兩年多 (這裡是指原文書) ，但在我心中它已經是經典了！以下我簡單做個導讀。</p>
<!-- raw HTML omitted -->
<p>第一章很生動地描寫出了舊有軟體專案的特徵與樣貌，還有它為什麼會變成這樣，以及我們在維護時的心境。你會發現作者在字裡行間的描述都很貼近我們所面對的真實狀況，不得不說作者這一路走來也是很苦呀。</p>
<p>第二章講的是你為什麼會對這些舊專案有負面的觀感，所以在重構之前我們要先深入瞭解專案以找到這些痛點。透過一些工具的輔助，我們可以將這些痛點數據化與可視化，以便把重構的時間花在有價值的地方。</p>
<p>第三章要開始準備重構了，這邊講的重點是決定該重構還是重寫，要讓整個團隊瞭解整個計劃有哪些風險，並得到上頭的批准。我自己也有這方面的經驗分享，請參考：<a href="https://jaceju.net/refactor-or-rebuild/">《面對 Legacy Code ，該重構還是重寫？》</a>。</p>
<p>第四章進入重構程式碼的階段，強調在重構程式碼時要注意的事情，還有如何透過工具和方法來協助重構。本章也用一些例子來說明該被重構的程式碼特徵，以及該怎麼補上自動化測試。作者也推薦<a href="https://www.tenlong.com.tw/products/9787111466253">《修改代碼的藝術》</a>這本書 (中譯本已絕版) ，<a href="https://www.facebook.com/jaceju/posts/2078275572254125">我前陣子也剛看完</a>。</p>
<p>第五章講的是重搭整個專案架構，畢竟有時只重構程式碼可能無法讓維護難度減低。本章講到怎麼把一大塊的單體架構分解成較小的模組，然後介紹幾個常見的專案系統架構，並分析了它們的優缺點。</p>
<p>第六章就是講舊專案沒救了要重寫，前提是你已經試過各種藥方要幫它重構。本章介紹了該針對哪些地方重寫的分析，瞭解舊有程式裡有實作哪些規格、避開什麼問題，以及各種新舊資料庫的轉移方法。</p>
<p>接下來的章節開始進入 DevOps 的部份。</p>
<p>第七章介紹了怎麼讓開發環境的建置自動化，目標是讓新人在接手專案時，可以快速地進入狀況，而不必花費心力在建立開發環境上；重點是專案的說明文件，以及如何使用自動化工具來快速建立開發環境，並且在本機就可以獨立運作。</p>
<p>第八章講到怎麼把自動化建置延伸到測試環境和線上環境，而且最好是放在雲端服務上以減輕維運負擔。另外就是不要讓各個運行環境有所差異，導致出現在特定環境才會發生的問題。</p>
<p>第九章提到建置、持續整合以及佈署的自動化。儘可能更換掉過時的工具，因為新的建置用的工具鏈通常會更容易使用且易於維護。</p>
<p>第十章也是最後一章，這邊算是前面章節的回顧。儘可能要使用現代化的技術來開發，團隊之間也要互相公開資訊；然後做好持續改善，並將一切自動化。可以的話要讓程式或系統儘可能地小，將它們分解後會更容易維護。</p>
<p>當然上面只是對這本書介紹個大概，但我相信如果你也對舊專案很困擾的話，這個導讀應該能引起你對這本書的興趣。可惜我太晚知道有這本書，所以在重建某個專案時真的是跌跌撞撞；不過也因為有這樣的經驗，才會對這本書的內容特別有感觸。之後有機會的話，我會分享一下我們真實的經驗給大家。</p>
]]></content>
		</item>
		
		<item>
			<title>在 Laravel 專案中整合 Vue CLI</title>
			<link>https://jaceju.net/integrate-vue-cli-into-laravel/</link>
			<pubDate>Tue, 12 Feb 2019 14:27:08 +0800</pubDate>
			
			<guid>https://jaceju.net/integrate-vue-cli-into-laravel/</guid>
			<description>自從 Vue CLI 3 發佈以來，如何將它整合在 Laravel 裡是不少開發者的疑問；因此 Vue 的老爸尤雨溪便針對這個問題寫了一個教學範例 ，本文即是參考該範例所寫，不過有根</description>
			<content type="html"><![CDATA[<p>自從 Vue CLI 3 發佈以來，如何將它整合在 Laravel 裡是不少開發者的疑問；因此 Vue 的老爸尤雨溪便針對這個問題寫了一個<a href="https://github.com/yyx990803/laravel-vue-cli-3">教學範例</a> ，本文即是參考該範例所寫，不過有根據 Laravel 的新特性做一些調整。</p>
<!-- raw HTML omitted -->
<h2 id="準備環境">準備環境</h2>
<p>開始前記得先安裝必要工具：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ composer global install laravel/installer
$ composer global install laravel/valet
$ npm i -g @vue/cli
</code></pre></div><h2 id="建立並修改-laravel-專案">建立並修改 Laravel 專案</h2>
<p>建立一個乾淨的 Laravel 5.7 專案，然後刪掉所有跟前端有關的目錄與檔案。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ laravel new project
$ <span class="nb">cd</span> project
$ rm -rf package.json <span class="se">\
</span><span class="se"></span>  webpack.mix.js <span class="se">\
</span><span class="se"></span>  yarn.lock <span class="se">\
</span><span class="se"></span>  resources/view/welcome.blade.php <span class="se">\
</span><span class="se"></span>  resources/<span class="o">{</span>js,sass<span class="o">}</span> <span class="se">\
</span><span class="se"></span>  public/<span class="o">{</span>js,css<span class="o">}</span>
</code></pre></div><p>然後修改 <code>routes/web.php</code> ，將內容置換成：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Illuminate\Support\Facades\Route</span><span class="p">;</span>

<span class="nx">Route</span><span class="o">::</span><span class="na">view</span><span class="p">(</span><span class="s1">&#39;/{any}&#39;</span><span class="p">,</span> <span class="s1">&#39;index&#39;</span><span class="p">)</span>
    <span class="o">-&gt;</span><span class="na">where</span><span class="p">(</span><span class="s1">&#39;any&#39;</span><span class="p">,</span> <span class="s1">&#39;.*&#39;</span><span class="p">);</span>
</code></pre></div><p>為了避免建置後的靜態檔案被加入版本控制中，修改專案根目錄下的 <code>.gitignore</code> ，加入以下內容：</p>
<pre><code>/public/js
/public/css
/public/img
/public/svg
/resources/views/index.blade.php
</code></pre><p>然後做版本控制。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ git init
$ git add .
$ git ci -m <span class="s2">&#34;Init project&#34;</span>
</code></pre></div><p>接著用 Valet 來設定網站：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ valet link
$ valet secure project
</code></pre></div><p>這樣我們測試用的網址即為 <a href="https://project.test"><code>https://project.test</code></a> 。</p>
<h2 id="建立前端用資料夾">建立前端用資料夾</h2>
<p>接下來用 Vue CLI 建立前端資料夾，以便管理所有跟前端有關的資源：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ vue create frontend
<span class="c1"># 這邊視專案規模來決定要用哪些設定</span>
</code></pre></div><p>建立 <code>frontend/vue.config.js</code> ，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="c1">// 在專案開發中如果呼叫 API 時會 pass 給這個 proxy 網址
</span><span class="c1"></span>  <span class="c1">// 這邊就用前面以 Valet 建立的網站網址
</span><span class="c1"></span>  <span class="nx">devServer</span><span class="o">:</span> <span class="p">{</span>
    <span class="nx">proxy</span><span class="o">:</span> <span class="s1">&#39;https://project.test&#39;</span>
  <span class="p">},</span>

  <span class="c1">// 建置前端靜態檔案時要擺放的目錄
</span><span class="c1"></span>  <span class="c1">// 在 package.json 也要調整 &#34;build&#34; 這個 script
</span><span class="c1"></span>  <span class="nx">outputDir</span><span class="o">:</span> <span class="s1">&#39;../public&#39;</span><span class="p">,</span>

  <span class="c1">// 開發階段修改 index.html 來讓 js/css 可以作用
</span><span class="c1"></span>  <span class="c1">// 上線階段則會修改 Laravel 的樣版
</span><span class="c1"></span>  <span class="nx">indexPath</span><span class="o">:</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">===</span> <span class="s1">&#39;production&#39;</span>
    <span class="o">?</span> <span class="s1">&#39;../resources/views/index.blade.php&#39;</span>
    <span class="o">:</span> <span class="s1">&#39;index.html&#39;</span>
<span class="p">}</span>
</code></pre></div><p>然後修改 <code>frontend/package.json</code> 的 <code>scripts.build</code> ，主要是避免把 <code>public</code> 整個刪除：</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">&#34;scripts&#34;: {
  &#34;serve&#34;: &#34;vue-cli-service serve&#34;,
<span class="gd">- &#34;build&#34;: &#34;vue-cli-service build&#34;,
</span><span class="gd"></span><span class="gi">+ &#34;build&#34;: &#34;rm -rf ../public/{js,css,img} &amp;&amp; vue-cli-service build --no-clean&#34;,
</span><span class="gi"></span>  &#34;lint&#34;: &#34;vue-cli-service lint&#34;
},
</code></pre></div><p>最後就可以用以下指令來開發或建置專案：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ <span class="nb">cd</span> frontend
$ yarn <span class="c1"># 安裝套件</span>
$ yarn serve <span class="c1"># 啟動開發用伺服器</span>
$ yarn build <span class="c1"># 建置上線用版本</span>
</code></pre></div>]]></content>
		</item>
		
		<item>
			<title>我如何從需求出發到完成一個專案</title>
			<link>https://jaceju.net/how-i-built-my-side-project/</link>
			<pubDate>Wed, 30 Jan 2019 12:04:37 +0800</pubDate>
			
			<guid>https://jaceju.net/how-i-built-my-side-project/</guid>
			<description>身為一個 Web 開發者，有時候我還是會問自己：「如果今天讓你一個人從頭開發出一個網站，你要怎麼開始呢？」這倒不是說我不知道怎麼做，而是要讓我自己徹</description>
			<content type="html"><![CDATA[<p>身為一個 Web 開發者，有時候我還是會問自己：「如果今天讓你一個人從頭開發出一個網站，你要怎麼開始呢？」這倒不是說我不知道怎麼做，而是要讓我自己徹底地去理解需求所面對的問題，然後明白自己可以用什麼工具去解決它。</p>
<p>剛好前陣子遇到一個公司內部的需求，讓我有機會重新省視一下自己目前的技能是否足以完成這個需求。</p>
<!-- raw HTML omitted -->
<h2 id="從需求開始">從需求開始</h2>
<p>在我們的產品用戶端要出版前，都會由 SQA 先進行自動化測試，然而這些測試卻無法在伺服器端的 API 佈署完成時自動執行。為了解決這個問題，我們內部在討論過後，決定建立一個佈署監控服務，在 API 專案完成佈署後，自動通知 SQA 的 CI 系統來進行自動化測試。</p>
<p>只是我們公司的網路架構比較特別， SQA 的 CI 環境無法從外界穿透，所以我們也很難拿現成的服務來套用 (例如 SNS) ；加上我們待佈署的機器通常有很多台，無法很容易地在所有機器都佈署完成後得到它們的狀態，需要一些特別的機制來處理。因此這套監控服務就決定由我來實作，也方便針對我們的環境做客製化。由於這牽扯到一些敏感資訊，我只能說這個服務的機制大致上是透過監控上傳到 AWS S3 的佈署狀態，以做到專案佈署與服務通知的解耦。</p>
<p>當然這個方案是因為我個人在有限的知識裡思考出來的，不見得是最佳方案；所以還是要強調一下，**這篇文章並不是探討有什麼好方案，而是要紀錄我在開發這個專案時的歷程。**我相信各位如果有機會跟我身處一樣的狀況時，一定可以想到比我的這個方案更聰明的方式。</p>
<h2 id="所以怎麼開始呢">所以怎麼開始呢？</h2>
<p>首先我們當然是實驗看看這個想法可不可行，免得後面做白工。我跟 SRE 的同事先從規格定義開始，也就是他該上傳什麼格式的資訊到 S3 上來讓我存取；這樣一來 SRE 同事可以先行處理上傳狀態的部份，而我這邊也可以做解析狀態的 PoC 。</p>
<p>在確定這個機制可行後，就可以正式讓它繼續往下走了。接著我就開始思考這個專案還需要什麼進一步的資訊，也就是進入到了設計階段。由於這個服務不限於只用在一個專案上，因此我也把它設計成讓開發者可以加入他們自己的專案；也只有登記在案的專案才能享有這個服務的機制，也方便我在介面上的呈現以及未來對權限上的控管。</p>
<p>而在通知服務的部份， SQA 的同事也提供了他們的 webhook 來讓我串接；只不過如果每次都真的觸發他們的自動化測試的話，他們的困擾也不小。因此在這邊一開始我也不是真的去打他們給的網址，而是先試著觸發一些無傷大雅的測試網址。這時候我就想到，也許這個機制也不見得只能觸發 SQA 的自動化測試，而是可以觸發其他服務的 webhook ，因此我就將它設計成可以設定想要通知的服務，而非直接寫死在程式裡。</p>
<p>大致上需要的資訊都定義好後，我就開始設計 Database Schema 以及開票請託其他部門同事協助建立一些網站必要的基礎建設。</p>
<h2 id="來個管理介面吧">來個管理介面吧</h2>
<p>既然是監控佈署狀態，所以就需要有個地方讓我們看到目前佈署的狀態。這裡我決定用前後端分離的方式來製作這個專案，後端的部份當然毫無疑問地是選用我個人熟悉的 <a href="https://laravel.com/">Laravel</a> 來做為 API 的基礎，而在前端的部份我則是選用了 <a href="https://vuejs.org/">Vue.js</a> 做後台介面。</p>
<p>在 API 的部份主要是設計給前端 UI 存取後端資源，以及給 CI 佈署指令做 hook 使用；基本上 API 部份用到的技術大致上是：</p>
<ul>
<li><a href="https://laravel.com/docs/5.7/routing">Laravel Routing</a> - 用在 RESTful API 上</li>
<li><a href="https://laravel.com/docs/5.7/validation">Laravel Validation</a> - 用在 POST 資料的驗證</li>
<li><a href="https://laravel.com/docs/5.7/eloquent">Laravel Eloquent ORM</a> - 建立 Service 層來隔離 Model 的操作</li>
</ul>
<p>而前端 UI 則是包含了監控用的 dashboard ，以及專案與服務的設定功能。這裡我用到了以下的套件和技術：</p>
<ul>
<li><a href="https://element.eleme.io/">Element</a></li>
<li><a href="https://vuex.vuejs.org/">Vuex</a></li>
<li><a href="https://router.vuejs.org/">Vue Router</a></li>
<li><a href="https://github.com/axios/axios">Axois</a></li>
<li><a href="https://laravel.com/docs/5.7/mix">Laravel Mix</a></li>
</ul>
<p>這些應該都是 Vue.js 及 Laravel 在前端開發時很常見的工具了，所以這邊我也不多提。我在前端上花比較多心力的部份是如何讓後端的狀態可以即時反應到前端 UI 來，這部份就用到了以下的技術：</p>
<ul>
<li><a href="https://laravel.com/docs/5.7/events">Laravel Events</a></li>
<li><a href="https://laravel.com/docs/5.7/broadcasting">Laravel Broadcasting</a>
<ul>
<li><a href="https://github.com/happyDemon/vue-echo">Vue-Echo</a></li>
<li><a href="https://github.com/tlaverdure/laravel-echo-server">Laravel Echo Server</a> (以 <a href="http://pm2.keymetrics.io/">PM2</a> 啟動)</li>
</ul>
</li>
</ul>
<p>接下來就是把這些基本要素組合起來，它們的運作方式是這樣子的：</p>
<p><img src="/resources/how-i-built-my-side-project/laravel-echo-flow.png" alt=""></p>
<p>最後的成果就像這樣：</p>
<p><img src="/resources/how-i-built-my-side-project/ui-v1.png" alt=""></p>
<p>註：請不要吐槽我的命名，命名一直是我的弱點 Orz</p>
<h2 id="再稍微聊聊主要的核心功能">再稍微聊聊主要的核心功能</h2>
<p>這個專案的主要核心功能是這樣子的：當 CI 執行完佈署指令後，會通知監控程式在 S3 上追蹤各台主機的佈署狀態。這邊採用的技術有：</p>
<ul>
<li><a href="https://laravel.com/docs/5.7/queues">Laravel Queues</a></li>
<li><a href="https://laravel.com/docs/5.7/scheduling">Laravel Task Scheduling</a></li>
<li><a href="https://laravel.com/docs/5.7/filesystem">Laravel Filesystem</a></li>
</ul>
<p>我一直覺得 Laravel 在抽象化這部份做得實在太好了，像是拜 Laravel Filesystem 所賜，我可以把存取 storage 這部份抽象化後先在本地端測試，到時候想切換成 S3 的話只要改個設定就搞定了。</p>
<p>這部份的功能其實在 PoC 時就做得差不多了，但後來多加的一些判斷式讓我覺得它可以用 <a href="https://www.sitepoint.com/introduction-to-chain-of-responsibility/">Chain of Responsibility</a> 這個模式來重寫它，大概像這樣子：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">AbstractStep::registerSteps([
    resolve(UpdateHostDeployStatuses::class),
    resolve(CheckDeployStatusTimeout::class),
    resolve(CheckHostDeployStatusCompleted::class),
    resolve(NotifyAllServices::class),
])-&gt;handle($this-&gt;deployStatus);
</code></pre></div><p>註：概念實作可以參考我的 <a href="https://gist.github.com/jaceju/a7267159ffd3b1c1785e40d28ff4e5b3">gist</a> 。</p>
<p>這麼一來我的主要程式邏輯的架構就非常清晰，任何時候想要調整流程都非常容易，在後來要新增需求時證明這個設計是非常好的。</p>
<p>這裡要補充一下同事問我的問題：「這個設計看起來跟 Laravel Pipeline 很像，為什麼你不用它呢？」好問題，第一是 CoR 模式實現上也不困難，而且我保有自訂的彈性；第二我其實根本忘了有 Laravel Pipeline 這個機制 (畢竟沒有正式文件) 。但後來複習了一下 Laravel Pipeline 後，還是覺得就這個需求來說，我自己寫的比較好懂。</p>
<h2 id="沒測試我不會寫程式">沒測試我不會寫程式</h2>
<p>雖然擺到現在才講，但其實我的開發過程大致上都會以 TDD 和 BDD 的方式進行。而在這個專案的測試裡，我做了以下的部份：</p>
<ul>
<li>API schema 驗證</li>
<li>類別的單元測試</li>
<li>以實例來驗證規格</li>
</ul>
<p>先來聊聊什麼是「 API schema 驗證」呢？換句話說就是你定義好了一個 RESTful API 的 schema 後，你預期 API 程式的輸出應該要符合這份 schema 。由於我是用 <a href="https://apiblueprint.org/">API-Blueprint</a> 在制訂 API schema ，它可以很方便地將這些 schema 以 <a href="https://github.com/apiaryio/drafter">Drafter</a> 轉換成 JSON 格式，再配合 Laravel 的 <a href="https://laravel.com/docs/5.7/http-tests">HTTP Tests</a> 以及我自己開發的 <a href="https://github.com/goez-tools/apib-unit">Unit Test Helper for API-Blueprint</a> ，就可以在測試裡自動驗證我的 API 程式輸出是否符合這些 schema 了。</p>
<p>至於類別的單元測試，我主要用在一些輔助用類別的測試，因為它們通常不會牽扯到資料庫或外部服務，很單純的就是一些計算邏輯；而這邊採用的就是 Laraval 的 <a href="https://laravel.com/docs/5.7/testing">Testing</a> 機制，所以就不多提了。</p>
<p>最後我個人最有成就感的就是以實例來驗證規格這部份了，因為它就是真真正正的「活文件」。先來看看例子：</p>
<p><img src="/resources/how-i-built-my-side-project/live-document.png" alt=""></p>
<p>是不是看起來很像規格文件，重要的是它的每個場景都是可以用程式自動去驗證的；這邊就是用 <a href="http://behat.org/">Behat</a> 這個 BDD 工具來實作的，細節請參考拙作：</p>
<ul>
<li><a href="https://jaceju.net/behat-in-laravel/">在 Laravel 中使用 Behat 來加強測試的可讀性 - 基礎篇</a></li>
<li><a href="https://jaceju.net/behat-in-laravel-advance/">在 Laravel 中使用 Behat 來加強測試的可讀性 - 進階篇</a></li>
</ul>
<h2 id="上線後才是挑戰">上線後才是挑戰</h2>
<p>做到這邊，我就讓這個系統上線試用了，不然我也不知道它到底有沒有什麼我沒想到的問題。果然在上線不久，就陸陸續續地收到了一些反饋：</p>
<ul>
<li>可否支援舊有的佈署機制？</li>
<li>可否更清楚的知道佈署過程？</li>
<li>可否在佈署前就自動建立行事曆活動？</li>
</ul>
<h3 id="如何支援舊有佈署機制">如何支援舊有佈署機制？</h3>
<p>在我們公司還是有一些舊專案在佈署後也需要觸發 SQA 的自動化測試，只是它們並沒有透過 CI 來做佈署，而是透過手動輸入指令來佈署；因此我的監控機制就必須提供一些方法來讓這些舊專案也支援。雖然我一開始也幫這類型專案做了跳過監控機制而直接通知 SQA CI 服務的設計，然而後來我們還是重新討論一些改善的做法，因此這部份也還在進行中。</p>
<h3 id="如何更清楚的知道佈署過程">如何更清楚的知道佈署過程？</h3>
<p>在第一版的介面其實也只能看到佈署階段而已，無法瞭解每個階段的細節。所以我重新設計了後端事件，並做了事件的紀錄，然後把這些資訊在前端用時間軸的方式來呈現。而在重新設計介面時，我改用了以下的技術：</p>
<ul>
<li><a href="https://cli.vuejs.org/guide/">Vue-CLI 3</a></li>
<li><a href="https://github.com/vueComponent/ant-design-vue">Ant-Design-Vue</a></li>
</ul>
<p>Vue-CLI 整合到 Laravel 的方式可以參考 Vue 老爸尤雨溪寫的 <a href="https://github.com/yyx990803/laravel-vue-cli-3">Using Vue CLI 3 with Laravel</a> 一例。</p>
<p>選用 Ant-Design-Vue 主要是它有很棒的 step 元件與 timeline 元件，剛好滿足我的需求。只是就在我把新介面做完快上線時，剛好遇到了 <a href="https://github.com/ant-design/ant-design/issues/13848">Ant-Design 聖誕節彩蛋 (炸彈) 事件</a> ，差點沒讓我從椅子上跌下來；當下也只能自嘲還好這只是個 side project ，大家對它沒這麼敏感。</p>
<p>總之改善後的介面像這樣子：</p>
<p><img src="/resources/how-i-built-my-side-project/ui-v2-1.png" alt=""></p>
<p>而且佈署細節也一目瞭然：</p>
<p><img src="/resources/how-i-built-my-side-project/ui-v2-2.png" alt=""></p>
<h3 id="如何在佈署前就自動建立行事曆活動">如何在佈署前就自動建立行事曆活動</h3>
<p>因為公司政策關係，我們內部在將主要專案佈署上線或調整線上設定時，通常需要建立一個 Goolge 行事曆活動，讓所有團隊對這些重要事件能一目瞭然。不過因為手動建立太麻煩了，所以我的部門主管一直很想把這段自動化，也就是在建立 Release Merge Request 時會自動建立活動，在專案佈署上線後將活動標示為結束。</p>
<p>在我們討論之後，我發現其實可以透過我的監控服務來完成這個自動化機制。我在監控服務上提供了一個 API ，讓 GitLab 可以在建立或更新 Merge Request 時透過 webhook 觸發；然後再透過 <a href="https://cloud.google.com/">Google Cloud Platform</a> 提供的 Calendar API 來建立行事曆活動，在 Laravel 這裡有一個很方便的套件 <a href="https://github.com/spatie/laravel-google-calendar"><code>spatie/laravel-google-calendar</code></a> 可以幫我處理 Calendar API 上的串接。</p>
<h2 id="為未來的你著想寫點文件吧">為未來的你著想，寫點文件吧</h2>
<p>近年來我一直很著重寫文件這件事，為的就是希望之後接手維護的人可以省點心，不必一開始就只能跳進程式裡去挖規格。雖然在 BDD 幫助下可以理解這個系統能做什麼，但有些非規格的資訊還是需要額外寫文件說明一下。在這個專案我也不想放棄這個堅持，所以我寫的文件包含這些資訊：</p>
<ul>
<li>簡介與使用方式</li>
<li>如何整合 Google Calendar</li>
<li>正式環境環境設置</li>
<li>開發指南</li>
</ul>
<p><img src="/resources/how-i-built-my-side-project/doc.png" alt=""></p>
<p>這邊就用到了 <a href="https://github.com/GitbookIO/gitbook-cli">GitBook CLI</a> 來幫我產生文件，並且放到 GitLab Pages 上。</p>
<p>另外也因為我是用 <a href="https://apiblueprint.org/">API-Blueprint</a> 來撰寫 API 規格，所以 API 文件就可以透過 <a href="https://github.com/danielgtaylor/aglio">Aglio</a> 這個工具來產生。成果就像這樣：</p>
<p><img src="/resources/how-i-built-my-side-project/api.png" alt=""></p>
<p>當然文件的產生也是自動化的，這部份就是透過 GitLab CI 搭配以 Docker 執行的 runner 來產生。</p>
<p>註：更正一下，我們不是用 <a href="https://apiary.io/">Apiary</a> 這個工具，雖然它也不錯用。</p>
<h2 id="結論">結論</h2>
<p>感謝大家跟我一起很快地走過這個專案的建立過程，雖然它斷斷續續花了我快半年的時間。老實說我的記憶力也不是很強，像是在開發這個專案時，其實有很多技術我也記不清全貌，大多數細節還是要邊做邊查手冊；所以趁著現在記憶還鮮明時，趕緊把這個過程記錄下來，免得以後可能要寫書時就忘了自己曾經做過什麼了 (想太多) 。</p>
<p>而且各位大概可以發現其實我在這個專案上用的技術都很平常，基本上都是目前網站開發者都懂的東西。事實上就我個人的經驗，大多數專案的需求其實用不到什麼高深的技術，目前主流的開發工具或框架幾乎都可以滿足這些需求。所以不要看輕那些看起來不潮的工具，真正該磨練與精進的是其實是你自己如何去分析並解決問題的能力。</p>
<p>再總結一下整個歷程：</p>
<ol>
<li>確認需求真正到底要的是什麼。</li>
<li>討論並提出有共識的方案。</li>
<li>實驗並確認方案是可行的。</li>
<li>開發初期版本並上線運行。</li>
<li>收集反饋並持續改善。</li>
</ol>
<p>最後我想說這個專案對我的意義是很大的，因為它解決了團隊的痛點，讓團隊可以不必分心在一些雜務上；所以我認為所謂的成就感，有時不見得是學會多新穎的技術，而是做出對別人來說是有意義的事情。</p>
]]></content>
		</item>
		
		<item>
			<title>Laravel 執行測試時出現 Function name must be a string</title>
			<link>https://jaceju.net/function-name-must-be-a-string-in-laravel-testing/</link>
			<pubDate>Mon, 28 Jan 2019 12:26:42 +0800</pubDate>
			
			<guid>https://jaceju.net/function-name-must-be-a-string-in-laravel-testing/</guid>
			<description>在 Laravel 撰寫單元測試有用到 @dataProvider ，執行測試時卻出現 Function name must be a string 的錯誤。 這是因為所有的 Data Provider 會比 setUp 更早被執行，所以不能在 Data Provider 裡用任何在 setUp 後才會有的東西，</description>
			<content type="html"><![CDATA[<p>在 Laravel 撰寫單元測試有用到 <code>@dataProvider</code> ，執行測試時卻出現 <code>Function name must be a string</code> 的錯誤。</p>
<p>這是因為所有的 Data Provider 會比 <code>setUp</code> 更早被執行，所以不能在 Data Provider 裡用任何在 <code>setUp</code> 後才會有的東西，例如 <code>$this-&gt;app</code> 或 helper function ，因為這時候它們還沒有被初始化或被 autoload 載入。</p>
]]></content>
		</item>
		
		<item>
			<title>身為前端工程師，對你來說，你認為最重要的是什麼？</title>
			<link>https://jaceju.net/what-is-essential-to-frontend-engineers/</link>
			<pubDate>Tue, 15 Jan 2019 00:06:19 +0800</pubDate>
			
			<guid>https://jaceju.net/what-is-essential-to-frontend-engineers/</guid>
			<description>好友 Kuro 公開問了這個問題，嘗試挑戰一下。不過主要也只是整理了一下我從身邊的前端同事及社群朋友們上看到的一些特質，畢竟比起我來，他們在前端領域打</description>
			<content type="html"><![CDATA[<p>好友 Kuro 公開問了<a href="https://www.facebook.com/kurotanshi/posts/10210440012635051">這個問題</a>，嘗試挑戰一下。不過主要也只是整理了一下我從身邊的前端同事及社群朋友們上看到的一些特質，畢竟比起我來，他們在前端領域打滾得更久。當然這些特質應該是適用大部份工程師 (不論哪一端) ，但我還是認為前端工程師平時要更著重這些特質。</p>
<p>註：其他領域或許也有「前端」這個術語，但一般人認知的「前端」是泛指「 Web 前端」。</p>
<p>這些特質包含：</p>
<ul>
<li>厚實的基礎能力</li>
<li>擅長找出問題點</li>
<li>擁有靈活的思維</li>
<li>時常保持好奇心</li>
</ul>
<!-- raw HTML omitted -->
<h2 id="厚實的基礎能力">厚實的基礎能力</h2>
<p>現代很多剛接觸前端的開發者都誤以為只要學好框架、會套版面、最後發佈網站就是他們全部的工作了；雖然不得不說這是很多公司找人的目的，讓很多自稱前端工程師的朋友也只能在人後默默流淚。</p>
<p>但如果想讓自己成為一個優秀的前端開發者，不能只做這些宛如打雜的表面工作，而是要深深地打好基礎。基本上，前端大部份的工作成果都得靠瀏覽器呈現，而其中要理解的基礎大致上可以分成幾個重點：</p>
<ul>
<li>瀏覽器要呈現的內容從哪來？</li>
<li>瀏覽器背後到底做了些什麼？</li>
<li>怎麼讓瀏覽器正確呈現內容？</li>
</ul>
<p>剛好有一篇<a href="https://cythilya.github.io/2018/11/26/what-happens-when-you-type-an-url-in-the-browser-and-press-enter/">《在瀏覽器輸入網址並送出後，到底發生了什麼事？》</a>把這些要點介紹得很清楚，而文章中的各個環節就是前端開發者所該具備的基礎。</p>
<p>那該怎麼開始打好這些基礎呢？你可以在 Google 上搜尋一下「 2018 前端技能樹」，大概會蹦出不少有用的參考資源，以下羅列幾個：</p>
<ul>
<li><a href="https://blog.fundebug.com/2018/09/04/2018-frontend-roadmap/">2018 前端工程師成長路線圖 (譯文)</a> <a href="https://medium.com/tech-tajawal/modern-frontend-developer-in-2018-4c2072fa2b9c">(原文)</a></li>
<li><a href="https://sherlock.phodal.com/">Skill Tree Sherlock</a></li>
<li><a href="https://leohxj.gitbooks.io/front-end-database/interview/skill-path.html">前端技能圖譜</a> (這本電子書裡面還有很多寶可以挖。)</li>
</ul>
<p>註：你可能會問為什麼不是找 2019 年的技能樹？拜託，現在才一月耶，去年的繼續用就好。</p>
<p>當然並不是說上面列的全部技能都是基礎，但至少瀏覽器相關及網頁三巨頭 HTML/CSS/JavaScript 背後的知識都是前端工程師日常工作中必備的基礎技能。有句話我記得好像是這麼說的：</p>
<blockquote>
<p>大師只是把基礎功練得比別人更極致而已。</p>
</blockquote>
<p>所以厚實的基礎能力是前端工程師很重要的特質，有些前端工程師基礎好到只要是可以用 JavaScript 做出來放到瀏覽器上跑的東西，他們都會去做，更何況這世界上每個東西看起來都像 JavaScript 。</p>
<h2 id="擅長找出問題點">擅長找出問題點</h2>
<p>大多數前端工程師平常不只是在開發新功能而已，他們還需要面臨使用者在瀏覽器操作上的各種問題，還有開發工具帶來的各種問題，以及為了解決問題所開發的工具所帶來的新問題。有厚實的基礎能力還有 <a href="https://stackoverflow.com/">Stack Overflow</a> 作為後盾，他們就能在看到這些問題時，可以很快分析出問題點。</p>
<p>所以知道如何用瀏覽器提供的開發者工具來偵錯或是查看效能瓶頸，熟悉各種瀏覽器之間不一致的實作等等，這些都是前端開發者在尋找問題點時很重要的技能。今天如果是我面試前端工程師時，我第一個問題大概會問：「你知道主流瀏覽器開啟開發者工具的快速鍵嗎？」</p>
<h2 id="擁有靈活的思維">擁有靈活的思維</h2>
<p>「技術領域裡不會只有一種解決需求的方式。」這句話的威力在前端開發中特別顯著。前端開發者的強大不在於他們學會多少工具，而是會不會用手邊既有的工具來處理需求或是解決問題。你以為我要說「龍五手上只要有槍，誰都殺不了他。」嗎？對，我已經說了。</p>
<p>在我所知道的前端開發者裡，有一個思維靈活到極致的男人叫 Martin Kleppe ，他常用的帳號是 <a href="https://twitter.com/aemkei">@aemkei</a> 。他對 JavaScript 的<del>惡搞</del>研究很難有人可以相提並論，我曾經在 JSConf 裡看過他的演講，那時我才知道自己在 JavaScript 上思維有多僵硬，大概跟我的肝一樣硬。</p>
<p>來看看他的簡報和議程錄影：</p>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<h2 id="時常保持好奇心">時常保持好奇心</h2>
<p>就跟貓一樣，你大概很難阻止前端開發者的好奇心。常會看到前端同事或社群朋友在網路上逛一逛後就撿到一些有趣的 JavaScript 或 CSS 寫法，接著就會分享出來討論它們是用了哪些技巧。然而好奇心會殺死貓，也會殺死前端工程師；他們除了喜歡去探究那些新奇的玩意是怎麼做到之外，也可能會在看到一個效果後，就日以繼夜，自己從頭摸索來做出同樣的效果。</p>
<p>當然他們也不會僅滿足於做到別人已經做出的成果，還會自己去研究 W3C 制定的規格或是其它技術的理論，進一步打造出前所未見、令人驚豔的成品；所以你可以說好奇心就是前端工程師進步的原動力，否則到今天我們可能還是在「首頁製作百寶箱」裡找程式碼複製貼上。不過現在有進步一點，大多數前端工程師都學會在 <a href="https://codepen.io/">CodePen</a> 或是 <a href="https://jsbin.com/">JS Bin</a> 找程式碼了。</p>
<h2 id="結論">結論</h2>
<p>好啦，我還是要掛個免戰牌 (你看我連評論都不給你寫了) ，希望各位別認為這些特質就是全部或是絕對；更好的方式是直接去觀察各位身邊的優秀前端工程師，看看他們平時在玩些什麼遊戲，不對，是有什麼樣的特質才會如此受人尊敬。</p>
]]></content>
		</item>
		
		<item>
			<title>我的 2018 年</title>
			<link>https://jaceju.net/my-2018/</link>
			<pubDate>Wed, 02 Jan 2019 01:01:30 +0800</pubDate>
			
			<guid>https://jaceju.net/my-2018/</guid>
			<description>又過了一年了， 2018 年只在這個部落格零零落落地加了幾篇文章，感覺自己似乎沒什麼長進。最近寫作的機會大多是在專案的文件上，對於技術知識的研究與自己</description>
			<content type="html"><![CDATA[<p>又過了一年了， 2018 年只在這個部落格零零落落地加了幾篇文章，感覺自己似乎沒什麼長進。最近寫作的機會大多是在專案的文件上，對於技術知識的研究與自己內心的探討變得越來越少，所以應該是來寫篇心得感想的時候了。</p>
<p>老實說原本打算寫篇年底回顧，不過整理了一下手邊的隨手筆記和平常記在 Facebook 上的資訊後，發現想講的東西太多了，就隨性想到什麼寫什麼吧。</p>
<!-- raw HTML omitted -->
<h2 id="2018-年做的事">2018 年做的事</h2>
<p>先粗略地記一下我在 2018 年做的事情：</p>
<ul>
<li>花了一年跟功能團隊完成了一起聽專案的新功能。</li>
<li>將原有排程程式改成在 Docker 上運行的 Daemon 服務，以求更即時的反應。</li>
<li>開發讓 Laravel 專案能跟公司設定檔系統更易接合的套件。</li>
<li>建立內部服務串接系統，增強佈署後自動化測試的機制。</li>
<li>改良了數個內部用獨立套件的架構，使其更易擴充。</li>
<li>為大部份專案補齊缺少的文件。</li>
<li>舉辦了程式碼研習會，學習一些值得借鑑的程式架構或寫法。</li>
</ul>
<p>不過如果要把 2018 年的事做個簡單的文字總結，那應該就是「改善」。而「改善」不光只在技術層面的變化，同時也有持續維運的改良，以及思維觀念的調整。</p>
<p>我們團隊經常在微調程式架構，目的就是希望能因應不斷變化的需求，讓程式可易於維護。這些調整不僅只是應用程式層級，也包含了底下的輔助用套件。我們會花一些時間去討論怎麼設計架構才會好維護、程式介面才會好用；最重要的，是要讓團隊理解每個設計會有什麼優缺點，而不會止於「會動就好」的心態上。雖然前期投資了很多時間，但事實證明這些決定都幫我們節省更多時間。</p>
<p>除了自己手邊的專案外，跟其他部門合作上的改善也是重點。為了讓我們的專案佈署上線後，能夠驅動後續讓 SQA 能進行自動化驗證的步驟，我也做了個簡單的內部服務串接系統。這個系統活用了許多 Laravel 提供的機制，也讓我重新複習與學習不少前端開發的技巧。由於 BDD 目前已經內化成我開發時的流程了，所以這個專案在後端程式的部份我也是用 BDD 開發；但是 BDD 工具的部份我一直不太滿意；在花了一些時間研究後，終於被我找到還不錯的方法。</p>
<ul>
<li><a href="https://jaceju.net/behat-in-laravel/">在 Laravel 中使用 Behat 來加強測試的可讀性 - 基礎篇</a></li>
<li><a href="https://jaceju.net/behat-in-laravel-advance/">在 Laravel 中使用 Behat 來加強測試的可讀性 - 進階篇</a></li>
</ul>
<p>說真的，不管哪一項「改善」都很花時間，只是我們都是抓緊一些空餘時間來做改善；我們不求一步到位，而是一點一點地累積；因為今天你不做，明天你就會後悔。</p>
<h2 id="那些對程式開發上的體悟">那些對程式開發上的體悟</h2>
<p>2018 年我對程式開發有一些新的體悟，雖然大部份都是軟體界很常聽到的觀念，但自己在實務上體會之後又是另一種不一樣的感受。</p>
<h3 id="架構">架構</h3>
<p>我常常問自己：除了框架幫你定義好的架構外，你有辦法設計出能適應業務邏輯變化的架構嗎？</p>
<p>架構的彈性不是保留下來的，是隨著需求調整出來的。如果不時常從需求的源頭審視架構的設計，而一直頭痛醫頭、腳痛醫腳地 workaround 下去，軟體的腐敗將是可預期的。另外系統架構本身也跟程式架構有密切關係，很多時候 SRE 採用的方案也會間接地影響到我們怎麼去設計程式的架構。所以該擔心的不是需求與底層系統的改變，而是架構的模組間沒有做好隔離所導致的抽換困難；簡單來說，如果無法從某個點切開後，調整其中一邊而不影響整個服務，那就是有問題的架構了。</p>
<p>所以身為設計架構的人，必須學會從需求中理出一個易懂的規則，進而調整出易於實作與擴展的架構；而不是看到一個新需求，就在原先的架構樹中長出一個雜亂的分支。專案的起頭沒有將架構設計好，未來改架構所要花的工真的很大；尤其當同事已經照著這個架構寫出不少新功能的時候，得重頭理解同事的程式碼來重構它們。</p>
<h3 id="測試">測試</h3>
<p>不得不說我們團隊已經把自動化測試 (大多數是後端) 列入標準開發流程了，至於是不是用制式的 TDD 流程來開發並不是那麼要求，重點在於我們寫的測試有沒有驗證到需求所要的結果。每次幫伙伴 code review 時，只要看他的 merge request 描述與測試是否符合，大致上就能確定該情境的程式碼邏輯對不對。</p>
<p>當然測試也不是萬靈丹，總是會有些情境你沒考慮到。有一次我發現某個主專案在更新了某個內部用套件的新版後，意外的導致程式反應速度變慢；一開始以為是系統效能不彰，後來追 code 追了兩個多小時才找到 bug 所在。這個套件有新舊兩個類別，舊類別是針對 legacy project 寫的，新類別是針對新專案寫的。而針對舊環境寫的類別，測試過，完美；針對新環境寫的類別，也測試過，完美。但問題就出在主專案在把套件裡的兩個新舊類別混在一起用，結果發生衝突。所以我得到一個心得：就算單元測試通過，也永遠不要預設別人會怎麼用你寫的程式。</p>
<h3 id="bug">Bug</h3>
<p>在一起聽新功能上線的這陣子，我們很常遇到一些只有在上線環境才會遇到的 bug ，這時候才體會到什麼是：「 Log 到 debug 時方恨少」。像 2018 年最後一個上班日，我們就從 log 中找到某個 bug 的成因；伙伴也重新 review 程式碼裡相關的邏輯，終於在放假前把 bug 給解決了。</p>
<p>Bug 跟測試也是息息相關，我們在解一些只能在上線環境重現的 bug 時，會講求以下原則：</p>
<blockquote>
<p>先求不傷身體 (不破壞原有功能) ，再講求藥效 (真的有解決問題) 。</p>
</blockquote>
<p>也就是無論如何，我們所做的任何修正都不可以讓原有的測試失敗；換句話說，原有的功能不可以沒有測試！沒寫測試的程式 debug 起來就像《醉後大丈夫》中的劇情：隔天來上班的你只能從 log 裡的蛛絲馬跡中去一一回想你昨晚下班前到底做了什麼才導致這個 bug ，因為原有的程式碼邏輯已經不是你預期中的那樣了。</p>
<p>還有就是別幹蠢事。有次 QA 反應了一個 bug ，聽起來是後端這邊的問題；所以 PM 就問我們怎麼回事，我就想這上次不是修正了嗎？結果一查，有人幹了蠢事了 (就是我) 。後來開會時我把修正的方式告知了 PM 後：</p>
<p>「聽起來修正後的邏輯應該沒問題呀？」</p>
<p>「嗯，問題是它一直躺在測試環境沒上線。」</p>
<h3 id="文件">文件</h3>
<p>2018 年也不時地在還技術債，其中一項就是文件。其實在寫文件的過程中，同時也是在檢討自己過往那些不成熟的思維。當初可能是因為專案趕時間，可能是因為邏輯不夠嚴謹；可能是因為想說口頭交接一下就好，可能是因為當初沒想過這玩意會變成大家都要用的東西。但無論如何，如果不能留點東西來讓別人瞭解自己做的是什麼，那你的成果到後面就變得跟垃圾沒什麼兩樣了。</p>
<p>只要是一家公司，人都會來來去去，在公司裡任何活著的程式不可能只靠一個人來延續它的生命。程式開發本來就不是我做一半你就能馬上接手繼續的事，其中的脈絡如果前人沒有保留下來 (註解、文件、測試) ，那麼我們就得花時間從頭看程式碼；而且就算看完所有程式碼，也不能保證就能理解當初為什麼要這樣寫。</p>
<p>永遠不要相信上星期五的自己有留下完美但未完成的程式碼給星期一的自己。程式之神可以保證當你過了個快樂的週末後，一定會忘了你上個星期寫了什麼玩意。所以就算只是寫給自己看的註解，也要記得寫下當初為什麼要寫這些程式碼。</p>
<p>如果有寫時間寫文件，那麼文件到底要寫些什麼呢？可以回想一下你從頭打造這個專案時的歷程，有哪些該注意的地方，有哪些該連絡的人；這些摸索的路徑，這就會是你新專案的文件大綱。然後調整心境，抱持著以往自己接手前人專案時的心情來寫，才不會覺得寫文件是件浪費時間的事，因為我們可以幫未來的自己活得更輕鬆。</p>
<p>那真的沒時間寫文件怎麼辦？而且這時候 TDD 就能派上用場了，那些針對情境所寫的測試案例，正好可以協助我們回憶這些程式碼的邏輯。就算用沒時間這個藉口不寫文件，測試案例多少也能算是我們在這個專案上對文件的掙扎。</p>
<p>今天的你，也許可以幫三天後的自己或接手你程式的人多做點什麼。</p>
<h2 id="部門的伙伴們">部門的伙伴們</h2>
<p>必須說，我一直很慶幸我身處的部門是一個很棒的技術團隊；雖然我在部門裡扮演的角色是代理 leader ，但現在我們小隊成員已經可以接到任務後自動動起來了，該做什麼都很清楚，不需要我介入太多；而且遇到問題會自動自發地找大家一起討論來一起想辦法解決，而不再是埋頭自幹。事實上，我一直覺得我們部門的伙伴們有幾個不錯的特質。</p>
<h3 id="理解自己打算要做的">理解自己打算要做的</h3>
<p>每次收到需求，我們不會太早跳入細節，而是先仔細討論它的合理性以及影響範圍；多數時候我們可以花比較少的力氣去完成需求，而不會因為需求的不確定性而改來改去 (當然還是有機會發生修改) 。</p>
<p>像我的伙伴常會先把他對需求的理解做個整理，然後附上他認為可行的解決方案，最後請整個技術部門一起審視與討論。我的伙伴們就是可以這麼用心看待每項交予他們的任務，這就是這個團隊讓我很安心的原因。</p>
<h3 id="持續改善已經做過的">持續改善已經做過的</h3>
<p>我的伙伴們很棒的一點就是不會滿足於現在能動的程式碼，他們會在測試的保障中去尋找更易維護的方法來改善程式碼。改善的時機不限於程式上線，也會在開發中、 code review 時找時間做。</p>
<p>當遇到需求與現在架構衝突的狀況時，他們就會立刻思考並討論現在架構的缺點，進而提出改善的方案。接著就會跟 TPM 商議並安排時間來進行，完全沒有「這邊 workaround 就好」的想法。</p>
<h3 id="不侷限在需求想做的">不侷限在需求想做的</h3>
<p>我們有時會做一些協助開發的小工具來讓團隊使用，而不是需求要什麼我們就只做什麼。乍看之下我們花了很多時間，但整體來說團隊會節省許多時間。</p>
<p>像是在做一起聽新功能時，我原本打算自幹一個提供給 client team 測試用的小工具；沒想到前端同事主動幫忙，才一下午就做出超乎我想像的介面了，而且這還包括了他研究了某個 UI framework 的時間！</p>
<h3 id="勇於挑戰從沒做過的">勇於挑戰從沒做過的</h3>
<p>曾經有個需求要用到一個我們不熟悉的技術，但我的伙伴們從研究、討論、製訂規格到實作完成並上線，前後只花了兩個星期的時間，而且品質也好到沒話說，由此就能知道他們真的很用心在面對自己的挑戰；這背後不論是他們堅強的實力或是敢於冒險的心態，都再再呈現出這是個能獨當一面的團隊，叫我怎能不愛他們呢？‬</p>
<p>‪然而我也不是把工作一股腦塞給伙伴，以下都是我一直嘗試去做的方向：</p>
<ol>
<li>要先瞭解伙伴們是否有興趣與時間研究更深入的技術。</li>
<li>交接不藏私，任何自己想過的，研究過的都儘可能交代。</li>
<li>交接時也可聽取伙伴們的想法，找出自己的盲點。</li>
<li>伙伴想怎麼做由他，但要讓伙伴瞭解優先序。</li>
<li>相信伙伴能做好，少干預。</li>
</ol>
<p>其實凝聚伙伴的向心力遠比學習技術還要難，當你身處一個優秀的團隊後，就完全不會想離開它了；這大概是我進 KKBOX 之後，最大的收穫之一吧。</p>
<h2 id="一個真正的團隊">一個真正的團隊</h2>
<p>每次看到同事們在討論技術或是解決服務上的問題時，就會深深覺得自己真的只是個能力有限的小螺絲；如果沒有身邊及其他部門同事們的幫助，而是像以前一樣自己一個人從軟體架構設計、前後端程式開發到底層服務建置一條龍做完，所要花費的時間和精力不知道有多少。</p>
<p>2018 年花了一些時間記錄了一些跟團隊有關的點點滴滴。</p>
<h3 id="讓團隊維持熱情">讓團隊維持熱情</h3>
<p>有時工程師提出功能逐步上線的建議卻很少被採納，常常是做完一大包後才上線；這不僅僅是曠日費時，而且常常會因為市場的變化使得這些努力白費。這樣一來只會打擊整個團隊的士氣，讓團隊漸漸對產品失去熱情。</p>
<p>某同事曾經說過：「我們不是沒有 Agile 精神，我們是沒有精神。」</p>
<p>其實不管有沒有 Agile ，團隊需要的是對於工作的熱情，對於產品的熱情。而這樣的熱情，必須從上而下地傳達，讓團隊成員明白自己的努力成果是有機會被使用者看見的。</p>
<blockquote>
<p>要形成優秀的產品，別只做做敏捷的形，卻沒有貫徹敏捷的意。</p>
</blockquote>
<p>那麼如果團隊有的是熱情，卻不知道該怎麼做時怎麼辦？就做給他們看呀，願意成長的人就會做得比你更好。在團隊裡我不是最厲害的一個，但我會儘可能把基礎建設做好，讓大家知道怎麼去打好根基，並設計出更棒的架構。或許我們可以用高額的薪資吸引到優秀的人材，但卻不見得能創造出優秀的團隊。只有信任及以身作則才能讓團隊成員願意更積極地去展現自己的自主性，因為他們知道自己的背後有其他伙伴可以信賴！</p>
<h3 id="看得見的會議共識">看得見的會議共識</h3>
<p>偶爾在開會時，會看到有人講完簡報後，就要大家開始討論；但老實說這種會議很難達成共識，而一個沒有共識的會議只是浪費大家的時間。那麼該怎麼讓大家有共識？方向很簡單：創造讓大家能提出自己看法的機會，同時也讓每個人都看到彼此的看法。</p>
<p>曾經在年初組織的功能團隊裡，遇到一個技術問題需要討論；這時我試著利用白板加上文件的投影，讓大家對實作細節、問題釋疑還有規格的確認都有了共識，結果這個方法讓整個團隊在一個小時會議中搞定了三個問題。事後在檢討會議上，團隊裡負責實作 client 的成員也對這個方法非常讚賞。</p>
<p>我個人用的方式並沒有參考哪一種方法論，我只是思考了一下怎麼在白板上傳達大家的看法比較合適。但我個人還是建議學習一些相關的知識，例如「看板方法」或是參考像是《引導者的工具箱》這類書籍裡的技巧；當然，也要學著活用這些技巧，而不是照本宣科地去做。</p>
<h3 id="面對變化">面對變化</h3>
<p>提起軟體界的需求改變，我想大概可以回想一下二師兄所講過的一些話。</p>
<p>前天他說：「從數據來看，我們的產品畫面應該要針對個別使用者做客製化！」</p>
<p>昨天他說：「我希望每個畫面的呈現都應該統一，這樣才有一致感。」</p>
<p>今天他說：「不要管數據說什麼，我們應該主動找出最好的使用體驗提供給使用者！」</p>
<p>剛剛他說：「別管什麼使用者體驗了！趕快想辦法給我衝註冊會員數！」</p>
<p>有時候需求的改變不是來自於使用者，而是人在無助或想不出好方法時產生的奇怪念頭，當然這些不論公司大小都有機會發生，而身為團隊的一分子，就必須適時地給予建議。</p>
<p>不過人生總是有無奈的時候，既然要吃，那就用自己的方式吃吧，總不能讓自己對於程式開發的熱情也沒了。</p>
<p>第一步我們從程式面下手，我們善用了框架的優點來建立良好的基礎架構，多數需求的變化都可以在很短的時間調整過來。第二步我們不再單方面接受 PM 傳達過來的解決方案，而是直接聆聽需求方的問題困難點；而直接聽完之後所想出來的方案，都遠比原來複雜很多的解決方案來得簡單又符合要求。這同時也給我一個新的體悟：</p>
<blockquote>
<p>別一直覺得要用自己知道的專業來解決問題，要想想有沒有其他專業能夠解決同樣的問題。</p>
</blockquote>
<p>很多時候利益關係人 (stakeholder) 都是三心二意的，或是想到什麼告訴你什麼，所以一定要好好思考他們問題的本質，你提出的方案才能真正解決他們的痛點。不過更麻煩不是每個利益關係人都有同樣的需求，很有機會這個需求是 A 想做的，結果在 B 那邊是會出問題的。所以原則上有個重點要保握：</p>
<blockquote>
<p>‪每個需求都要確認利益關係人買單，‬‪避免實作完成後才發現有人不喜歡。‬</p>
</blockquote>
<p>還有一種情況是 PM 給了超級複雜的規格文件 (PRD, Product Requirements Document) ，你做完條件 A 後，才發現跟條件 B 有衝突。然而要避免這個問題，一個很重要的方法就是請 PM 給實例。</p>
<blockquote>
<p>與其把 PRD 寫得花花綠綠，不如表列出各種情境的實例。</p>
</blockquote>
<p>這麼一來在討論需求實例時就發現 bug 的成本，遠比實作完才發現 bug 的成本低得多；前者可能只需要幾句話解決，後者可能要花幾個人週才能解決。</p>
<h2 id="對自己的期許">對自己的期許</h2>
<p>2018 年其實我的倦怠感有點重，參與社群活動的意願也不高，很多時間都花在調適自己的心情上。會這樣的原因有很多，但我自己認為對產品新功能目標的不認同可能是主因之一。不過跟著一群好伙伴工作，多少沖淡了我被這些負面狀態的影響。</p>
<p>接下來的 2019 年，我期許自己做到這些：</p>
<ul>
<li>調適自己的心態，讓自己能更積極地看待事物。</li>
<li>訓練自己的寫作與講話更有條理。</li>
<li>把買的書看一看，把買的遊戲玩一玩。</li>
<li>不要只問自己問題，還能要講出自己能接受的答案。</li>
<li>學習新的東西，不要只侷限工作上的技術。</li>
</ul>
<p>最後，再給自己一次這段話：</p>
<blockquote>
<p>不要因別人做了什麼所以跟著去做什麼，‬而是要自己想做什麼所以用心去做什麼。</p>
<p>‬‪然後，成功了會有快樂，失敗了也會有體悟。‬</p>
</blockquote>
<p>寫著寫著，年就在我手指敲擊中跨過去了。</p>
<p>新年快樂。</p>
]]></content>
		</item>
		
		<item>
			<title>在 Laravel 中使用 Behat 來加強測試的可讀性 - 進階篇</title>
			<link>https://jaceju.net/behat-in-laravel-advance/</link>
			<pubDate>Fri, 09 Nov 2018 18:38:53 +0800</pubDate>
			
			<guid>https://jaceju.net/behat-in-laravel-advance/</guid>
			<description>在上一篇文章中，我介紹了如何把 Behat 整合到 Laravel 裡；不過後來我發現在專案規格越來越複雜時，把所有 step definitions 都寫在 FeatureContext 類別中變得非常不易閱讀。另一個問題就是我</description>
			<content type="html"><![CDATA[<p>在<a href="https://jaceju.net/2018-11-08-behat-in-laravel/">上一篇文章</a>中，我介紹了如何把 Behat 整合到 Laravel 裡；不過後來我發現在專案規格越來越複雜時，把所有 step definitions 都寫在 <code>FeatureContext</code> 類別中變得非常不易閱讀。另一個問題就是我不想要用 <code>putenv</code> 函式來定義環境變數，而是希望能有 <a href="https://github.com/laracasts/Behat-Laravel-Extension">Behat-Laravel-Extension</a> 把環境變數放在 <code>.env.behat</code> 裡的用法。</p>
<p>而經過不斷地改良後，我找到了一個目前我很滿意的做法；所以接下來我會延續上一篇的範例來介紹新的做法。</p>
<h2 id="改良環境改數的設定方式">改良環境改數的設定方式</h2>
<p>為了讓 Behat 執行時可以讀取 <code>.env.behat</code> ，我們需要在 Application 初始化時載入新的環境設定檔案。因此我們就不能直接用 Laravel 提供的 <code>Tests\CreatesApplication</code> 這個 trait 了，因為它預設會載入 <code>.env</code> 檔；這時這也表示我們不再需要直接繼承 <code>Tests/TestCase</code> 這個類別，因為它也只是使用了 <code>Tests\CreatesApplication</code> 這個 trait 。</p>
<p>取而代之的是我們改為繼承 <code>Illuminate\Foundation\Testing\TestCase</code> 這個類別，然後自行覆寫 <code>createApplication</code> 這個方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Behat\Behat\Context\Context</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Illuminate\Contracts\Console\Kernel</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Illuminate\Foundation\Testing\TestCase</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">FeatureContext</span> <span class="k">extends</span> <span class="nx">TestCase</span> <span class="k">implements</span> <span class="nx">Context</span>
<span class="p">{</span>
    <span class="c1">// ...
</span><span class="c1"></span>
    <span class="k">protected</span> <span class="k">const</span> <span class="no">ENV_FILE</span> <span class="o">=</span> <span class="s1">&#39;.env.behat&#39;</span><span class="p">;</span>

    <span class="sd">/**
</span><span class="sd">     * @return \Illuminate\Foundation\Application
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">createApplication</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nv">$app</span> <span class="o">=</span> <span class="k">require</span> <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/../../bootstrap/app.php&#39;</span><span class="p">;</span>

        <span class="nv">$app</span><span class="o">-&gt;</span><span class="na">loadEnvironmentFrom</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="na">ENV_FILE</span><span class="p">);</span>

        <span class="nv">$app</span><span class="o">-&gt;</span><span class="na">make</span><span class="p">(</span><span class="nx">Kernel</span><span class="o">::</span><span class="na">class</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">bootstrap</span><span class="p">();</span>

        <span class="k">return</span> <span class="nv">$app</span><span class="p">;</span>
    <span class="p">}</span>
</code></pre></div><p>可以看到新的 <code>createApplication</code> 方法主要是在 <code>Kernel::bootstrap</code> 之前，讓 Application 改讀 <code>.env.behat</code> 。</p>
<p>接下加入 <code>.env.behat</code> ，內容可參考 <code>phpunit.xml</code> 裡的 <code>&lt;php&gt;</code> 區段設定，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-ini" data-lang="ini"><span class="na">APP_ENV</span><span class="o">=</span><span class="s">testing</span>
<span class="na">DB_CONNECTION</span><span class="o">=</span><span class="s">sqlite</span>
<span class="na">DB_DATABASE</span><span class="o">=</span><span class="s">:memory:</span>
<span class="na">BROADCAST_DRIVER</span><span class="o">=</span><span class="s">log</span>
<span class="na">CACHE_DRIVER</span><span class="o">=</span><span class="s">array</span>
<span class="na">SESSION_DRIVER</span><span class="o">=</span><span class="s">array</span>
<span class="na">BCRYPT_ROUNDS</span><span class="o">=</span><span class="s">4</span>
<span class="na">QUEUE_DRIVER</span><span class="o">=</span><span class="s">sync</span>
</code></pre></div><h2 id="分類-step-definitions">分類 Step definitions</h2>
<p>當 step definitions 很多時，通通都放在 <code>FeatureContext</code> 類別裡就不是個明智的做法了。所以接下來我依照 step definitions 的類型來建立不同的 Context 檔，這樣維護起來也很方便。</p>
<p>首先我們要在專案根目錄下建立一個 <code>behat.yml</code> 檔， Behat 在執行時會讀取它裡面的設定：</p>
<div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="k">default</span><span class="p">:</span><span class="w">
</span><span class="w">  </span><span class="k">suites</span><span class="p">:</span><span class="w">
</span><span class="w">    </span><span class="k">default</span><span class="p">:</span><span class="w">
</span><span class="w">      </span><span class="k">contexts</span><span class="p">:</span><span class="w">
</span><span class="w">      </span>- ApiFeatureContext<span class="w">
</span><span class="w">      </span>- DatabaseAssertionContext<span class="w">
</span></code></pre></div><p>然後我們執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">vendor/bin/behat --init
</code></pre></div><p>這麼一來 Behat 會幫我們自動產生所有 context 檔案：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">+f features/bootstrap/ApiFeatureContext.php - place your definitions, transformations and hooks here
+f features/bootstrap/DatabaseAssertionContext.php - place your definitions, transformations and hooks here
</code></pre></div><p>註：雖然這裡只針對範例拆分，但你可以加入其它的 context 檔，後面我會給一些例子。</p>
<p>接著編輯每個 context 檔，先讓它們繼承 <code>FeatureContext</code> 類別，並拿掉 <code>__construct</code> 建構子；然後再把原來放在 <code>FeatureContext</code> 類別裡的 step definitions 搬到對應的 context 類別裡。</p>
<p>API 相關的 step definitions 放在 <code>ApiFeatureContext</code> 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Behat\Gherkin\Node\TableNode</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">ApiFeatureContext</span> <span class="k">extends</span> <span class="nx">FeatureContext</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * @var string
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="nv">$apiUrl</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>

    <span class="sd">/**
</span><span class="sd">     * @var array
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="nv">$apiBody</span> <span class="o">=</span> <span class="p">[];</span>

    <span class="sd">/**
</span><span class="sd">     * @var \Illuminate\Foundation\Testing\TestResponse
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="nv">$response</span><span class="p">;</span>

    <span class="sd">/**
</span><span class="sd">     * @Given API 網址為 :apiUrl
</span><span class="sd">     * @param string $apiUrl
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">apiUrl</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$apiUrl</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">apiUrl</span> <span class="o">=</span> <span class="nv">$apiUrl</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="sd">/**
</span><span class="sd">     * @Given API 附帶資料為
</span><span class="sd">     * @param TableNode $table
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">apiBody</span><span class="p">(</span><span class="nx">TableNode</span> <span class="nv">$table</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">apiBody</span> <span class="o">=</span> <span class="nv">$table</span><span class="o">-&gt;</span><span class="na">getHash</span><span class="p">()[</span><span class="mi">0</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="sd">/**
</span><span class="sd">     * @When 以 :method 方法要求 API
</span><span class="sd">     * @param string $method
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">request</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$method</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">response</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">json</span><span class="p">(</span><span class="nv">$method</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">apiUrl</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">apiBody</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="sd">/**
</span><span class="sd">     * @Then 回傳狀態應為 :statusCode
</span><span class="sd">     * @param int $statusCode
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">assertStatus</span><span class="p">(</span><span class="nx">int</span> <span class="nv">$statusCode</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">response</span><span class="o">-&gt;</span><span class="na">assertStatus</span><span class="p">(</span><span class="nv">$statusCode</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>資料庫相關的 step definitions 放在 <code>DatabaseAssertionContext</code> 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Behat\Gherkin\Node\TableNode</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">DatabaseAssertionContext</span> <span class="k">extends</span> <span class="nx">FeatureContext</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * @Then 資料表 :tableName 應有資料
</span><span class="sd">     * @param string $tableName
</span><span class="sd">     * @param TableNode $table
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">assertTableRecordExisted</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$tableName</span><span class="p">,</span> <span class="nx">TableNode</span> <span class="nv">$table</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertDatabaseHas</span><span class="p">(</span><span class="nv">$tableName</span><span class="p">,</span> <span class="nv">$table</span><span class="o">-&gt;</span><span class="na">getHash</span><span class="p">()[</span><span class="mi">0</span><span class="p">]);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>當然不僅 API 和資料庫可以拆分，例如建立 Model 資料、 Event 或 Queue 相關的 step definitions 可以這樣做。</p>
<p>Model Factory ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">class</span> <span class="nc">ModelFactoryContext</span> <span class="k">extends</span> <span class="nx">FeatureContext</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * @Given 存在使用者 :name
</span><span class="sd">     * @param string $name
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">user</span><span class="p">(</span><span class="nx">string</span> <span class="nv">$name</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nx">factory</span><span class="p">(</span><span class="nx">\App\User</span><span class="o">::</span><span class="na">class</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">create</span><span class="p">([</span>
            <span class="s1">&#39;name&#39;</span> <span class="o">=&gt;</span> <span class="nv">$name</span><span class="p">,</span>
        <span class="p">]);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>Event 相關：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">App\Events\UserCreated</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Illuminate\Support\Facades\Event</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">EventAssertionContext</span> <span class="k">extends</span> <span class="nx">FeatureContext</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * @BeforeScenario
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">setUpFake</span><span class="p">()</span><span class="o">:</span> <span class="nx">void</span>
    <span class="p">{</span>
        <span class="nx">Event</span><span class="o">::</span><span class="na">fake</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="sd">/**
</span><span class="sd">     * @Then 應發送事件「已新增用戶」
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">assertDeployStatusCreatedEventDispatched</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nx">Event</span><span class="o">::</span><span class="na">assertDispatched</span><span class="p">(</span><span class="nx">UserCreated</span><span class="o">::</span><span class="na">class</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>其他就請大家自行發揮了。</p>
<h2 id="進一步改良">進一步改良</h2>
<p>上述的調整有個缺點，就是 <code>FeatureContext::before</code> 方法及 <code>FeatureContext::after</code> 方法都會初始化 Context 類別時都跑一次，但每個情境在執行時都會再次初始化所有 Context 類別；換句話說每個情境在執行時，有幾個 Context 類別，就會執行幾次 <code>FeatureContext::before</code> 方法及 <code>FeatureContext::after</code> 方法。這樣一來 <code>$this-&gt;app</code> 就會被重複初始化，徒然浪費執行時間。</p>
<p>我們希望 <code>FeatureContext::before</code> 方法及 <code>FeatureContext::after</code> 方法在每個情境執行前後各執行一次就好，可是每個 context 物件實體因為作用域的關係，它們拿到的 <code>$this-&gt;app</code> 都不會是同一個；這時也需要一個機制來記住已經被初始化的 <code>$this-app</code> ，才不用每個 context 都做一次。</p>
<p>綜合上述的想法，最後完整的 <code>FeatureContext</code> 類別如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Behat\Behat\Context\Context</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Illuminate\Contracts\Console\Kernel</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Illuminate\Foundation\Testing\RefreshDatabase</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Illuminate\Foundation\Testing\TestCase</span><span class="p">;</span>

<span class="k">abstract</span> <span class="k">class</span> <span class="nc">FeatureContext</span> <span class="k">extends</span> <span class="nx">TestCase</span> <span class="k">implements</span> <span class="nx">Context</span>
<span class="p">{</span>
    <span class="k">use</span> <span class="nx">RefreshDatabase</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">const</span> <span class="no">ENV_FILE</span> <span class="o">=</span> <span class="s1">&#39;.env.behat&#39;</span><span class="p">;</span>

    <span class="sd">/**
</span><span class="sd">     * @var \Illuminate\Foundation\Application
</span><span class="sd">     */</span>
    <span class="k">protected</span> <span class="k">static</span> <span class="nv">$contextSharedApp</span><span class="p">;</span>

    <span class="sd">/**
</span><span class="sd">     * @return \Illuminate\Foundation\Application
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">createApplication</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nv">$app</span> <span class="o">=</span> <span class="k">require</span> <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/../../bootstrap/app.php&#39;</span><span class="p">;</span>

        <span class="nv">$app</span><span class="o">-&gt;</span><span class="na">loadEnvironmentFrom</span><span class="p">(</span><span class="nx">self</span><span class="o">::</span><span class="na">ENV_FILE</span><span class="p">);</span>

        <span class="nv">$app</span><span class="o">-&gt;</span><span class="na">make</span><span class="p">(</span><span class="nx">Kernel</span><span class="o">::</span><span class="na">class</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">bootstrap</span><span class="p">();</span>

        <span class="k">return</span> <span class="nv">$app</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="sd">/**
</span><span class="sd">     * @BeforeScenario
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">before</span><span class="p">()</span><span class="o">:</span> <span class="nx">void</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">static</span><span class="o">::</span><span class="nv">$contextSharedApp</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">parent</span><span class="o">::</span><span class="na">setUp</span><span class="p">();</span>
            <span class="k">static</span><span class="o">::</span><span class="nv">$contextSharedApp</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">app</span><span class="p">;</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">app</span> <span class="o">=</span> <span class="k">static</span><span class="o">::</span><span class="nv">$contextSharedApp</span><span class="p">;</span>
        <span class="p">}</span>

    <span class="p">}</span>

    <span class="sd">/**
</span><span class="sd">     * @AfterScenario
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">after</span><span class="p">()</span><span class="o">:</span> <span class="nx">void</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="k">static</span><span class="o">::</span><span class="nv">$contextSharedApp</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">parent</span><span class="o">::</span><span class="na">tearDown</span><span class="p">();</span>
            <span class="k">static</span><span class="o">::</span><span class="nv">$contextSharedApp</span> <span class="o">=</span> <span class="k">null</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>相信上面的程式碼應該很簡單，這邊就不再多做說明了。最後我一樣把範例放在 GitHub 上，請大家自行下載參考。</p>
]]></content>
		</item>
		
		<item>
			<title>在 Laravel 中使用 Behat 來加強測試的可讀性 - 基礎篇</title>
			<link>https://jaceju.net/behat-in-laravel/</link>
			<pubDate>Thu, 08 Nov 2018 18:30:00 +0800</pubDate>
			
			<guid>https://jaceju.net/behat-in-laravel/</guid>
			<description>Laravel 的測試框架是基於 PHPUnit 上所建立出來的，而在 Laravel 5.5 之後，測試框架的功能也大幅地加強了。只不過在越來越複雜的專案規格下，我個人覺得 PHPUnit 在情境案例的描述</description>
			<content type="html"><![CDATA[<p>Laravel 的測試框架是基於 PHPUnit 上所建立出來的，而在 Laravel 5.5 之後，測試框架的功能也大幅地加強了。只不過在越來越複雜的專案規格下，我個人覺得 PHPUnit 在情境案例的描述能力上還是不太夠，最好可以用人們看得懂的語言；而目前能夠用自然語言來描述規格情境的，當然就是 <a href="https://cucumber.io/">Cucumber</a> 的 <a href="http://behat.org/en/latest/user_guide/gherkin.html">Gherkin</a> 語法了。</p>
<!-- raw HTML omitted -->
<p>Cucumber 在 PHP 中的實作，就是 <a href="http://behat.org">Behat</a> 這個 BDD 框架；雖然我很早就接觸過它了，但實際熟悉它則是在 <a href="https://skilltree.my/events/skilltree3">91 哥的 TDD 課程</a>之後的自我練習裡。後來我看到 <a href="https://laracasts.com/">Laracasts</a> 裡 Jeffrey Way 介紹他開發的 <a href="https://github.com/laracasts/Behat-Laravel-Extension">Behat-Laravel-Extension</a> 可以將 Behat 整合到 Laravel 中，著實讓我開心了一陣子。</p>
<p>不久後我就透過 Behat-Laravel-Extension 在新開發的 API 專案裡整合了 Behat ，也確實體會到了 BDD 的優異之處；而同事也在接手這個專案時，因為透過自然語言所描述的情境，很快地掌握了整個專案的規格。我們就這樣透過 BDD 很快地把一個又一個的 API 生出來，兼顧了開發效率與規格文件。</p>
<p>不過這一兩年來 Behat-Laravel-Extension 已經很久沒人維護了，在我試圖升級專案的 Laravel 版本時，這個套件發生了版本不相容的問題；加上 Behat-Laravel-Extension 相依了許多我其實用不到的 Behat 延伸套件，因此找出一個更精簡的方案就勢在必行了。</p>
<h2 id="突破點">突破點</h2>
<p>要把 Behat 用在 Laravel 的測試上，最大的問題是如何初始化 Application 。事實上 Behat 執行時只是把 <code>features/*.feature</code> 檔的 step definitions 和 <code>FeatureContext</code> 類別 (<code>features/bootstrap/FeatureContext.php</code>) 裡的 method 對應起來後，再跑遍每個 scenario 而已，所以初始化 Application 它並不負責。</p>
<p>不過如果各位有追蹤過 Laravel 專案的 <code>Illuminate\Foundation\Testing\TestCase</code> 這個類別的原始碼，你會發現它在 <code>setUp</code> 裡已經初始化了 Application ；而既然已經有類別把這件事做好，我是不是就可以直接拿它來用？沒錯！這就是我後來想到的方法。不過試了一陣子，陸陸續續有些問題我無法順利解決，導致這個作法一直被我塵封在腦海裡。</p>
<p>註：其實 Behat-Laravel-Extension 主要也是用來幫忙做初始化 Application 的工作。</p>
<p>就在前陣子我回頭思考這個問題時，想說是不是也有人有想過同樣的作法，結果還真的有！外國 Laravel 開發者 <a href="https://matthewdaly.co.uk/">Matthew Daly</a> 早在一年多前就想到這個方法了： <a href="https://matthewdaly.co.uk/blog/2017/02/18/integrating-behat-with-laravel/">Integrating Behat With Laravel</a> 。</p>
<p>以下我們就來實驗一下這個做法。</p>
<h2 id="在-laravel-初始化-behat-環境">在 Laravel 初始化 Behat 環境</h2>
<p>首先我們要建立一個新的 Laravel 專案：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">laravel new Behat-in-Laravel
<span class="nb">cd</span> Behat-in-Laravel
</code></pre></div><p>註：如果想在現有專案上直接來的話，可以省掉這個步驟，不過記得先將程式進版本控制系統。</p>
<p>安裝 Behat ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">composer require behat/behat --dev
</code></pre></div><p>接著用以下指令來初始化 Behat 的環境：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">vendor/bin/behat --init
</code></pre></div><p>這將會建立以下資料夾與檔案：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">+d features - place your *.feature files here
+d features/bootstrap - place your context classes here
+f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here
</code></pre></div><p>到這裡我們只是初始化環境而已，接下來編輯 <code>features/bootstrap/FeatureContext.php</code> 這個檔案：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Behat\Behat\Context\Context</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Behat\Gherkin\Node\PyStringNode</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Behat\Gherkin\Node\TableNode</span><span class="p">;</span>

<span class="sd">/**
</span><span class="sd"> * Defines application features from the specific context.
</span><span class="sd"> */</span>
<span class="k">class</span> <span class="nc">FeatureContext</span> <span class="k">implements</span> <span class="nx">Context</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * Initializes context.
</span><span class="sd">     *
</span><span class="sd">     * Every scenario gets its own context instance.
</span><span class="sd">     * You can also pass arbitrary arguments to the
</span><span class="sd">     * context constructor through behat.yml.
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">()</span>
    <span class="p">{</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>可以看到 <code>FeatureContext</code> 類別其實只有實作 <code>Behat\Behat\Context\Context</code> 這個介面，所以我們可以對它進行一些改造手術。</p>
<p>首先直接把 <code>FeatureContext</code> 類別繼承 Laravel 專案附帶的 <code>Tests\TestCase</code> 這個類別，並拿掉 <code>__construct</code> 建構子：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Behat\Behat\Context\Context</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Behat\Gherkin\Node\PyStringNode</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Behat\Gherkin\Node\TableNode</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Tests\TestCase</span><span class="p">;</span>

<span class="sd">/**
</span><span class="sd"> * Defines application features from the specific context.
</span><span class="sd"> */</span>
<span class="k">class</span> <span class="nc">FeatureContext</span> <span class="k">extends</span> <span class="nx">TestCase</span> <span class="k">implements</span> <span class="nx">Context</span>
<span class="p">{</span>
<span class="p">}</span>
</code></pre></div><p>接下來就是重頭戲了，加上 <code>before</code> 及 <code>after</code> 兩個 public methods ，然後讓它們分別呼叫 <code>TestCase</code> 的 <code>setUp</code> 與 <code>tearDown</code> 方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class FeatureContext extends TestCase implements Context
{
    /**
     * @BeforeScenario
     */
    public function before()
    {
        $this-&gt;setUp();
    }

    /**
     * @AfterScenario
     */
    public function after()
    {
        $this-&gt;tearDown();
    }
}
</code></pre></div><p>當然別忘了加上這兩個 hooks ： <code>@BeforeScenario</code> 及 <code>@AfterScenario</code> 。</p>
<p>之前失敗的原因主要是資料庫相關的環境變數及 migration ，而在前述的文章中 Matthew 是這樣解決的，我們照抄：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    /**
     * @BeforeScenario
     */
    public function before()
    {
        putenv(&#39;DB_CONNECTION=sqlite&#39;);
        putenv(&#39;DB_DATABASE=:memory:&#39;);
        $this-&gt;setUp();
    }
</code></pre></div><p>註：文章 Matthew 是在 <code>__construct</code> 中做環境變數的初始化與 <code>setUp</code> ，理由可能是為了不重複初始化 Application ；但我之後會介紹更巧妙的方法，所以改寫在這邊。</p>
<p>那麼該怎麼做 migration 呢？文章中因為 Laravel 的版本是 <code>5.4</code> 的關係，所以是手動呼叫 artisan 指令來處理，不過現在我們有 <code>RefreshDatabase</code> 這個 trait 可以用了：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">use Illuminate\Foundation\Testing\RefreshDatabase;

/**
 * Defines application features from the specific context.
 */
class FeatureContext extends TestCase implements Context
{
    use RefreshDatabase;
</code></pre></div><p>加入 <code>RefreshDatabase</code> trait 後， migration 相關的動作就會在 <code>setUp</code> 方法裡執行；當然所有 Laravel 測試框架提供的 trait 都可以這樣加入， <code>setUp</code> 方法都會幫你處理好。</p>
<p>這樣一來 Behat 就可以在 Laravel 測試框架的基礎上執行我們的測試，而不必再透過其他 extension 囉。</p>
<h2 id="範例">範例</h2>
<p>接下來我直接來個超簡易範例，來確認一下這個做法是否有效。<strong>不過要事先提醒大家，這個範例僅是為了示範 Behat 整合到 Laravel 的開發流程，所以會省略掉 Behat 與 Laravel 的基礎介紹，以及實務開發時該注意的細節。</strong></p>
<p>假設專案有以下這個需求：</p>
<pre><code>提供使用者名稱、 Email 與密碼，並呼叫建立 User 的 API 後，會在資料庫建立一筆使用者的資料。
</code></pre><p>經過規格討論後，我們用 Gherkin 語法建立了 <code>features/users-api.feature</code> 這個檔案，並包含了一個情境：</p>
<pre><code>#language: zh-TW

功能: User APIs

  場景: 建立使用者
    假定 API 網址為 &quot;/api/users&quot;
    而且 API 附帶資料為
      | name | email            | password |
      | User | user@example.com | example  |
    當 以 &quot;POST&quot; 方法要求 API
    那麼 回傳狀態應為 201
    而且 資料表 &quot;users&quot; 應有資料
      | id | name | email            |
      | 1  | User | user@example.com |
</code></pre><p>先執行一次以下指令：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">vendor/bin/behat --append-snippets
</code></pre></div><p>它會問我們要把 step definitions 放在哪裡：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">功能: User APIs

  場景: 建立使用者                 <span class="c1"># features/users-api.feature:5</span>
    假定 API 網址為 <span class="s2">&#34;/api/users&#34;</span>
    而且 API 附帶資料為
      <span class="p">|</span> name <span class="p">|</span> email            <span class="p">|</span> password <span class="p">|</span>
      <span class="p">|</span> User <span class="p">|</span> user@example.com <span class="p">|</span> example  <span class="p">|</span>
    當 以 <span class="s2">&#34;POST&#34;</span> 方法要求 API
    那麼 回傳狀態應為 <span class="m">201</span>
    而且 資料表 <span class="s2">&#34;users&#34;</span> 應有資料
      <span class="p">|</span> id <span class="p">|</span> name <span class="p">|</span> email            <span class="p">|</span>
      <span class="p">|</span> <span class="m">1</span>  <span class="p">|</span> User <span class="p">|</span> user@example.com <span class="p">|</span>

<span class="m">1</span> scenario <span class="o">(</span><span class="m">1</span> undefined<span class="o">)</span>
<span class="m">5</span> steps <span class="o">(</span><span class="m">5</span> undefined<span class="o">)</span>
0m0.17s <span class="o">(</span>21.79Mb<span class="o">)</span>

 &gt;&gt; default suite has undefined steps. Please choose the context to generate snippets:

  <span class="o">[</span>0<span class="o">]</span> None
  <span class="o">[</span>1<span class="o">]</span> FeatureContext
</code></pre></div><p>選 <code>1</code> 把所有的 step definitions 都存在 <code>FeatureContext</code> 類別裡：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">u features/bootstrap/FeatureContext.php - <span class="sb">`</span>API 網址為 <span class="s2">&#34;/api/users&#34;</span><span class="sb">`</span> definition added
u features/bootstrap/FeatureContext.php - <span class="sb">`</span>API 附帶資料為<span class="sb">`</span> definition added
u features/bootstrap/FeatureContext.php - <span class="sb">`</span>以 <span class="s2">&#34;POST&#34;</span> 方法要求 API<span class="sb">`</span> definition added
u features/bootstrap/FeatureContext.php - <span class="sb">`</span>回傳狀態應為 201<span class="sb">`</span> definition added
u features/bootstrap/FeatureContext.php - <span class="sb">`</span>資料表 <span class="s2">&#34;users&#34;</span> 應有資料<span class="sb">`</span> definition added
</code></pre></div><p>用編輯器打開 <code>features/bootstrap/FeatureContext.php</code> 後，就會看到以下新增的方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">
    /**
     * @Given API 網址為 :arg1
     */
    public function apiWangZhiWei($arg1)
    {
        throw new PendingException();
    }

    /**
     * @Given API 附帶資料為
     */
    public function apiFuDaiZiLiaoWei(TableNode $table)
    {
        throw new PendingException();
    }

    /**
     * @When 以 :arg1 方法要求 API
     */
    public function yiFangFaYaoQiuApi($arg1)
    {
        throw new PendingException();
    }

    /**
     * @Then 回傳狀態應為 :arg1
     */
    public function huiChuanZhuangTaiYingWei($arg1)
    {
        throw new PendingException();
    }

    /**
     * @Then 資料表 :arg1 應有資料
     */
    public function ziLiaoBiaoYingYouZiLiao($arg1, TableNode $table)
    {
        throw new PendingException();
    }
</code></pre></div><p>雖然 <code>behat --append-snippets</code> 所產生的方法在 annonation 會保留原來的中文句子，但卻會把中文的 step definition 轉換成拼音式的方法名稱，因此我們需要將每個方法的名稱換成可讀性高的名稱，同時調整參數名稱：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    /**
     * @Given API 網址為 :apiUrl
     * @param string $apiUrl
     */
    public function apiUrl(string $apiUrl)
    {
        throw new PendingException();
    }

    /**
     * @Given API 附帶資料為
     * @param TableNode $table
     */
    public function apiBody(TableNode $table)
    {
        throw new PendingException();
    }

    /**
     * @When 以 :method 方法要求 API
     * @param string $method
     */
    public function request(string $method)
    {
        throw new PendingException();
    }

    /**
     * @Then 回傳狀態應為 :statusCode
     * @param int $statusCode
     */
    public function assertStatus(int $statusCode)
    {
        throw new PendingException();
    }

    /**
     * @Then 資料表 :tableName 應有資料
     * @param string $tableName
     * @param TableNode $table
     */
    public function assertTableRecordExisted(string $tableName, TableNode $table)
    {
        throw new PendingException();
    }
</code></pre></div><p>然後再次執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">vendor/bin/behat --append-snippets
</code></pre></div><p>就不會再次詢問是不是要加入 snippets ，而是希望你把 step definitions 的方法實作出來：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">功能: User APIs

  場景: 建立使用者                 <span class="c1"># features/users-api.feature:5</span>
    假定 API 網址為 <span class="s2">&#34;/api/users&#34;</span> <span class="c1"># FeatureContext::apiUrl()</span>
      TODO: write pending definition
    而且 API 附帶資料為            <span class="c1"># FeatureContext::apiBody()</span>
      <span class="p">|</span> name <span class="p">|</span> email            <span class="p">|</span> password <span class="p">|</span>
      <span class="p">|</span> User <span class="p">|</span> user@example.com <span class="p">|</span> example  <span class="p">|</span>
    當 以 <span class="s2">&#34;POST&#34;</span> 方法要求 API     <span class="c1"># FeatureContext::request()</span>
    那麼 回傳狀態應為 <span class="m">201</span>           <span class="c1"># FeatureContext::assertStatus()</span>
    而且 資料表 <span class="s2">&#34;users&#34;</span> 應有資料     <span class="c1"># FeatureContext::assertTableRecordExisted()</span>
      <span class="p">|</span> id <span class="p">|</span> name <span class="p">|</span> email            <span class="p">|</span>
      <span class="p">|</span> <span class="m">1</span>  <span class="p">|</span> User <span class="p">|</span> user@example.com <span class="p">|</span>

<span class="m">1</span> scenario <span class="o">(</span><span class="m">1</span> pending<span class="o">)</span>
<span class="m">5</span> steps <span class="o">(</span><span class="m">1</span> pending, <span class="m">4</span> skipped<span class="o">)</span>
0m0.18s <span class="o">(</span>21.82Mb<span class="o">)</span>
</code></pre></div><p>接下來把方法實作補上：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    /**
     * @var string
     */
    private $apiUrl = &#39;&#39;;

    /**
     * @var array
     */
    private $apiBody = [];

    /**
     * @var \Illuminate\Foundation\Testing\TestResponse
     */
    private $response;

    /**
     * @Given API 網址為 :apiUrl
     * @param string $apiUrl
     */
    public function apiUrl(string $apiUrl)
    {
        $this-&gt;apiUrl = $apiUrl;
    }

    /**
     * @Given API 附帶資料為
     * @param TableNode $table
     */
    public function apiBody(TableNode $table)
    {
        $this-&gt;apiBody = $table-&gt;getHash()[0];
    }

    /**
     * @When 以 :method 方法要求 API
     * @param string $method
     */
    public function request(string $method)
    {
        $this-&gt;response = $this-&gt;json($method, $this-&gt;apiUrl, $this-&gt;apiBody);
    }

    /**
     * @Then 回傳狀態應為 :statusCode
     * @param int $statusCode
     */
    public function assertStatus(int $statusCode)
    {
        $this-&gt;response-&gt;assertStatus($statusCode);
    }

    /**
     * @Then 資料表 :tableName 應有資料
     * @param string $tableName
     * @param TableNode $table
     */
    public function assertTableRecordExisted(string $tableName, TableNode $table)
    {
        $this-&gt;assertDatabaseHas($tableName, $table-&gt;getHash()[0]);
    }
</code></pre></div><p>再次執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">vendor/bin/behat --append-snippets
</code></pre></div><p>就會出現：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">功能: User APIs

  場景: 建立使用者                 <span class="c1"># features/users-api.feature:5</span>
    假定 API 網址為 <span class="s2">&#34;/api/users&#34;</span> <span class="c1"># FeatureContext::apiUrl()</span>
    而且 API 附帶資料為            <span class="c1"># FeatureContext::apiBody()</span>
      <span class="p">|</span> name <span class="p">|</span> email            <span class="p">|</span> password <span class="p">|</span>
      <span class="p">|</span> User <span class="p">|</span> user@example.com <span class="p">|</span> example  <span class="p">|</span>
    當 以 <span class="s2">&#34;POST&#34;</span> 方法要求 API     <span class="c1"># FeatureContext::request()</span>
    那麼 回傳狀態應為 <span class="m">201</span>           <span class="c1"># FeatureContext::assertStatus()</span>
      Expected status code <span class="m">201</span> but received 404.
      Failed asserting that <span class="nb">false</span> is true.
    而且 資料表 <span class="s2">&#34;users&#34;</span> 應有資料     <span class="c1"># FeatureContext::assertTableRecordExisted()</span>
      <span class="p">|</span> id <span class="p">|</span> name <span class="p">|</span> email            <span class="p">|</span>
      <span class="p">|</span> <span class="m">1</span>  <span class="p">|</span> User <span class="p">|</span> user@example.com <span class="p">|</span>

Failed scenarios:

    features/users-api.feature:5

<span class="m">1</span> scenario <span class="o">(</span><span class="m">1</span> failed<span class="o">)</span>
<span class="m">5</span> steps <span class="o">(</span><span class="m">3</span> passed, <span class="m">1</span> failed, <span class="m">1</span> skipped<span class="o">)</span>
0m0.19s <span class="o">(</span>22.82Mb<span class="o">)</span>
</code></pre></div><p>剩下的就是完成 API 程式碼實作啦，這裡就不多做介紹了，完成的程式碼請到 <a href="https://github.com/jaceju-tutorial-examples/behat-in-laravel-basic">GitHub</a> 上查看。</p>
<p>下一篇我會再介紹稍微進階的做法。</p>
]]></content>
		</item>
		
		<item>
			<title>理解 composer.json 的 replace</title>
			<link>https://jaceju.net/composer-replace/</link>
			<pubDate>Fri, 02 Nov 2018 12:30:00 +0800</pubDate>
			
			<guid>https://jaceju.net/composer-replace/</guid>
			<description>通常你在 composer.json 裡很少會用到 replace 這個 schema 屬性 ，不過在以下情境它就很有用了。 例如你正在開發一個專案 (也就是在 composer.json 中的 type 為 project ) ，它相依 original/library 這個套件以及 other/package 這個套</description>
			<content type="html"><![CDATA[<p>通常你在 composer.json 裡很少會用到 <code>replace</code> 這個 schema 屬性 ，不過在以下情境它就很有用了。</p>
<p>例如你正在開發一個專案 (也就是在 <code>composer.json</code> 中的 <code>type</code> 為 <code>project</code> ) ，它相依 <code>original/library</code> 這個套件以及 <code>other/package</code> 這個套件。很巧的是， <code>other/package</code> 這個套件也相依了 <code>original/library</code> 這個套件。</p>
<p><img src="/resources/composer-replace/original-library.png" alt=""></p>
<p>假設現在你覺得 <code>orginal/library</code> 這個可能已經太老舊，但維護者又不想讓你更動它，所以你決定把它 fork 出來改成一個相容但是功能更強大的 <code>better/library</code> ，並且把它標上新版號釋出。</p>
<p>現在回到你的專案，將原來的 <code>original/library</code> 改成 <code>better/library</code> ，並希望一切正常；只是 <code>other/package</code> 還相依在 <code>original/library</code> ，使得它跟你的 <code>better/library</code> 有衝突。那麼該怎麼讓 <code>other/package</code> 也改用 <code>better/library</code> 呢？我們可不打算再 fork 一份 <code>other/package</code> ！</p>
<p>這時候就是 <code>replace</code> 出場的時機了。</p>
<!-- raw HTML omitted -->
<p><code>replace</code> 的用法很簡單，你可以在 <code>better/library</code> 的 <code>composer.json</code> 裡加入這個設定：</p>
<pre><code>&quot;replace&quot;: {
    &quot;original/library&quot;:&quot;1.*&quot;
}
</code></pre><p>這麼一來 composer 就會在安裝 <code>other/package</code> 時，自動把有用到 <code>original/library</code> 全換成 <code>better/library</code> 了。</p>
<p><img src="/resources/composer-replace/better-library.png" alt=""></p>
<p>另外 <code>replace</code> 也常用在主套件包含多個子套件的用途，因為通常安裝主套件時就會涵蓋所有子套件的功能；例如 <code>laravel/framework</code> 就是這樣的用法，你會看到它的 <code>composer.json</code> 包含了這段：</p>
<pre><code>&quot;replace&quot;: {
    &quot;illuminate/auth&quot;: &quot;self.version&quot;,
    &quot;illuminate/broadcasting&quot;: &quot;self.version&quot;,
    ...
    &quot;illuminate/view&quot;: &quot;self.version&quot;,
    &quot;tightenco/collect&quot;: &quot;self.version&quot;
},
</code></pre><p>跟前面一樣的邏輯，假設你的專案相依了 <code>laravel/framework</code> ，而另一個相依的套件用到了 <code>illuminate/auth</code> 的話，那麼它就會以 <code>laravel/framework</code> 的版本為主，而不會重複再載入 <code>illuminate/auth</code> 。而其中 <code>self.version</code> 就是指向主套件目前的版本。</p>
<p>內容來源參考： <a href="http://stackoverflow.com/questions/18882201/how-does-the-replace-property-work-with-composer/18905069#18905069">How does the “replace” property work with composer?</a></p>
]]></content>
		</item>
		
		<item>
			<title>重構或重寫 Legacy code 的幾個階段</title>
			<link>https://jaceju.net/steps-of-refactoring-or-rebuilding/</link>
			<pubDate>Wed, 09 May 2018 20:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/steps-of-refactoring-or-rebuilding/</guid>
			<description>看完前一篇的介紹，我想你應該已經想好面對 legacy code 時，應該要重構還是重寫了。如果你打算對正在線上的程式碼進行重構，那就像在幫飛行中的飛機換引擎一樣</description>
			<content type="html"><![CDATA[<p>看完<a href="/refactor-or-rebuild">前一篇的介紹</a>，我想你應該已經想好面對 legacy code 時，應該要重構還是重寫了。如果你打算對正在線上的程式碼進行重構，那就像在幫飛行中的飛機換引擎一樣；如果是重寫，那就是造一架新飛機讓它升空，然後在空中把舊飛機裡的乘客接過來一樣。</p>
<p>那麼要怎麼在空中幫飛機換引擎或接送乘客呢？應該有些具體的方案吧？其實有幾件事是兩個方法都必須先做的，以下就整理了幾個重構或重寫前重要的步驟。</p>
<!-- raw HTML omitted -->
<h2 id="綜覽全局">綜覽全局</h2>
<p>不論你打算重構或重寫，我都會建議你花點時間整理一下專案目前的規格以及各功能的使用情境；這些資訊的來源可能有好幾種：文件裡或 issue tracking 系統中有記載的、專案關係人或曾經維護過該專案的同事還記得的、直接從 UI 上操作後所得到的結果等等。</p>
<p>這些資訊可以用白板加便利貼來整理，或是使用 Trello 讓大家都能看到；如果專案太過龐大，那麼至少整理一下你負責的部份。這麼做的原因是幫助你或團隊更為理解這個專案的全貌。同時也可以趁這個時候去確認功能的存廢，以及是否有哪些規格與實作的對應上其實有問題的。</p>
<p>你可能會覺得這應該是 PM 的工作，但我必須說，現實中的你就是 PM 的延伸。當然如果是整個團隊決定要重構或重寫的話，就讓大家一起來做這些工作吧。</p>
<h2 id="理解-legacy-code-的架構">理解 legacy code 的架構</h2>
<p>如果你不知道 legacy code 是怎麼運作的，那就別想要重構或重寫了。有時候 legacy code 中的某些設計，通常是因為當時的時空背景而產生的。所以當你能夠從系統架構到程式架構來深入理解前人在 legacy code 中做了哪些事情，就能瞭解你面臨了什麼樣的限制。</p>
<p>在理解的過程中，你可以嘗試整理出一些開發文件，釐清程式需要的系統環境、使用的資料庫或外部服務、或是相依的套件等資訊。得到這些資訊後，你可以著手分析應該哪些部份應該重構或是重寫；因為有時候可能會因為某些 legacy code 而導致系統無法進行安全性升級，或是服務轉換上的困難，像這類的程式碼就可能要進行重寫。</p>
<h2 id="導入持續整合與自動化佈署">導入持續整合與自動化佈署</h2>
<p>多數 legacy 專案從開發到上線的過程中，常常有很多地方需要人工介入，像是靜態分析、程式碼風格及語法檢查、測試、佈署等等。透過導入持續整合 (Continuous Integration 簡稱 CI) 以及自動化佈署後，這些專案就再也不是 legacy 專案了；整個建置佈署的過程完全自動化，因為人工介入而出錯的機會大大降低。</p>
<p>這一步看起來對實際的程式碼沒什麼影響，但事實上對開發者心理層面影響非常巨大。一般來說，這個流程還會搭配版本控制系統的分支來做不同執行環境的自動化佈署，分離出測試環境與正式環境；在程式佈署到測試環境時，就能讓開發者更快速地發現某些無法透過單元測試來找出的整合性問題。</p>
<h2 id="導入-e2e-自動化測試">導入 e2e 自動化測試</h2>
<p>另一項更重要的工作是自動化測試，它對程式來說就像是個安全保險。所謂的 legacy code 通常就是指沒有自動化測試的程式碼，所以每次在修改它們時總是會讓人心驚膽跳，深怕改東壞西。因此不論是想重構或是重寫，都必須先為程式碼加自動化測試才行。</p>
<p>但這時候幫細部程式做單元測試其實不是很明智的抉擇，因為大粒度的重構或是整個重寫，都會讓程式單元有大幅度的調整。因此通常會依據正在上線的程式所呈現的外在行為來做為測試結果的驗證基準，一般來說這就是指撰寫 e2e 測試；而前面整理出來的規格與情境，這時就可以用來 e2e 測試的測試案例了。</p>
<p>完成 e2e 測試後，接下來就可以進行重構或重寫了。而重構或重寫都有某些技巧，以下介紹幾個：</p>
<h2 id="選個好-ide-讓自己看見-legacy-code-的病因">選個好 IDE 讓自己看見 legacy code 的病因</h2>
<p>以 PHP 為例，很多人會覺得用 VIM 或其他程式編輯器來開發 PHP 就已經足夠了，當然這沒有什麼不好。只是當每次我用 PhpStorm 打開用他們寫的程式碼時，就會看到紅色波浪底線、土黃色背景的常數、灰色的變數或類別，心中就會有種不快的感覺。因為你太晚知道你的程式碼有病，而這些病是其他編輯器沒有幫你找到的。</p>
<p>也許你想到了程式碼靜態分析是可以發覺這些問題的方法，只是大多數開發者不會意識到要去做這件事；而且即便知道該做而去做，時間一久也會覺得麻煩或根本就忘了這回事。也許有些人想說乾脆在 commit 前先讓工具去自動檢查吧，這聽起來像是個好主意；只是要等到每次 commit 才會發現這些問題，你早就遺失當下 coding 時的 mind context 了。</p>
<h2 id="用功能切換-feature-toggle-來重構">用功能切換 (Feature Toggle) 來重構</h2>
<p>面對絕大部份的 legacy code 我們很難一步到位地重構它，這時我們可以透過 feature toggle 這種手法來將系統的功能一個一個地轉換到新的架構上。 Feature toggle 可以讓新舊程式架構同時並行，利用開關變數來切換新舊程式。</p>
<p>由於系統對外的 API 或頁面並沒有改變，而且在 e2e 測試的保證下，使用者其實並不會有所感覺。另一個好處就是，即使是在重構的過程中有新需求的加入，就可以將它放到新架構中，不必回到 legacy code 中再做一次。</p>
<h2 id="用框架重新改寫">用框架重新改寫</h2>
<p>如果 legacy code 連 e2e 測試都不好做，那麼表示它跟環境的關係已經根深蒂固了。我在遇到這類專案時，選擇的就是重寫。只不過重寫不能再重蹈覆轍，所以選擇一個能夠方便做 e2e 測試或整合測試的框架就是我重寫的第一個目標。雖然說 e2e 測試或整合測試也是可以自己來，但與其把力氣花在測試架構的設計上，不如去找個優秀框架來得快；因為優秀的框架通常已經把很多底層的工作抽象化，搭配整理好的規格與情境所撰寫的 e2e 測試，就可以讓開發者專注在功能面的開發上。</p>
<p>在用框架重寫時，可以把握一個重點：越高層越接近需求，越底層越接近實作；也就是利用框架的抽象機制來封裝底層的操作細節，例如資料庫或快取等。這麼一來即使底層服務需要抽換，也不至於影響到高層的邏輯。很多 legacy code 就是在邏輯層還夾雜很多底層服務的操作，結果在系統需要更換環境時，得花很多力氣在修改這些程式碼。</p>
<h2 id="討厭寫文件那就改用-bdd-behavior-driven-development">討厭寫文件？那就改用 BDD (Behavior-Driven Development)</h2>
<p>重寫的專案我會要求用 BDD 來開發，原因無它：因為寫好的文件就可以拿來驗證程式碼。如果你覺得寫文件很浪費時間，不如寫程式一次搞定，那麼 BDD 絕對會是你應該試試的開發模式。因為常見的 BDD 框架通常是採用 <a href="https://github.com/cucumber/cucumber/wiki/Gherkin">Gherkin 語法</a>來描寫功能 (feature) ，這使得文件本身很好讀，又容易轉換成驗證用的 context 程式，所以也很適合用在 PM 與開發者合作；也就是 PM 寫 feature ，讓開發者用 feature 來驗證自己寫的程式碼。</p>
<p>BDD 通常是以情境來當驗證案例，所以前面收集到的規格和情境很適合用在這裡，也因此 BDD 通常會結合 e2e 測試來進行。這麼一來文件的更新，也會影響到測試是否能夠通過，就不會再發生文件和程式碼不一致的情況了。</p>
<h2 id="持續保持正確的心態">持續保持正確的心態</h2>
<p>不知道是哪一國的童軍守則提到：「讓出去的營地比進去時乾淨。」這是身為優秀開發者也必須遵守的好守則。面對 legacy code 我們當然可以抱怨，但也不可以因此就撒手不管。既然接手維護了，在沒什麼外力的狀況下 (通常是政治因素) ，就應該利用各種方法一步一步讓它變的更好維護，而不是成為之後接手的人的夢魘。</p>
<p>不論是重構或是重寫，能生出易於維護的優良程式碼絕對是開發者的驕傲。</p>
]]></content>
		</item>
		
		<item>
			<title>面對 Legacy Code ，該重構還是重寫？</title>
			<link>https://jaceju.net/refactor-or-rebuild/</link>
			<pubDate>Wed, 09 May 2018 20:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/refactor-or-rebuild/</guid>
			<description>在程式已經上線很久很久的情況下，公司也找了不少人來維護這些程式碼；而因為開發者來來去去，加上每個人對程式開發的觀念不同，他們也許都有過這樣的</description>
			<content type="html"><![CDATA[<p>在程式已經上線很久很久的情況下，公司也找了不少人來維護這些程式碼；而因為開發者來來去去，加上每個人對程式開發的觀念不同，他們也許都有過這樣的感受：</p>
<ul>
<li>因為缺少規格文件，這團程式碼到底有哪些是該拔還是不該拔，很抖；算了，放著不管吧。</li>
<li>因為迫於時間壓力，被只求解決眼前問題所帶來的不穩定性困擾著；算了，有 bug 再修。</li>
<li>因為沒有寫過測試，修正程式時常常擔心改東壞西的不安全感；算了，有人叫再說。</li>
<li>因為團隊已有慣例，想要動手調整架構，卻怕同事不支持；算了，給下個人弄吧。</li>
<li>因為依賴語言特性，有些好的作法引入之後反而造成問題；算了，反正會動就好。</li>
</ul>
<p>結果專案中的技術債越欠越多，而這些程式碼也變成了所謂的 legacy code 。</p>
<!-- raw HTML omitted -->
<p>在維護這樣的 legacy code 時，我想很多開發者應該都思考過這個問題：「我到底應該要重構還是重寫？」老實說我的答案就是 &ldquo;It depends&rdquo; 。</p>
<p>重構與重寫沒有哪種方法一定比較好，不過在選擇之前，我還是得先把重構和重寫的不同說明清楚。</p>
<h2 id="重構不是你想的那樣">重構不是你想的那樣</h2>
<p>不過很多人對「重構」這個詞有所誤解，以為它是在功能完成後大幅度地修改原有的程式碼；放在 legacy code 上時，大概就會認為是「架構設計翻新」這種程度的修改。</p>
<p>事實上重構的粒度是非常小的，它並不是整個專案做完之後才開始，而是開發功能的過程中就一小步一小步的進行。重構的時間點不要離寫完程式碼的時間點太遠，像是在完成一小部份的功能後就立刻回頭看看是否能讓程式碼更結構化，或是在每次開始新功能先思考既有結構是否影響了新功能的加入；然後針對這些部份進行微調，所以重構佔的是開發一小部份的時間。</p>
<p>會提出要重構 legacy code 的人，多數是希望不要花太多力氣去撰寫新的程式碼，而是重用已經存在的邏輯；畢竟已上線運作的舊有程式碼，比起那些沒經過時間驗證過的新寫程式碼來得可靠數百倍。只是如果想重用這些既有邏輯有兩個前提：一、它們易於理解且有組織化；二、有自動化測試。不過令人苦惱的是：大多數 legacy code 都很難達成這兩個要求。</p>
<p>但假設這兩個前提達到的情況下，進行大粒度的重構也不是不可能，但還是有些工作得先進行。</p>
<h2 id="重寫也不是你想的那樣">重寫也不是你想的那樣</h2>
<p>那麼重寫整個專案呢？重寫聽起來是擺脫一些舊包袱的好方法，似乎這樣就可以不用再去理會那些 legacy code 了。所以就可能會有些剛到公司幾個月的新人，會在看不慣前人寫的程式碼時就提出了重寫的建議，只不過事情通常不會是像他們想得這麼簡單。</p>
<p>很多 legacy code 問題都不是技術的問題，而是它已經創造出一個每個人都依賴著它的世界，所有相關人士與現存系統對規則的認知都是基於這些 legacy code ；因此一旦你決定真的要重寫整個專案，你必須有相當的自信去說服所有關係人，並爭取到上頭的支持；最好能提出一份可行的 roadmap ，來讓大家能夠審視你的想法。再來也要考慮到公司是否對這個專案有其他預定中的計劃，有沒有時間讓你重寫。</p>
<p>重寫最重要的一點，是你夠不夠清楚這個系統現行的規格。當你真的可以重寫時，你必須邀請 PM 或熟悉這個系統來龍去脈的人員來協助你瞭解整個舊有系統。很多失敗的重寫，就是因為不夠瞭解原有的系統規則，不清楚為什麼前人針對某些情境撰寫 workaround ，結果在新專案上線時因為少做了某些細部功能而使得整個服務大爆炸。所以當你想大刀闊斧地砍掉重寫，就得想辦法把所有規則都涵蓋到；而這件事本身就是曠日費時，你不可能馬上完成，所以還是必須顧好現行的 legacy code 。</p>
<p>這使得重寫有個很明顯的劣勢：當關係人打算在服務新增功能時，你可能要在舊專案和新專案上同時進行。所以一般重寫時都會請 PM 凍結需求，讓新專案上線後再繼續實作這些新功能；只是這時候你就得跟時間賽跑，如果你家 PM 人很好的話會幫你擋需求，只是他擋得了一時，擋不了一世。</p>
<h2 id="所以我到底該選哪條路">所以我到底該選哪條路？</h2>
<p>假設不論是重構或是重寫，公司都給時間了，我們該怎麼選擇呢？雖然說是 &ldquo;It depends&rdquo; ，但總應該有些判斷的原則吧？這邊我簡單整理重構時要著重的幾個要點：</p>
<ol>
<li>專案本身已經有整理好相關文件，同時也有自動化測試。</li>
<li>專案本身只是部份程式體質不佳，整體來說還算好理解。</li>
<li>專案影響到的關係人太多，無法輕易凍結需求。</li>
</ol>
<p>符合上述特點的專案，你可以考慮用重構，一點一點地來將它的體質調整好。</p>
<p>那麼什麼時候要考慮重寫？</p>
<ol>
<li>改個幾行程式碼就得花很多成本來手動測試。</li>
<li>專案用的技術已經沒人改得動它了，或是新的系統環境不支援。</li>
<li>現行的技術或是你的能力可能可以更快地實現同樣的系統。</li>
<li>沒人知道某些功能還有沒有用，文件也早就遺失了。</li>
</ol>
<p>當然以上列的這些要點不見得有全面的考量，重要的還是要看大家所面對的專案狀況來決定。簡單來說，當 legacy code 的複雜度已經高到你難以維護，每次修改都要耗費大量時間與人力去驗證時，也許就是重寫的契機；在這之前，你還是應該以重構為優先。</p>
<p>不論是重構或重寫，都一定要瞭解這兩者對公司所產生的衝擊，這樣才能找出影響最小的方式來讓 legacy project 煥然一新。</p>
<p>下一篇，我來聊聊一些我在<a href="/steps-of-refactoring-or-rebuilding">重構或重寫時的經驗與心得</a>。</p>
]]></content>
		</item>
		
		<item>
			<title>測試該驗證結果還是該驗證細節</title>
			<link>https://jaceju.net/to-test-the-detail-or-to-test-the-result/</link>
			<pubDate>Sat, 18 Mar 2017 23:29:11 +0800</pubDate>
			
			<guid>https://jaceju.net/to-test-the-detail-or-to-test-the-result/</guid>
			<description>很多人在寫測試時，常會陷入該去測試結果還是測試細節的困擾裡。雖然我的測試寫得不算太多，但我還是提供一些看法與實務上的經驗供大家參考。 從生活中</description>
			<content type="html"><![CDATA[<p><img src="/resources/to-test-the-detail-or-to-test-the-result/to-test-the-detail-or-to-test-the-result.png" alt=""></p>
<p>很多人在寫測試時，常會陷入該去測試結果還是測試細節的困擾裡。雖然我的測試寫得不算太多，但我還是提供一些看法與實務上的經驗供大家參考。</p>
<!-- raw HTML omitted -->
<h2 id="從生活中的實例來看">從生活中的實例來看</h2>
<p>我們從一個生活上的例子來說明好了，以下我會把「你」當成主角，這樣代入感會強烈一點。</p>
<p>假設你每週三下班後都要固定參加某個聚會，而這個聚會的開始時間是晚上七點半；換句話說，不管你何時下班，你一定要在七點半前出現在會場。</p>
<p>用稍微正式一點的 User Story 寫法就會像這樣：</p>
<pre><code>為了「下班去參加每週三七點半的聚會」
身為「固定成員」
我要在「七點半前出現在會場」
</code></pre><p>好了，現在你需要交通工具去會場，以你的經驗，你覺得公車是最快的方式。</p>
<p>(為了解釋方便，這裡我會特意忽略走路等其他瑣碎的行為所耗費的時間，請不要太在意。)</p>
<p>所以以下是你的測試過程：</p>
<ul>
<li>假定我晚上六點半下班</li>
<li>假定公車是晚上六點四十分來</li>
<li>我搭公車搭了四十分鐘</li>
<li>我斷定自己在晚上七點半前到達會場</li>
</ul>
<p>看起來很完美，連續幾週下來這個測試也沒發生什麼問題，直到某天你嚴重遲到。</p>
<p>追查這個測試失敗的原因，你發現公車時刻更改了，原本六點四十分的公車改到六點五十分；另外加上七點時某段路口塞車狀況突然變得很嚴重，使得原本四十分鐘的路程變成五十分鐘。這兩個因素使得你必須另外找尋替代的交通工具，例如捷運或計程車，想當然這個測試也不能再用了。</p>
<p>從上面的例子可以看到，搭公車的細節影響著你對這個規格的驗證，但實際上規格並沒有要求你一定要搭公車。搭公車本身上一種 Non-Feature ，是開發者為了滿足需求而加入的額外條件。事實上你搭任何一種交通工具都可以，只要它能在時間內帶你到目的地就行。</p>
<p>既然在這個需求中搭公車是不必要的細節，我們可以把測試改成這樣：</p>
<ul>
<li>假定我晚上六點半下班。</li>
<li>我搭上車程一小時以內的交通工具。</li>
<li>我斷定自己在晚上七點半前到達會場。</li>
</ul>
<p>現在你就可以搭任何一種交通工具，只要它能滿足你的需求。換句話說，你的程式是依賴在交通工具這個介面上。</p>
<p>這麼一來就能導出一個觀念：<strong>測試案例應該只測試關注的事情，當測試粒度越粗，關注的事也該越抽象</strong>。</p>
<p>讓測試相依於介面的好處在於重構時，我們不用再對細節去調整測試；你只需要對介面去調整期望的輸出就好，即便是你想測試異常狀況。</p>
<h2 id="那麼細節不重要嗎">那麼細節不重要嗎？</h2>
<p>你可能會想：「不對呀，我還是對車程一小時要怎麼達成這件事沒什麼概念。像剛剛公車時刻和公車車程就很容易驗證，只有一小時這個條件的話我很難驗證，所以還是要驗證細節吧？」</p>
<p>當然，這裡我們確實需要對特定交通工具的車程有驗證方法，但它不屬於顯性的需求，我們要另外對這類隱性需求來獨立出我們的模組。</p>
<p>剛剛提到我們可以有不同的交通工具來達成需求，但我們因為經驗只選擇了公車；這對應到軟體開發實務上的話，也就是因為時程關係，我們會先用某種當下可行的做法。</p>
<p>這時候我們要對實作交通工具這個介面的公車做行為上的驗證，目標是一小時內的車程，所以我們可以對公車寫出以下的測試：</p>
<ul>
<li>假定晚上六點四十分有一班 306 公車從甲地出發。</li>
<li>假定它到達乙地的的時刻為晚上七點二十分。</li>
<li>我搭上這班公車。</li>
<li>我斷定我到達乙地花了四十分鐘。</li>
</ul>
<p>因為我們關注的對象是公車，所以這時候公車時刻和哪班公車這些細節對我們來說就是必要的。</p>
<p>把實作拆開來獨立測試後，你就可以專注在這個實作應該有的行為；重構也不成問題，只要它最後符合我們需要的介面即可。而當你需要更換實作時，也不會因此對上一層的測試有太大的影響，不論你是不是用到測試替身 (不用替身時也許會有小調整，例如更換實作類別) 。</p>
<h2 id="實務經驗">實務經驗</h2>
<p>我們在實作 API 時，就是利用這樣的思維在寫測試的：我們會先定義好 API 要怎麼用，也就是外部會怎麼操作這個 API ，所有的測試也是針對這個行為而寫。在實作上呢，它實際是透過 controller 去呼叫 service 的方法，而 service 層也會呼叫很多 libraries 或 respositories 來做資料上的互動。</p>
<p>一開始我負責寫 API 的測試與實作，但 service 則交給同事開發。我們討論好它們兩者之間用什麼方法溝通，用什麼格式溝通之後，就各自進行 TDD 開發。這麼一來，我不必擔心 service 實作會影響我的開發，同事也只要專注在 service 的行為是不是符合業務邏輯的需求就可以。</p>
<h2 id="結論">結論</h2>
<p>我在開發架構的中心思想一直是：越上層就越抽象來貼近需求，越下層就越具體以達成實作；每一層都應該清楚它是因為了什麼目的而存在，進而專注去實現這個目的。</p>
<p>我後來發現這個思想也可以套用在寫測試上面：不論是哪一層的測試都應該專注在驗證該層被賦予的責任上，不要跨層去看下一層的細節是怎麼做的；結果在測試中大量測試了這些細節，導致在修改程式時，還得花大量時間去修改測試。</p>
]]></content>
		</item>
		
		<item>
			<title>分析 PHP 程式碼品質</title>
			<link>https://jaceju.net/php-quality-analysis/</link>
			<pubDate>Wed, 11 Jan 2017 18:29:49 +0800</pubDate>
			
			<guid>https://jaceju.net/php-quality-analysis/</guid>
			<description>前兩天 Laravel 老爸 Taylor 分享了他對幾個知名的 framework 所做的程式碼品質分析，引起了社群很大的討論。 因此，我想藉機分享一下： 如何對 PHP 程式碼做分析。 分析出來的數值</description>
			<content type="html"><![CDATA[<p>前兩天 Laravel 老爸 Taylor 分享了他對幾個知名的 framework 所做的<a href="https://medium.com/@taylorotwell/measuring-code-complexity-64356da605f9#.i9ah5inwx">程式碼品質分析</a>，引起了社群很大的<a href="https://www.facebook.com/groups/laravel.tw/permalink/1200376173364763/">討論</a>。</p>
<p>因此，我想藉機分享一下：</p>
<ul>
<li>如何對 PHP 程式碼做分析。</li>
<li>分析出來的數值有什麼意義。</li>
</ul>
<!-- raw HTML omitted -->
<h2 id="php-程式碼分析工具">PHP 程式碼分析工具</h2>
<p>PHP 程式碼分析工具其實有很多，但一一安裝其實很花時間。這裡我推薦一個套件： <a href="https://github.com/EdgedesignCZ/phpqa">PHPQA</a> ，它已經把以下常用的 PHP 程式碼分析工具整合在一個命令列裡了：</p>
<ul>
<li><a href="https://github.com/sebastianbergmann/phploc">phploc</a> - 測試 PHP 專案的大小 (即程式碼行數等資訊)</li>
<li><a href="https://github.com/sebastianbergmann/phpcpd">phpcpd</a> - 重複程式偵測</li>
<li><a href="https://github.com/squizlabs/PHP_CodeSniffer">phpcs</a> - 程式碼風格檢查</li>
<li><a href="https://github.com/pdepend/pdepend">pdepend</a> - 程式碼依賴度檢查</li>
<li><a href="https://github.com/phpmd/phpmd">phpmd</a> - 找出專案複雜度過高的程式碼</li>
<li><a href="https://github.com/Halleck45/PhpMetrics">phpmetrics</a> - PHP 程式碼靜態分析</li>
<li><a href="https://github.com/JakubOnderka/PHP-Parallel-Lint">parallel-lint</a> - 同時檢查多個 PHP 檔案的語法</li>
<li><a href="https://github.com/phpstan/phpstan">phpstan</a> - 在還沒執行程式前就找到可能的 bug (結果它本身也有 bug )</li>
</ul>
<p>PHPQA 的安裝方式很簡單，用 composer 全域安裝即可：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ composer global require edgedesign/phpqa --update-no-dev
</code></pre></div><p>不過這時候安裝的 <code>phpcpd 2.0.4</code> 會有<a href="https://github.com/sebastianbergmann/phpcpd/issues/132">問題</a>，所以要安裝 <code>dev-master</code> 版：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ composer global require sebastian/phpcpd:dev-master
</code></pre></div><p>現在可以確認一下是否有安裝成功了，執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ phpqa tools
+---------------+------------+----------------------------------------------+
<span class="p">|</span> Tool          <span class="p">|</span> Version    <span class="p">|</span> Authors                                      <span class="p">|</span>
+---------------+------------+----------------------------------------------+
<span class="p">|</span> phpqa         <span class="p">|</span> 1.9.1      <span class="p">|</span> Zdenek Drahos                                <span class="p">|</span>
<span class="p">|</span> phpmetrics    <span class="p">|</span> 1.10.0     <span class="p">|</span> Jean-François Lépine                         <span class="p">|</span>
<span class="p">|</span> phploc        <span class="p">|</span> 3.0.1      <span class="p">|</span> Sebastian Bergmann                           <span class="p">|</span>
<span class="p">|</span> phpcs         <span class="p">|</span> 2.7.1      <span class="p">|</span> Greg Sherwood                                <span class="p">|</span>
<span class="p">|</span> phpmd         <span class="p">|</span> 2.5.0      <span class="p">|</span> Manuel Pichler,Other contributors,Marc Würth <span class="p">|</span>
<span class="p">|</span> pdepend       <span class="p">|</span> 2.3.2      <span class="p">|</span>                                              <span class="p">|</span>
<span class="p">|</span> phpcpd        <span class="p">|</span> dev-master <span class="p">|</span> Sebastian Bergmann                           <span class="p">|</span>
<span class="p">|</span> parallel-lint <span class="p">|</span> 0.9.2      <span class="p">|</span> Jakub Onderka                                <span class="p">|</span>
+---------------+------------+----------------------------------------------+
</code></pre></div><blockquote>
<p>記得確認一下 <code>$HOME/.composer/vendor/bin</code> 有在你的 <code>$PATH</code> 環境變數裡，不然沒辦法執行安裝好的指令。</p>
</blockquote>
<h2 id="用-phpqa-產生分析報表">用 PHPQA 產生分析報表</h2>
<p>現在可以找一個 PHP 專案來試試 <code>phpqa</code> ，這裡我直接用一個新的 Laravel 專案來實驗：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ laravel new qa-example
$ <span class="nb">cd</span> qa-example
</code></pre></div><p>然後執行 <code>phpqa</code> 來跑出報表：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ phpqa --ignoredDirs vendor --report
</code></pre></div><p><code>--ignoredDirs</code> 是指排除掉指定的目錄，也可以改用 <code>--analyzedDirs</code> 來分析特定的目錄；目錄名稱之間可以用逗號 (<code>,</code>) 隔開。</p>
<p>執行結果如下：</p>
<pre><code>... (略) ...
[phpqa]
+---------------+----------------+--------------+--------+---------------------------+
| Tool          | Allowed Errors | Errors count | Is OK? | HTML report               |
+---------------+----------------+--------------+--------+---------------------------+
| phpmetrics    |                |              | ✓      | build//phpmetrics.html    |
| phploc        |                |              | ✓      | build//phploc.html        |
| phpcs         |                | 26           | ✓      | build//phpcs.html         |
| phpmd         |                | 4            | ✓      | build//phpmd.html         |
| pdepend       |                |              | ✓      | build//pdepend.html       |
| phpcpd        |                | 0            | ✓      | build//phpcpd.html        |
| parallel-lint |                | 0            | ✓      | build//parallel-lint.html |
+---------------+----------------+--------------+--------+---------------------------+
| phpqa         |                | 30           | ✓      | build//phpqa.html         |
+---------------+----------------+--------------+--------+---------------------------+

[phpqa] No failed tools
</code></pre><p>這樣 <code>phpqa</code> 就會在專案下建立 <code>build</code> 資料夾，裡面就會放著所有分析報表；我們只需要打開 <code>build/phpqa.html</code> 就可以看到所有的報表了。</p>
<p><img src="../resources/phpqa/phpqa.png" alt="phpqa.html"></p>
<p>更好的做法是把它們整合到 CI 自動化流程裡，這樣每一次 push 程式碼後就可以自動產生這些報表。</p>
<p>另外我們也可以在專案目錄下建立一個 <a href="https://github.com/EdgedesignCZ/phpqa/blob/master/.phpqa.yml"><code>.phpqa.yml</code></a> 設定檔，來微調這些工具的設定。例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-yml" data-lang="yml"><span class="k">phpcs</span><span class="p">:</span><span class="w">
</span><span class="w">    </span><span class="k">standard</span><span class="p">:</span><span class="w"> </span>PSR2<span class="w">
</span><span class="w">
</span><span class="w"></span><span class="k">phpmd</span><span class="p">:</span><span class="w">
</span><span class="w">    </span><span class="k">standard</span><span class="p">:</span><span class="w"> </span>app/phpmd.xml<span class="w">
</span><span class="w">
</span><span class="w"></span><span class="k">phpcpd</span><span class="p">:</span><span class="w">
</span><span class="w">    </span><span class="k">minLines</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span><span class="w">    </span><span class="k">minTokens</span><span class="p">:</span><span class="w"> </span><span class="m">70</span><span class="w">
</span><span class="w">
</span><span class="w"></span><span class="k">phpstan</span><span class="p">:</span><span class="w">
</span><span class="w">    </span><span class="k">level</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span><span class="w">    </span><span class="c"># https://github.com/phpstan/phpstan#configuration</span><span class="w">
</span><span class="w">    </span><span class="c"># standard: tests/.travis/phpstan.neon</span><span class="w">
</span><span class="w">
</span><span class="w"></span><span class="c"># paths are relative to .phpqa.yml, so don&#39;t copy-paste this section if you don&#39;t have custom templates</span><span class="w">
</span><span class="w"></span><span class="k">report</span><span class="p">:</span><span class="w">
</span><span class="w">    </span><span class="k">phploc</span><span class="p">:</span><span class="w"> </span>app/report/phploc.xsl<span class="w">
</span><span class="w">    </span><span class="k">phpcpd</span><span class="p">:</span><span class="w"> </span>app/report/phpcpd.xsl<span class="w">
</span><span class="w">    </span><span class="k">phpcs</span><span class="p">:</span><span class="w"> </span>app/report/phpcs.xsl<span class="w">
</span><span class="w">    </span><span class="k">pdepend</span><span class="p">:</span><span class="w"> </span>app/report/pdepend.xsl<span class="w">
</span><span class="w">    </span><span class="k">phpmd</span><span class="p">:</span><span class="w"> </span>app/report/phpmd.xsl<span class="w">
</span></code></pre></div><p>在執行 <code>phpqa</code> 時，可以用 <code>--config</code> 來指定：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ phpqa --config<span class="o">=</span>./.phpqa.yml --report
</code></pre></div><h2 id="各報表的意義">各報表的意義</h2>
<p>這裡僅簡單介紹每個報表的意義，詳細的介紹就請大家參考官方說明。</p>
<p><code>PhpMetrics report</code> 本身就包含了多種面向的分析，像是複雜度、相依性、程式碼大小等資訊，官網有介紹<a href="http://www.phpmetrics.org/documentation/how-to-read-report.html">如何解讀</a>。在 <code>Explore</code> 頁籤上可以瀏覽到所有檔案的分析指標，這些指標也可以從官方網站上找到<a href="http://www.phpmetrics.org/documentation/index.html">它們的說明</a>。</p>
<p><code>phploc report</code> 主要是列出專案的程式碼大小，但會細分出一些資訊；比較重要的資訊像是：邏輯總行數 (LLOC) 、最大類別行數 (Maximum Class Length) 、最大函式行數 (Maximum Method Length) 。通常類別行數或函式行數小表示有較佳的程式碼品質，因為小類別與小函式較容易專注在一件事上，可讀性與可維護性都會好一些。</p>
<p><code>phpcs report</code> 主要會列出每支 PHP 檔中不符合程式碼風格規範的地方，預設的程式碼規範是 PSR2 。</p>
<p><code>phpmd report</code> 會列出一些糟糕的程式碼，像是定義後卻沒有用到的變數、或是變數名稱過於簡略等。詳細的規則可以參考官方手冊的 <a href="https://phpmd.org/rules/index.html">Rules</a> 說明。</p>
<p><code>PDepend report</code> 也跟 <code>PhpMetrics</code> 一樣同時包含了多種指標，不同的是它對每個類別所相依的類別會有比較清楚的列表，而不是用連接線來表示。這裡的各項指標主要是影響類別的穩定度與耦合度，例如抽象類依賴太多實體類別，那麼耦合度就會很高；如果有太多實體類別的互動，那麼穩定度就會下降。<a href="https://www.testwo.com/blog/7640">這篇文章</a> 有更詳細的說明，值得一讀。</p>
<p><code>phpcpd report</code> 會列出有複製貼上程式碼的檔案以及重複出現的行數。 <code>phpcpd</code> 也是 <code>PHPUnit</code> 老爸的作品。</p>
<p><code>parallel-lint</code> 會列出有 PHP 語法錯誤的檔案，預設會用當下的 PHP 版本來驗證。</p>
<h2 id="總結">總結</h2>
<p>就像在平常開發時，我們會從程式碼測試覆蓋率報表中看出還有沒有值得加強測試的部份；而利用 phpqa 所分析出來的這些報表其實也是讓我們瞭解目前程式碼的現狀，進而從這些指標找出可能有問題的地方來加以重構。</p>
<p>希望這樣的介紹可以讓大家開始用這些工具來時時關心自己的程式碼品質，早一步分析出程式碼中潛藏的問題，這樣一來我們就可以把時間花在值得修改的地方。</p>
]]></content>
		</item>
		
		<item>
			<title>很值得一看的 Spotify 的工程文化</title>
			<link>https://jaceju.net/spotify-engineering-culture/</link>
			<pubDate>Tue, 27 Dec 2016 12:18:46 +0800</pubDate>
			
			<guid>https://jaceju.net/spotify-engineering-culture/</guid>
			<description>這是在網路上流傳很久的 Spotify 公司工程文化的介紹，雖然是兩年前的影片，但看完後覺得跟我們公司有很多相似的地方，而且也有值得學習的部份。因此特別把其</description>
			<content type="html"><![CDATA[<p>這是在網路上流傳很久的 Spotify 公司工程文化的介紹，雖然是兩年前的影片，但看完後覺得跟我們公司有很多相似的地方，而且也有值得學習的部份。因此特別把其重點整理並紀錄下來，以供往後組織參考與討論。</p>
<h2 id="part-1">Part 1</h2>
<!-- raw HTML omitted -->
<p>重點整理：</p>
<ul>
<li>基於敏捷的工程文化：早期引入 Scrum ，但彈性去應用這些標準。</li>
<li>拆分小隊來自主管理與執行任務，但都朝向產品目標前進。並想辦法讓目標一致性與小隊自主性都達到最大值。</li>
<li>由領導者去溝通該解決的問題與原因，由敏捷團隊共同去找出最好的解決方案。</li>
<li>不強制所有小隊標準化，讓小隊自行決定該怎麼進行任務。而小隊之間可以互相傳授技術，讓工作的進行逐漸減少阻礙。</li>
<li>將系統的功能單純化並互相獨立，並讓一個小隊負責一個系統。但注重程式碼共享，不用等到負責的小隊有空才修改。可以在修改完後由負責的小隊 code review 。</li>
<li>尊重隊友，並信任每個人的自主性，但又可以在需要協助時獲得幫忙。</li>
<li>持續改善職場環境，讓員工的滿意度達到最高。</li>
<li>用部落、分會、公會等分散式社群結構來凝聚每個員工，不讓公司階層式組織架構侷限了員工的領域發展與自主性。</li>
<li>發佈程式版本應該要容易且頻繁，所以投資在自動化測試與持續整合交付的基礎建設上。</li>
<li>發佈時應該要互不影響，所以各模組改用嵌入式網頁的區塊來獨立開發與發佈。</li>
<li>開發小隊分成：用戶 App 小隊群、功能開發小隊群、基礎建設小隊群。</li>
<li>自主服務模式是讓小隊先行啟用環境，讓其他小隊可以更容易加入功能，同時也讓交接不再被視為畏途。</li>
<li>利用功能切換模式 (Feature toggle) 來加入未完成的功能，而不是 branch 的方式來加入功能。這樣可以早期曝露整合問題或更容易地進行 A/B 測試。</li>
<li>信任比控制重要，恐懼不會抹滅信任，但會扼殺創新。如果失敗會被懲罰，人們就會不敢嘗試新的事物。</li>
</ul>
<h2 id="part-2">Part 2</h2>
<!-- raw HTML omitted -->
<p>重點整理：</p>
<ul>
<li>一個包容失敗並快速復原的環境，讓團隊能更快地犯錯，並儘快從中學習，並且改善它們。</li>
<li>不是檢討誰犯錯，而是檢討為什麼會犯這個錯，以及學到了什麼，改善了什麼。</li>
<li>持續改善流程，而不是僅僅修復產品。並且從下而上驅動，從上到下支持這個文化</li>
<li>失敗必須有爆炸範圍，不能因為一個小功能失敗而影響整個系統。每個功能都會事先釋出給小部份使用者試用，等待穩定後再逐步切換。</li>
<li>實驗階段的精實創業：先思考功能是不是用戶需要的，再建立 prototype 來讓人們試用並取得回饋；依照回饋來建立 MVP ，儘快釋出給部份用戶並且學習與改進直到達到預期成效，最後才全面釋出。</li>
<li>除非跟合作夥伴或行銷活動有關，否則就先專注於是否交付出有價值的創新功能、而後才會注重計劃性的預期成效。</li>
<li>駭客日或駭客週可以讓工程師有空做一些自己有興趣的創作或實驗，重點在有趣，而不是有用。鼓勵大家有所創新，並從中找到靈感。</li>
<li>鼓勵實驗的文化，嘗試各種可能性並比較哪種比較適合團隊或功能，讓文化的延續是基於客觀條件來決定的。</li>
<li>排斥浪費的文化，有價值的事可以持續進行，沒有價值的事就儘快拋棄，避免浪費工程師寶貴的時間。</li>
<li>大型專案通常是沒有必要的，儘可能以精簡的小專案來進行。真的有必要的大型專案就用視覺化流程與每日會議，讓每個小隊與利害關係人能協同討論合作並得到快速的回饋。</li>
<li>小而紥實的領導團隊來監看整個大型專案的方向，包含技術經理、產品經理與設計經理。</li>
<li>利用看板方法來看見專案的全貌，像是進行中的任務，或造成發展阻礙的項目。</li>
<li>用 Awesome 來定義哪些事是值得做到的方向，然後再用看板方法來專注在流程的改善與工作的追蹤。</li>
<li>透過健全的文化來快速修復有問題的流程；讓新人在一週內透過密集訓練去熟悉這些文化，並且有真正的產出。</li>
<li>用故事來流傳文化的價值，分享成功與失敗的經驗。</li>
<li>組織的文化就是成員的心態與行動的總和。當成員塑造出自我期待的行為，他們就是公司的文化。</li>
</ul>
<h2 id="心得">心得</h2>
<p>在 KKBOX 工作這麼久，雖然知道我們還有很多要改進的地方，但一直沒有很認真地思考這個問題；而兩年前的 Spotify 在敏捷開發的流程上，其實有很多值得我們團隊借鏡的地方。這點我們正在試著透過很多方式來影響團隊成員的態度，讓他們能夠逐漸接受這樣的觀念與方法，進而往這個時代應該要有的先進流程來邁進。</p>
<p>一個良好的公司文化真的要靠每個員工心裡願意去改變自己才能建立起來，一直抱持著趕快完成專案進度的心態而忘了公司真正產品的價值，是無法在這個業界繼續立足的。期盼公司高層能重視這樣的文化，並且讓每個 KKBOX 員工能以世界一流的軟體公司為榮。</p>
]]></content>
		</item>
		
		<item>
			<title>如何才有資格稱為資深工程師</title>
			<link>https://jaceju.net/be-a-senior-engineer/</link>
			<pubDate>Sat, 24 Dec 2016 00:22:01 +0800</pubDate>
			
			<guid>https://jaceju.net/be-a-senior-engineer/</guid>
			<description>看到這篇推，心中還滿有感觸的。本來就不是資歷混夠久、程式寫得好就能算得上資深工程師；資深工程師該怎麼定義其實很難拿個準，我的主管也曾問過我這</description>
			<content type="html"><![CDATA[<!-- raw HTML omitted -->
<p>看到這篇推，心中還滿有感觸的。本來就不是資歷混夠久、程式寫得好就能算得上資深工程師；資深工程師該怎麼定義其實很難拿個準，我的主管也曾問過我這個問題，我一時之間也很難完整回答。不過在面試時常常看到很多面試者都是應徵資深工程師，但實際問他們一些問題之後，卻發現他們卻缺乏了一些東西。</p>
<p>所以藉這個機會，我想從幾個面向來聊聊我心目中的工程師應該具備哪些特質與能力，而這些特質與能力，我都是從身邊厲害的同事們與社群的朋友們身上觀察到的，他們都是我所敬重的資深工程師 (當然職稱不一定是) ；同時我也會提供一些過去遇到的反指標，避免大家誤會資深工程師是否都是這樣的人。</p>
<!-- raw HTML omitted -->
<h2 id="技術能力層面">技術能力層面</h2>
<h3 id="對工具技術有深入的掌握度">對工具技術有深入的掌握度</h3>
<p>這個特點大概是一般人用來評斷資深工程師能力最明顯的表象特徵，也就是是否將常用的工具能練得很熟或是對語言理解得夠深，同時也將技術內化到自己平常的開發習慣裡，達到信手捻來的境界。這樣的資深工程師在開發上能有全面性的考量，同時也能幫助團隊更有效率地達成目標。</p>
<p>反指標：有時這個能力會以個人開發的效率來評估，使得有人常常誤認為只要能很快完成功能就有資格當資深工程師，但他們卻忽略掉其他更值得注意的能力，結果為團隊帶來災難。</p>
<h3 id="能寫出可理解可維護的程式碼">能寫出可理解可維護的程式碼</h3>
<p>這個特點的特徵就是平時就會撰寫測試、並對自己的程式碼做重構；對於自己的程式碼風格、變數或方法名稱等都非常要求，也絕對不會特意去走難懂的捷徑。這樣的資深工程師是非常自律的，所以被他 code review 時可能會有點痛苦，但絕對會學到很多。通常到這個階段的工程師，都是心靈上已經受過不少傷害，也對自己發過誓不再讓自己的程式碼傷害他人。</p>
<p>反指標：有些工程師會過度強調工具或標準帶來的規範，反而讓整個團隊疲於應付這些規則，失去了開發上的敏捷與效率。</p>
<h3 id="選擇技術的能力">選擇技術的能力</h3>
<p>雖然實務經驗豐富而能夠精準判斷是資深工程師的優勢之一，然而在面臨專案要使用的技術選項時，資深工程師不會把自己的喜好的技術強加在團隊上，而是跟團隊一起討論與研究，並在多方考量後去挑選出最適合這個專案以及這個團隊的技術。</p>
<p>反指標：有些時候老鳥會認為他所習慣的舊技術可以解決一切問題，即便這種技術維護起來不那麼容易。當專案使用新技術遇到瓶頸時，他不會伸手幫忙，反而會在旁幸災樂禍。</p>
<h3 id="軟體架構分析與設計能力">軟體架構分析與設計能力</h3>
<p>這個階段的能力靠得是非常多的實戰與專案經驗，加上對理論的融會貫通才能得到。一個優秀的資深工程師可以針對需求做出完整的分析，並從上而下、由粗到細地規劃出良好的軟體架構設計，也能在程式效率與可理解性之間取得平衡。他們對於每個技術的生態系都有一定程度的瞭解，幫助自己的規劃時能夠更快找到工具，也能避開嚴重的缺陷。</p>
<p>反指標：有些工程師可以將書中理論背得滾瓜爛熟，但在實際設計時卻沒有看清現實而規劃出不甚合理的架構，這是多數理論派工程師的通病。</p>
<h3 id="圖像解說能力">圖像解說能力</h3>
<p>能夠在解釋一個技術時，用簡單清楚的圖形來解釋原理，這點是資深工程師必備的能力。通常如果能用手繪圖形的方式來說明，表示對該技術的原理已經有足夠的瞭解。從畫出來的圖也能看出工程師對於知識的組織能力，好的圖像組識要能讓其他人一眼就能理解知識的全貌，這全仰賴工程師對該知識的理解。</p>
<p>反指標：有些工程師會只畫個方塊或圓形，然後就開始講解；而在講解過程中，動筆的次數也不多；也有些工程師只是把記憶裡的圖形畫出來，但卻說不出所以然。這些都是圖像解說能力不夠的現象。</p>
<h3 id="文件編寫能力">文件編寫能力</h3>
<p>多數的軟體工程師都討厭別人不寫文件，但討厭自己寫文件。其實寫開發文件並不是浪費時間的事情，而是可以減少其他開發者跑來打擾自己的機會。只是很多數人對於自己的文筆沒有太大的自信，把它當成不寫文件的藉口。資深工程師通常會在文件中交代這個專案的背景、架構以及如何安裝、維護等資訊，事實上他們會以未來的自己要重新接手這個專案的心情來寫，這樣就不會認為寫文件是痛苦的事了。</p>
<p>反指標：有些工程師會用自動產生器生出制式化文件後就交差了事，認為這樣就盡了編寫文件的責任。但通常這些文件僅僅包含函式和參數的名稱，沒有任何它為何存在的說明，參考的價值並不大。有時文件不見得要另外寫，註解裡也是說明為什麼的好地方。</p>
<h3 id="能綜觀全局的能力">能綜觀全局的能力</h3>
<p>在討論一個新功能，或是修改一個舊功能時，能不能考慮到所有會被影響的層面，進而判斷如何處理。另外這也考驗著資深工程師的記憶能力，因為通常這不是已經踩過無數類似的雷，就是對系統已經瞭若指掌了。資深工程師會站在俯瞰的角度來觀看整個專案全貌，並權宜各種技術面的優缺點後，提出可行的方案讓決策者參考，也讓其他執行的工程師在實作時能夠有依偱的方向。</p>
<p>反指標：有些工程師會過度憂慮未來的情況，而提出一些其實沒有明顯必要的因應措施；有時這些人是為了讓自己能佔有一席之地，所以用一些沒有根據的論點來讓自己顯得很資深。</p>
<h3 id="嘗試導入對團隊更好的流程">嘗試導入對團隊更好的流程</h3>
<p>資深工程師會從木桶定律體認到自己的效率不等於團隊的效率，所以會試著導入良好的開發流程，或是自動化佈署等讓團隊能更有效率的技術。在這期間，他會透過 code review 或是教育訓練的方式來協助伙伴們往好的方向前進，讓他們感受到好的開發流程所帶來的好處。</p>
<p>反指標：有些工程師很喜歡新技術，而沒有考慮到團隊的接受度就貿然導入；這種狀況很容易造成浪費過多人力在解決一些新技術所帶來的新麻煩，進而影響整個團隊的士氣。</p>
<h2 id="心理素質層面">心理素質層面</h2>
<h3 id="真正能完成一件事的自信">真正能完成一件事的自信</h3>
<p>這個特點並不是指自傲，而在技術層面的各項指標都達到一定水準後，資深工程師在面對任何挑戰時所產生的自信心。他能從各種面向來規劃出專案所需要的基礎建設，在開發時也能夠從架構面到程式碼都有很棒的見解與實作。在經歷過多場戰役後，這種態度通常都能贏得其他伙伴的敬重，明白只要有他在團隊裡，很多問題都能迎刃而解。</p>
<p>反指標：有些工程師會把熟練工具後就能完成的事當成自己的能力，進而產生過度的自信；一旦抽離這些工具，他們會很難找到替代方案來解決問題，進而對提出質疑的人產生防禦心態。</p>
<h3 id="不斷地自我提升">不斷地自我提升</h3>
<p>在工作之餘，資深工程師會調配自己的時間來接收新知，也會不斷地加強自己的基本能力。由於他們的基礎打得非常好，所以能對新事物有舉一反三的能力。他們對新事物接受度高，但也不會過份追逐新事物；他們會針對自己的興趣或是工作的需要來有計劃性地學習，避免把時間花在太多方向上，結果反而一事無成。</p>
<p>反指標：有些工程師會有自己的一套偏方，認為這些偏方就能幫他達到目標，不用花時間學新事物。但是累積錯誤糟糕的經驗並不能成為好的資深工程師，因為他從來不打算理解有無新的方法可以做得更好。</p>
<h3 id="臨危不亂">臨危不亂</h3>
<p>專案在進行開發或維護的過程中，常常會有一些意外發生，這常會讓一些沒有經驗的工程師慌了手腳。優秀的資深工程師會秉持著「難題不會因為你的憂心而變得簡單，不如保持平常心來看待。」的心態來面對意外。這會讓他們跳出被框住的思維，看見問題的全貌，進而找到更好的解決方案。</p>
<p>反指標：有些工程師會在問題發生時漠不關心，似乎這些問題事不關己一樣。這會發生在一些權責過度分明，犯錯就會被處罰的制度裡。</p>
<h3 id="樂於分享所知">樂於分享所知</h3>
<p>資深工程師會意識到只有技術能力高是不夠的，他們會透過演說的方式來訓練自己的口才技巧，也會透過紀錄文章的方式來加強自己的文筆能力。而藉由這些分享，除了能幫助他人少走冤枉路之外，事實上也能得到更多回饋來讓自己的視界變得更加廣闊。技術社群有很多這樣的工程師，他們會在工作之餘參加讀書會或研討會，以分享更多工作上的經驗。</p>
<p>反指標：有些工程師很喜歡分享書中的死知識，而不是自己的實務經驗談；他們分享的知識可能很有道理，但卻很難跟實際狀況有所連結。</p>
<h3 id="溝通受人敬重">溝通受人敬重</h3>
<p>在進行專案討論時，在自己有把握的部份會做出有自信的發言；而且也會尊重同事的想法，而不是用嗤之以鼻的方式來反駁。謙遜是資深工程師一個很重要的特色，他不會在不懂的地方上爭辯，而是會在時間允許的狀況下虛心請教，不然就是下次討論前先作足功課再上場。伙伴們通常都會喜歡找他討論技術，通常也不一定是因為能力落差的關係，而是跟他聊技術這件事本身就很快樂。</p>
<p>反指標：有些能力很強的工程師，會以強勢的方式來讓對方接受自己的想法。即便他是對的，但通常時間一久之後，就會破壞<del>皇城</del>團隊內的和諧。</p>
<h3 id="勇於認錯自我反省">勇於認錯、自我反省</h3>
<p>不論是線上的大問題，或是開發時的錯誤，資深工程師都會勇於認錯；多數這樣的工程師，也都是勇於承擔責任的人。他懂得承認自己的錯誤並不是可恥的事，因為他知道這是自己成長的契機；所以他會在問題被解決之後，重新省視自己做錯了什麼。這倒不是說資深工程師該把責任往自己身上攬，而是要讓團隊能夠儘快發現問題，儘快去解決。這樣的工程師通常可以在反省會議上提出不少好的見解，因為他其實就是團隊的鏡子，能幫助大家看見自己忽略的問題。</p>
<p>反指標：有些工程師會抱著「把過錯推給別人，比自己承認來得簡單。」他們可能會以職責權限來讓自己不受責難，或是毫無肩膀地把過錯直接推給資淺的同事。</p>
<h2 id="這些不是全部">這些不是全部</h2>
<p>以上這些面向並沒有完整地涵蓋一個資深工程師會有的特質與能力，我想各位可以就近觀察自己身邊的優秀同事，看看他們身上是有哪些值得你敬重的地方。當然也不是說每個資深工程師都要能做到上面這些，像我自己也常常會自省有哪些特質與能力是我還沒達到的。</p>
<p>而不論是能力或特質，其實都是相輔相乘的。在技術能力上雖然可以透過自我訓練來加強，不過心理素質就比較難在一夕之間改變了。優秀的資深工程師則會在這兩方面同時俱進，從工作上去找出讓自己的技術能力與心理素質都能提升的機會。</p>
]]></content>
		</item>
		
		<item>
			<title>在 PHPUnit 中測試需要 closure 的函式</title>
			<link>https://jaceju.net/php-closure-testing/</link>
			<pubDate>Mon, 09 Nov 2015 19:11:39 +0800</pubDate>
			
			<guid>https://jaceju.net/php-closure-testing/</guid>
			<description>不知道你有沒有在開發 PHP 程式的過程中，測試過需要使用 anonymous function 或 closure 的函式或類別方法？我在開發自己的函式庫時，就遇到了需要測試 closure 是否被正確調用的問題。</description>
			<content type="html"><![CDATA[<p>不知道你有沒有在開發 PHP 程式的過程中，測試過需要使用 anonymous function 或 closure 的函式或類別方法？我在開發自己的函式庫時，就遇到了需要測試 closure 是否被正確調用的問題。</p>
<p>在解決幾個問題後，我發現其實做法並不難，所以接下來我就來介紹幾個測試 closure 的方式。</p>
<!-- raw HTML omitted -->
<h2 id="範例">範例</h2>
<p>先來看看一個簡單的 closure 使用範例：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Example
{
    public function runClosure(Closure $closure)
    {
        $closure();
    }
}
</code></pre></div><p>在 <code>Example::runClosure</code> 方法中接受了一個 <code>$closure</code> 參數，而它的型別屬於 <code>Closure</code> 類別，使我們可以直接在程式裡用 <code>$closure()</code> 的方式來執行它的內容。</p>
<p>測試則是這樣寫的：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class ExampleTest extends PHPUnit_Framework_TestCase
{
    public function testRunClosure()
    {
        $example = new Example();

        $closure = function () {};
        $example-&gt;runClosure($closure);
    }
}
</code></pre></div><p>在測試中，我們傳入一個 anonymous function 給目標物件的 <code>runClosure</code> 方法使用。在 PHP 中， closure 和 anonymous function 其實是一樣的，它們最後都會轉化成 <code>Closure</code> 物件；這點和 JavaScript 不同，要特別注意。</p>
<p>問題來了，我們怎麼驗證 <code>$closure</code> 被呼叫了呢？</p>
<h2 id="遇到的問題">遇到的問題</h2>
<p>我第一個想法是使用 <a href="http://docs.mockery.io">Mockery</a> 來將 anonymous function 包起來，看看 PHP 底層會呼叫 closure 的哪個函式，我再做 <code>shouldReceive</code> 驗證：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$closure = Mockery::mock(function () {});
$example-&gt;runClosure($closure);
</code></pre></div><p>結果執行測試時，出現了以下錯誤訊息：</p>
<pre><code>Argument 1 passed to Example::runClosure() must be an instance of Closure, instance of Mockery_0_Closure_Closure given
</code></pre><p>這就奇怪了， Mockery 所 mock 出來的物件，類型應該是 Closure 的子類別呀？為什麼會被 type hint 打槍呢？</p>
<p>帶著疑惑，我試著直接 mock <code>Closure</code> 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$closure = Mockery::mock(Closure::class);
$example-&gt;runClosure($closure);
</code></pre></div><p>錯誤訊息變成了：</p>
<pre><code>Mockery\Exception: The class \Closure is marked final and its methods cannot be replaced. Classes marked final can be passed in to \Mockery::mock() as instantiated objects to create a partial mock, but only if the mock is not subject to type hinting checks.
</code></pre><p>原來問題就出在於 <code>Closure</code> 類別在 PHP 中是被宣告為 <code>final</code> ，也就是無法再被繼承。而 Mockery 遇到這樣的類別，<a href="http://docs.mockery.io/en/latest/reference/final_methods_classes.html">官方的建議</a>是：</p>
<blockquote>
<p>The simplest solution is not to mark classes or methods as final!</p>
</blockquote>
<p>就是不要用 <code>final</code> 啦！可是 Closure 是 PHP 的內建類別，沒辦法把 <code>final</code> 拿掉，這樣一來不就無解了？</p>
<h2 id="注入-spy-物件來驗證">注入 spy 物件來驗證</h2>
<p>其實轉個念頭，因為傳入待測程式的 closure 內容是我可以控制的，所以我不一定要去 mock closure ，而是讓它實際跑跑看，然後驗證裡面的程式碼是否有被執行就可以了。而最簡單的方法，就是插入一個 spy 物件，透過它來得知 closure 是否有被執行。</p>
<p>我在測試案例裡 mock 了 <code>stdClass</code> 這個標準類別，然後放在 <code>$spy</code> 這個變數裡；然後告訴它應該要接收到 <code>detected</code> 這個方法被執行一次的資訊。最後把這個 <code>$spy</code> 變數注入 closure 裡，在裡面執行 <code>detected</code> 方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function testRunClosure()
    {
        $spy = Mockery::mock(stdClass::class);
        $example = new Example();

        $spy-&gt;shouldReceive(&#39;detected&#39;)-&gt;once();

        $example-&gt;runClosure(function () use ($spy) {
            $spy-&gt;detected();
        });
    }
</code></pre></div><p>這樣一來就可以透過 Spy 物件來驗證 closure 是否有被執行了。</p>
<h2 id="驗證注入目標物件的-closure">驗證注入目標物件的 closure</h2>
<p>不過有時候我們會希望在 closure 裡使用目標物件，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Example
{
    public function runClosure(Closure $closure)
    {
        $closure($this);
    }
}
</code></pre></div><p>這時 closure 就可以將目標物件當做參數注入，然後再執行它的方法。例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$example = new Example();

$example-&gt;runClosure(function ($target) {
   $target-&gt;otherMethod();
});
</code></pre></div><p>但我只是要確認目標物件有被正確傳入 closure 中，所以應該要驗證目標物件的類別是 <code>Example</code> 就可以了。我們可以直接在 closure 中使用 <code>$this</code> 來呼叫驗證方法，因為這時的 <code>$this</code> 是指向測試案例的物件。所以測試就可以寫成：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">public function testRunClosure()
{
    $example = new Example();

    $example-&gt;runClosure(function ($target) {
        $this-&gt;assertInstanceOf(Example::class, $target);
    });
}
</code></pre></div><p>像這樣的場合就不需要使用 spy 物件了。</p>
<h2 id="驗證使用-bindto-的-closure">驗證使用 bindTo 的 closure</h2>
<p>如果在待測目標物件的方法裡，使用 <code>Closure::bindTo</code> 這個方法來重新定義 <code>$this</code> 時，該怎麼測試呢？例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">public function runClosure(Closure $closure)
{
    $cb = $closure-&gt;bindTo($this);
    $cb();
}
</code></pre></div><p>注意，這時候 <code>$cb</code> 並不是用注入的參數，而是使用執行時期的 context (也就是 <code>$this</code> ) 來指向目標物件；這也使得我們不能在測試中直接用 <code>$this</code> 來呼叫驗證方法，必須另尋出路。</p>
<p>所幸 PHP 的 closure 還提供了一個 <code>use</code> 的語法，讓我們可以把外部變數帶入 closure 中。但它不能直接帶入 <code>$this</code> ，所以必須換個名字。最後測試就可以改成：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">public function testRunClosure()
{
    $assert = $this;
    $example = new Example();

    $example-&gt;runClosure(function () use ($assert) {
        $assert-&gt;assertInstanceOf(Example::class, $this);
    });
}
</code></pre></div><h2 id="總結">總結</h2>
<p>closure 是在 PHP 5.3 中就引入的特性，現在越來越多函式庫與框架都已經將它納入設計時的考量了。當你有需要自己設計使用 closure 的方法時，就可以嘗試這些方法來測試 closure ：</p>
<ol>
<li>使用 anonymous function 時，使用 spy 物件來觀察。</li>
<li>當 closure 會注入目標物件時，直接驗證目標物件的類別。</li>
<li>當 closure 是透過 <code>bindTo</code> 來繫結目標物件時，用 <code>use</code> 來另外傳遞測試案例物件，以便呼叫 assertion 方法驗證。</li>
</ol>
<p>如果有更好的方法，也歡迎大家建議。</p>
]]></content>
		</item>
		
		<item>
			<title>利用 PHPUnit 與 Mink 來做 Web 測試</title>
			<link>https://jaceju.net/web-testing-with-phpunit-mink/</link>
			<pubDate>Tue, 27 Oct 2015 18:36:05 +0800</pubDate>
			
			<guid>https://jaceju.net/web-testing-with-phpunit-mink/</guid>
			<description>如果你面對的是以前舊有的 PHP 程式，是時候負起一些責任了。 我知道它改起來很痛苦，一堆不良的 PHP 程式習慣都阻礙你的修正；使得每次調整功能時，到底改得</description>
			<content type="html"><![CDATA[<p>如果你面對的是以前舊有的 PHP 程式，是時候負起一些責任了。</p>
<p>我知道它改起來很痛苦，一堆不良的 PHP 程式習慣都阻礙你的修正；使得每次調整功能時，到底改得對不對，得要等到上線才知道。想要重寫一個新版本，但太多的實作細節你不清楚；也沒有最新的規格文件，讓你無法為新版本做出功能無誤的保證。</p>
<p>現在你唯一擁有的，就是已經在線上運作的程式邏輯；雖然它可能還有 bug ，但至少大多數的功能是通過使用者驗證的。那麼先為它買個保險吧！確保之後的修改不會影響到其他功能的正常運作；而最直接的方式，就是把目前程式邏輯所呈現的結果或是使用者的操作，寫成自動化 Web 測試。</p>
<p>建立 Web 測試的方法有很多，這裡我將介紹我在實務上使用 <a href="https://phpunit.de/">PHPUnit</a> 加上 <a href="http://mink.behat.org/en/latest/">Mink</a> 搭配 <a href="http://phantomjs.org/">PhantomJS</a> 的方法。</p>
<!-- raw HTML omitted -->
<h2 id="所需工具與原理">所需工具與原理</h2>
<p>在 Web 測試中，主要分成三個部份：</p>
<ul>
<li>自動化測試框架：負責執行測試案例及驗證</li>
<li>瀏覽器控制器或模擬器：透過腳本來操作或模擬瀏覽器的行為</li>
<li>目標瀏覽器：就是我們常用的網頁瀏覽器</li>
</ul>
<p>PHPUnit 是 PHP 中最常見的自動化測試框架，要應用在舊專案中也非常輕鬆。</p>
<p>Mink 扮演的就是控制瀏覽器的角色，它可以透過不同的 <a href="http://mink.behat.org/en/latest/guides/drivers.html">Driver</a> 來控制或模擬瀏覽器。</p>
<p>而 PhantomJS 則是一個透過程式來操作的 Headless WebKit 瀏覽器；也因為它沒有視窗介面，所以啟動速度非常快，非常適合用來測試。另外它還內建 <a href="GhostDriver">GhostDriver</a> ，讓我們可以透過 <a href="https://code.google.com/p/selenium/wiki/JsonWireProtocol">WebDriver Wire Protocol</a>  來操作它。</p>
<p>所以整個 Web 測試的基礎，就是在 PHPUnit 的測試案例中，透過 Mink 的 Selenium2 Driver 來操作 PhamtomJS 。</p>
<p>接下來就進入實作吧。</p>
<h2 id="工具的安裝">工具的安裝</h2>
<p>以下介紹的安裝方式，都是在 Mac OS X 環境下完成；其他作業系統的安裝方式也差不多，這裡就不再贅述。</p>
<h3 id="安裝-phpunit-與-mink">安裝 PHPUnit 與 Mink</h3>
<p>先建立一個專案目錄，然後切換到專案目錄下，執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">composer require phpunit/phpunit behat/mink-selenium2-driver
</code></pre></div><p>這樣 Composer 會將 PHPUnit 、 Mink 及 Mink Selenium 2 Driver 安裝在 <code>vendor</code> 目錄下，並自動建立 <code>composer.json</code> 及 <code>composer.lock</code> 兩個檔案。</p>
<p>註：這裡我假設你的環境可以執行 <code>composer</code> 指令，所以也不再贅述 Composer 的安裝流程。</p>
<h3 id="安裝-phantomjs">安裝 PhantomJS</h3>
<p>接著到 <a href="http://phantomjs.org/download.html">PhantomJS 官網</a>下載 Mac OS X 專用的 ZIP 檔。然後執行：</p>
<pre><code>unzip phantomjs-2.0.0-macosx.zip
sudo mv phantomjs /usr/local/bin/
</code></pre><p>用以下指令確認有安裝完成：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">phantomjs --version
</code></pre></div><p>沒問題的話，應該會出現 <code>2.0.0</code> 。</p>
<h2 id="設定專案的-phpunit">設定專案的 PHPUnit</h2>
<p>在專案目錄下新增 <code>phpunit.xml</code> 檔，內容為：</p>
<div class="highlight"><pre class="chroma"><code class="language-xml" data-lang="xml"><span class="cp">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
<span class="nt">&lt;phpunit</span> <span class="na">backupGlobals=</span><span class="s">&#34;false&#34;</span>
         <span class="na">backupStaticAttributes=</span><span class="s">&#34;false&#34;</span>
         <span class="na">bootstrap=</span><span class="s">&#34;vendor/autoload.php&#34;</span>
         <span class="na">colors=</span><span class="s">&#34;true&#34;</span>
         <span class="na">convertErrorsToExceptions=</span><span class="s">&#34;true&#34;</span>
         <span class="na">convertNoticesToExceptions=</span><span class="s">&#34;true&#34;</span>
         <span class="na">convertWarningsToExceptions=</span><span class="s">&#34;true&#34;</span>
         <span class="na">processIsolation=</span><span class="s">&#34;false&#34;</span>
         <span class="na">stopOnFailure=</span><span class="s">&#34;false&#34;</span>
         <span class="na">syntaxCheck=</span><span class="s">&#34;false&#34;</span><span class="nt">&gt;</span>
    <span class="nt">&lt;testsuites&gt;</span>
        <span class="nt">&lt;testsuite</span> <span class="na">name=</span><span class="s">&#34;Application Test Suite&#34;</span><span class="nt">&gt;</span>
            <span class="nt">&lt;directory&gt;</span>./tests/<span class="nt">&lt;/directory&gt;</span>
        <span class="nt">&lt;/testsuite&gt;</span>
    <span class="nt">&lt;/testsuites&gt;</span>
<span class="nt">&lt;/phpunit&gt;</span>
</code></pre></div><p>執行 <code>./vendor/bin/phpunit</code> ，確認有使用這個設定檔：</p>
<pre><code>PHPUnit 5.0.8 by Sebastian Bergmann and contributors.

Time: 13 ms, Memory: 1.75Mb

No tests executed!
</code></pre><h2 id="測試實例">測試實例</h2>
<p>簡單介紹撰寫測試案例的步驟：</p>
<ol>
<li>建立一個 driver 物件，這裡是使用 <code>Selenium2Driver</code> 。</li>
<li>建立一個 session 物件，並透過上面的 driver 物件來操作瀏覽器。</li>
<li>將 session 物件連上指定網址。</li>
<li>從 session 取出 page 物件來操作頁面。</li>
<li>取出 page 物件的狀態或內容來驗證。</li>
</ol>
<p>詳細的測試寫法可以參考 <a href="http://mink.behat.org/en/latest/index.html">Mink 官方文件</a>。</p>
<p>以下我示範如何用 Google 來搜尋關鍵字，並驗證搜尋結果有包含我所預期的文字。</p>
<p>先建立 <code>tests</code> 目錄，然後新增一個 <code>tests/GoogleSearchTest.php</code> 檔，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">use</span> <span class="nx">Behat\Mink\Driver\Selenium2Driver</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Behat\Mink\Session</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">GoogleSearchTest</span> <span class="k">extends</span> <span class="nx">PHPUnit_Framework_TestCase</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">testSearchWithKeyword</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// 使用 Selenium2Driver 來操作 PhantomJS
</span><span class="c1"></span>        <span class="nv">$driver</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Selenium2Driver</span><span class="p">(</span><span class="s1">&#39;phantomjs&#39;</span><span class="p">);</span>

        <span class="c1">// 建立一個 Session 物件來控制瀏覧器
</span><span class="c1"></span>        <span class="nv">$session</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Session</span><span class="p">(</span><span class="nv">$driver</span><span class="p">);</span>
        <span class="nv">$session</span><span class="o">-&gt;</span><span class="na">start</span><span class="p">();</span>

        <span class="c1">// 瀏覽 Google 首頁
</span><span class="c1"></span>        <span class="nv">$session</span><span class="o">-&gt;</span><span class="na">visit</span><span class="p">(</span><span class="s1">&#39;https://www.google.com&#39;</span><span class="p">);</span>

        <span class="c1">// 操作頁面物件來搜尋關鍵字
</span><span class="c1"></span>        <span class="nv">$page</span> <span class="o">=</span> <span class="nv">$session</span><span class="o">-&gt;</span><span class="na">getPage</span><span class="p">();</span>
        <span class="nv">$page</span><span class="o">-&gt;</span><span class="na">fillField</span><span class="p">(</span><span class="s1">&#39;q&#39;</span><span class="p">,</span> <span class="s1">&#39;Jace Ju&#39;</span><span class="p">);</span>
        <span class="nv">$page</span><span class="o">-&gt;</span><span class="na">find</span><span class="p">(</span><span class="s1">&#39;css&#39;</span><span class="p">,</span> <span class="s1">&#39;form&#39;</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">submit</span><span class="p">();</span>

        <span class="c1">// 得到搜尋結果後驗證是否包含預期中的文字
</span><span class="c1"></span>        <span class="nv">$text</span> <span class="o">=</span> <span class="nv">$page</span><span class="o">-&gt;</span><span class="na">getText</span><span class="p">();</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertContains</span><span class="p">(</span><span class="s1">&#39;網站製作學習誌&#39;</span><span class="p">,</span> <span class="nv">$text</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><h2 id="執行測試">執行測試</h2>
<p>在執行測試之前，需要先啟動 PhantomJS 。 PhantomJS 提供一個 <code>--webdriver</code> 的選項讓它可以啟用遠端 WebDriver 模式，接收測試程式透過 WebDriver API 傳來的要求。另外因為有時測試的網址會包含 SSL ，所以要用 <code>--ssl-protocol=tlsv1</code> 及 <code>--ignore-ssl-errors=true</code> 來確保 SSL 的操作正常。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">phantomjs --webdriver<span class="o">=</span><span class="m">4444</span> --ssl-protocol<span class="o">=</span>tlsv1 --ignore-ssl-errors<span class="o">=</span><span class="nb">true</span>
</code></pre></div><p>PhantomJS 順利啟動後，就可以另開一個 terminal 視窗來進行測試了：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">./vendor/bin/phpunit
</code></pre></div><p>測試無誤的話會出現以下結果：</p>
<pre><code>PHPUnit 5.0.8 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 2.38 seconds, Memory: 4.50Mb

OK (1 test, 1 assertion)
</code></pre><h2 id="page-objects-模式">Page Objects 模式</h2>
<p>上面的例子中有個問題：當頁面功能沒有更動，但是 UI 改變時 (例如 DOM 元素或 id/class 名稱) ，我們就必須去更改測試案例的程式碼；而如果同樣的功能在多個測試案例中出現，那麼要改的地方就更多了。所以在實務中，我們會將頁面的功能行為與 UI 細節分離開來，以解決 UI 細節重複的問題；為了這個目標，我們引入 <a href="https://code.google.com/p/selenium/wiki/PageObjects">Page Objects</a> 這個模式。</p>
<p>要特別注意的是， Page Objects 模式和 Mink 的 page 物件是兩件事。 Page Objects 模式主要是透過 API 描述頁面的行為，並封裝 UI 細節；而 Mink 的 page 物件則實際上是一個 <code>DocumentElement</code> 物件，主要是用來操作頁面上的元素。換句話說，在 Page Objects 模式中，頁面類別所封裝的 UI 細節，就是用 <code>DocumentElement</code> 物件來操作的。</p>
<h3 id="實作-page-objects-模式">實作 Page Objects 模式</h3>
<p>雖然 Page Objects 模式可以自行實作，但為了省下一些自行撰寫的時間，我特地寫了一個 <a href="https://github.com/jaceju/mink-page-objects">goez/mink-page-objects</a> 供大家使用。</p>
<p>首先在專案目錄下執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ composer require goez/mink-page-objects --dev
</code></pre></div><p>建立一個 <code>tests/bootstrap.php</code> ，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="sd">/** @var Composer\Autoload\ClassLoader $loader */</span>
<span class="nv">$loader</span> <span class="o">=</span> <span class="k">require</span> <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/../vendor/autoload.php&#39;</span><span class="p">;</span>
<span class="nv">$loader</span><span class="o">-&gt;</span><span class="na">addPsr4</span><span class="p">(</span><span class="s1">&#39;Google\\&#39;</span><span class="p">,</span> <span class="no">__DIR__</span> <span class="o">.</span> <span class="s1">&#39;/Google/&#39;</span><span class="p">);</span>
</code></pre></div><p>將 <code>phpunit.xml</code> 中的 <code>vendor/autoload.php</code> ，改為 <code>tests/bootstrap.php</code> 。</p>
<h3 id="將頁面細節封裝在頁面行為功能裡">將頁面細節封裝在頁面行為功能裡</h3>
<p>接下來先建立 <code>tests/Google/Home.php</code> 檔；這是 Google 首頁類別，它繼承抽象的 <code>Page</code> 類別，並提供一個 <code>search</code> 方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">namespace</span> <span class="nx">Google</span><span class="p">;</span>

<span class="k">use</span> <span class="nx">Goez\PageObjects\Page</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">Home</span> <span class="k">extends</span> <span class="nx">Page</span>
<span class="p">{</span>
    <span class="k">protected</span> <span class="nv">$parts</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s1">&#39;SearchForm&#39;</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="s1">&#39;css&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;form&#39;</span><span class="p">],</span>
    <span class="p">];</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">search</span><span class="p">(</span><span class="nv">$keyword</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">getPart</span><span class="p">(</span><span class="nx">SearchForm</span><span class="o">::</span><span class="na">class</span><span class="p">)</span>
            <span class="o">-&gt;</span><span class="na">search</span><span class="p">(</span><span class="nv">$keyword</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>接下來我建立一個 <code>tests/Google/SearchForm.php</code> ，它主要是封裝搜尋的操作細節：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">namespace</span> <span class="nx">Google</span><span class="p">;</span>

<span class="k">use</span> <span class="nx">Goez\PageObjects\Part</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">SearchForm</span> <span class="k">extends</span> <span class="nx">Part</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * @param $keyword
</span><span class="sd">     * @return SearchResult
</span><span class="sd">     * @throws \Behat\Mink\Exception\ElementNotFoundException
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">search</span><span class="p">(</span><span class="nv">$keyword</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">element</span><span class="o">-&gt;</span><span class="na">fillField</span><span class="p">(</span><span class="s1">&#39;q&#39;</span><span class="p">,</span> <span class="nv">$keyword</span><span class="p">);</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">element</span><span class="o">-&gt;</span><span class="na">submit</span><span class="p">();</span>

        <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">createPage</span><span class="p">(</span><span class="nx">SearchResult</span><span class="o">::</span><span class="na">class</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>這裡，我把原來輸入關鍵字並送出表單的 UI 操作，封裝在 <code>search</code> 方法中，並回傳一個搜尋結果頁面物件。</p>
<p>再建立 <code>tests/Google/SearchResult.php</code> 檔，它主要是封裝搜尋結果頁。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">namespace</span> <span class="nx">Google</span><span class="p">;</span>

<span class="k">use</span> <span class="nx">Goez\PageObjects\Page</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">SearchResult</span> <span class="k">extends</span> <span class="nx">Page</span>
<span class="p">{</span>

<span class="p">}</span>
</code></pre></div><p>最後就可以把原來的測試案例改用新的頁面類別來重寫了：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">use</span> <span class="nx">Behat\Mink\Driver\Selenium2Driver</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Behat\Mink\Session</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Goez\PageObjects\Context</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Goez\PageObjects\Helper\PhantomJSRunner</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Google\Home</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">GoogleSearchTest</span> <span class="k">extends</span> <span class="nx">PHPUnit_Framework_TestCase</span>
<span class="p">{</span>
    <span class="c1">// 自動啟動 phantomjs
</span><span class="c1"></span>    <span class="k">use</span> <span class="nx">PhantomJSRunner</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">testSearchWithKeyword</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nv">$driver</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Selenium2Driver</span><span class="p">(</span><span class="s1">&#39;phantomjs&#39;</span><span class="p">);</span>

        <span class="nv">$session</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Session</span><span class="p">(</span><span class="nv">$driver</span><span class="p">);</span>
        <span class="nv">$session</span><span class="o">-&gt;</span><span class="na">start</span><span class="p">();</span>

        <span class="nv">$context</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Context</span><span class="p">(</span><span class="nv">$session</span><span class="p">,</span> <span class="p">[</span>
            <span class="s1">&#39;baseUrl&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;https://www.google.com&#39;</span><span class="p">,</span>
        <span class="p">]);</span>

        <span class="nv">$context</span><span class="o">-&gt;</span><span class="na">createPage</span><span class="p">(</span><span class="nx">Home</span><span class="o">::</span><span class="na">class</span><span class="p">)</span>
            <span class="o">-&gt;</span><span class="na">open</span><span class="p">()</span>
            <span class="o">-&gt;</span><span class="na">search</span><span class="p">(</span><span class="s1">&#39;Jace Ju&#39;</span><span class="p">)</span>
            <span class="o">-&gt;</span><span class="na">shouldContainText</span><span class="p">(</span><span class="s1">&#39;網站製作學習誌&#39;</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>這麼一來，在測試案例中就可以清楚地用頁面物件的行為去描述實際的需求，而不是落在操作 UI 的思維裡。讓外部的測試案例可以用更語意化的方式來使用這個類別，是一種 <code>Tell Don't Ask</code> 的實現。</p>
<h2 id="總結">總結</h2>
<p>雖然舊專案可能難以做到單元測試，但我們可以先利用 Web 測試來驗證它已經存在的行為；而在 Web 測試中可以透過程式來控制瀏覽器，達到自動化測試的目的。在撰寫測試案例時，最重要的是對需求的描述，而不是 UI 操作的細節；因此可以用 Page Objects 模式來封裝 UI 細節，讓頁面物件提供有語意化的行為操作方式。</p>
<p>希望這個介紹能幫助大家對 Web 測試有基本的瞭解，當然在實務上可能會遇到的問題會更複雜；有機會的話我會另文分享自己在實務上遇到的問題，也歡迎大家提供不同的見解。</p>
]]></content>
		</item>
		
		<item>
			<title>邁向 PHP 重構之路 - 以 Laravel 程式碼片段為例</title>
			<link>https://jaceju.net/simple-refatoring-example-01/</link>
			<pubDate>Mon, 05 Oct 2015 12:53:48 +0800</pubDate>
			
			<guid>https://jaceju.net/simple-refatoring-example-01/</guid>
			<description>來上 TDD 課的學員問到一個 Laravel 程式碼重構的問題，這裡簡單地做分享。未來如果有好的實戰範例，這系列就會延續下去。 開始重構 當然重構前，我們必須先有測試</description>
			<content type="html"><![CDATA[<p>來上 TDD 課的學員問到一個 Laravel 程式碼重構的問題，這裡簡單地做分享。未來如果有好的實戰範例，這系列就會延續下去。</p>
<!-- raw HTML omitted -->
<h2 id="開始重構">開始重構</h2>
<p>當然重構前，我們必須先有測試做保障。在每個步驟完成後，我們都應該確保修改後的程式碼能通過測試的驗證。</p>
<p>接下來開始重構，這是原本的程式碼：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// Step 0
if ($errorRedirectViewType == &#39;create&#39;) {
    return Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType)
        -&gt;with(&#39;message&#39;, &#39;一樣的 message&#39;)
        -&gt;withInput($allInput);
} else {
    return Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType, [&#39;id&#39; =&gt; $allInput[&#39;id&#39;]])
        -&gt;with(&#39;message&#39;, &#39;一樣的 message&#39;)
        -&gt;withInput($allInput);
}
</code></pre></div><p>第一步我們引入一個 <code>$redirect</code> 變數：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// Step 1
$redirect = null;
if ($errorRedirectViewType == &#39;create&#39;) {
    $redirect = Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType)
        -&gt;with(&#39;message&#39;, &#39;一樣的 message&#39;)
        -&gt;withInput($allInput);
} else {
    $redirect = Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType, [&#39;id&#39; =&gt; $allInput[&#39;id&#39;]])
        -&gt;with(&#39;message&#39;, &#39;一樣的 message&#39;)
        -&gt;withInput($allInput);
}
return $redirect;
</code></pre></div><p>第二步我們把共用的部份移出 <code>if...else</code> 外：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// Step 2
$redirect = null;
if ($errorRedirectViewType == &#39;create&#39;) {
    $redirect = Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType);
} else {
    $redirect = Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType, [&#39;id&#39; =&gt; $allInput[&#39;id&#39;]]);
}
return $redirect
        -&gt;with(&#39;message&#39;, &#39;一樣的 message&#39;)
        -&gt;withInput($allInput);
</code></pre></div><p>第三步把 <code>if...else</code> 提煉成 <code>redirectByViewType</code> 方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// Step 3
// Extracted method
protected function redirectByViewType($errorRedirectViewType, $id)
{
    $redirect = null;
    if ($errorRedirectViewType == &#39;create&#39;) {
        $redirect = Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType);
    } else {
        $redirect = Redirect::route(self::__module . &#39;.&#39; . self::__function . &#39;.&#39; . $errorRedirectViewType, [&#39;id&#39; =&gt; $id]);
    }
    return $redirect;
}
</code></pre></div><p>然後改用新的 <code>redirectByViewType</code> 方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// Step 3
$redirect = $this-&gt;redirectByViewType($errorRedirectViewType, $allInput[&#39;id&#39;]);

return $redirect
        -&gt;with(&#39;message&#39;, &#39;一樣的 message&#39;)
        -&gt;withInput($allInput);
</code></pre></div><p>至於第二步到第三步要不要做，就看我們有沒有 reuse 這段邏輯的需求；但一般我會做，因為程式碼看起來好讀，後面也可以再做其他重構。</p>
<p>第四步就可以把原來的 <code>$redirect</code> 拿掉，因為不需要了。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// Step 4
return $this-&gt;redirectByViewType($errorRedirectViewType, $allInput[&#39;id&#39;])
        -&gt;with(&#39;message&#39;, &#39;一樣的 message&#39;)
        -&gt;withInput($allInput);
</code></pre></div><p>這種先引入一個臨時變數讓重構好進行的方式，是很常見的。而什麼時候應該需要使用這個技巧？這就要多累積經驗。通常你可以想像一下重構後的程式碼，大致與重構前會有什麼樣的差異，再判斷是否需要引用一個臨時變數。</p>
<p>第五步，我們把 <code>redirectByViewType</code> 重複的程式碼再引用一個解釋用的變數 <code>$routeName</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// Step 5
protected function redirectByViewType($errorRedirectViewType, $id)
{
    $redirect = null;
    $routeName = self::__module . &#39;.&#39; .
               self::__function . &#39;.&#39; .
               $errorRedirectViewType;

    if ($errorRedirectViewType == &#39;create&#39;) {
        $redirect = Redirect::route($routeName);
    } else {
        $redirect = Redirect::route($routeName, [&#39;id&#39; =&gt; $id]);
    }
    return $redirect;
}
</code></pre></div><p>這樣程式碼就更容易被理解了。</p>
<h2 id="後記">後記</h2>
<p>希望這個小例子可以讓大家瞭解到，實戰中的重構其實是很簡單的。它就是在不更改原有邏輯的狀態下，一步一步讓你的程式碼變得更易讀也更易維護。</p>
<p>當初因為是臨時示範給學員看，所以並沒有特別加上測試，也沒有用 PhpStorm 來協助重構；結果後來我發現在 extract method 時，忘了把 <code>$allInput['id']</code> 帶到 <code>redirectByViewType</code> 裡面。</p>
<p>這就是一種工程師很容易忽略的盲點，就是太容易相信自己的想法，而不是真正去驗證它。在沒有測試和工具的輔助下，千萬要特別小心這種小錯誤。</p>
]]></content>
		</item>
		
		<item>
			<title>在 Laravel 上用 MailCatcher 發送測試信件</title>
			<link>https://jaceju.net/laravel-mailcatcher/</link>
			<pubDate>Wed, 29 Jul 2015 15:22:29 +0800</pubDate>
			
			<guid>https://jaceju.net/laravel-mailcatcher/</guid>
			<description>雖然 Laravel 在寄送測試信件上提供了 Mailgun 這個服務的串接方式，不過如果能夠在 Homestead 就可以直接測試是更棒的選擇；而 MailCatcher 剛好就提供這樣的功能，它能啟動一個 SMTP 模擬服</description>
			<content type="html"><![CDATA[<p>雖然 Laravel 在寄送測試信件上提供了 <a href="http://www.mailgun.com/">Mailgun</a> 這個服務的串接方式，不過如果能夠在 Homestead 就可以直接測試是更棒的選擇；而 <a href="http://mailcatcher.me/">MailCatcher</a> 剛好就提供這樣的功能，它能啟動一個 SMTP 模擬服務，並且讓我們透過 Web 介面來查看信件是否有被發送出來。</p>
<blockquote>
<p>註：類似的工類還有用 Go 寫的 <a href="https://github.com/mailhog/MailHog">MailHog</a> ，據說速度更快；雖然我還沒試過，但我想用法應該是相同的。</p>
</blockquote>
<p>以下就簡單介紹一下如何在 Laravel 5.1 上使用 MailCatcher 。</p>
<!-- raw HTML omitted -->
<h2 id="安裝-mailcatcher">安裝 MailCatcher</h2>
<p>這裡我們用 Homestead 示範，先把 Homestead 的 port 1080/1025 導到本機的 port 1080/1025 ，方便稍後在本機操作。執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ homestead edit
</code></pre></div><blockquote>
<p>註： <code>$ </code>  為提示字元，不需要輸入。</p>
</blockquote>
<p>然後加入：</p>
<div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="k">ports</span><span class="p">:</span><span class="w">
</span><span class="w">    </span>- <span class="k">send</span><span class="p">:</span><span class="w"> </span><span class="m">1080</span><span class="w">
</span><span class="w">      </span><span class="k">to</span><span class="p">:</span><span class="w"> </span><span class="m">1080</span><span class="w">
</span><span class="w">    </span>- <span class="k">send</span><span class="p">:</span><span class="w"> </span><span class="m">1025</span><span class="w">
</span><span class="w">      </span><span class="k">to</span><span class="p">:</span><span class="w"> </span><span class="m">1025</span><span class="w">
</span></code></pre></div><p>接著進入 Homestead ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ homestead up
$ homestead ssh
</code></pre></div><p>確認 Ruby 環境是安裝好的：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ ruby -v
ruby 2.1.2p95 <span class="o">(</span>2014-05-08<span class="o">)</span> <span class="o">[</span>x86_64-linux-gnu<span class="o">]</span>

$ gem -v
2.2.2
</code></pre></div><p>然後安裝 MailCatcher ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ sudo gem install mailcatcher --no-ri --no-rdoc
</code></pre></div><p>安裝完成後，直接執行 <code>mailcatcher</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ mailcatcher --ip 0.0.0.0
Starting <span class="nv">MailCatcher</span>
<span class="o">==</span>&gt; smtp://0.0.0.0:1025
<span class="o">==</span>&gt; http://0.0.0.0:1080
*** MailCatcher runs as a daemon by default. Go to the web interface to quit.
</code></pre></div><p>這樣 MailCatcher 的 port 1025 就會監聽 SMTP 請求，而 port 1080 就會是它的 Web 管理介面。</p>
<p>打開本機的瀏覽器，瀏覽 <code>http://127.0.0.1:1080</code> ，應該就會看到 MailCatcher 的 Web 管理介面：</p>
<p><img src="/resources/laravel-mailcatcher/mailcatcher-web-ui.png" alt="MailCatcher Web UI"></p>
<h2 id="修改-laravel-mail-設定">修改 Laravel Mail 設定</h2>
<p>這裡我假設你已經建立一個 Laravel 5.1 專案了，所以修改 <code>.env</code> 中的 <code>MAIL_HOST</code> 與 <code>MAIL_PORT</code> 即可：</p>
<div class="highlight"><pre class="chroma"><code class="language-ini" data-lang="ini"><span class="na">MAIL_HOST</span><span class="o">=</span><span class="s">127.0.0.1</span>
<span class="na">MAIL_PORT</span><span class="o">=</span><span class="s">1025</span>
</code></pre></div><p>其他一切都可以不需要改動。</p>
<h2 id="測試發送信件">測試發送信件</h2>
<p>我們可以直接用 tinker 來測試是否能夠發送信件，執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ php artisan tinker
Psy Shell v0.5.2 <span class="o">(</span>PHP 5.6.8 — cli<span class="o">)</span> by Justin Hileman
&gt;&gt;&gt;
</code></pre></div><p>然後輸入：</p>
<pre><code>Mail::raw('This is a test mail', function ($message) {
$message-&gt;subject('Test');
$message-&gt;from('laravel@example.com', 'Laravel');
$message-&gt;to('user@example.com');
});
</code></pre><p>結果應該會回傳 <code>1</code> ，然後你可以回到瀏覽器查看 MailCatcher 是否有收到這封信，結果應該會如下圖所示：</p>
<p><img src="/resources/laravel-mailcatcher/mailcatcher-result.png" alt="MailCatcher Result"></p>
<h2 id="與其他測試框架的整合">與其他測試框架的整合</h2>
<p>MailCatcher 可以跟 PHP 的自動化測試框架做很好的整合，詳情可以參考以下文章：</p>
<ul>
<li>PHPUnit - <a href="http://codeception.com/12-15-2013/testing-emails-in-php.html">Testing Emails in PHP. Part 1: PHPUnit</a></li>
<li>Behat - <a href="https://github.com/kibao/behat-mailcatcher-extension">MailCatcher extension for Behat</a></li>
<li>Codeception - <a href="https://github.com/captbaritone/codeception-mailcatcher-module">Test emails in your Codeception acceptance tests</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>Laravel 5.1 Events Broadcasting 實務練習</title>
			<link>https://jaceju.net/laravel-events-broadcasting/</link>
			<pubDate>Sun, 26 Jul 2015 00:38:37 +0800</pubDate>
			
			<guid>https://jaceju.net/laravel-events-broadcasting/</guid>
			<description>Laravel 5.1 提供了一個非常棒的 Events Broadcasting 特色，它能讓開發者建立一個 RealTime Web App 。作者 Taylor 也錄製了一個 Events Broadcasting 的教學影片，讓開發者可以更快瞭解這個新功能。 教學影片中雖然</description>
			<content type="html"><![CDATA[<p>Laravel 5.1 提供了一個非常棒的 Events Broadcasting 特色，它能讓開發者建立一個 RealTime Web App 。作者 Taylor 也錄製了一個 Events Broadcasting 的<a href="https://laracasts.com/lessons/broadcasting-events-in-laravel-5-1">教學影片</a>，讓開發者可以更快瞭解這個新功能。</p>
<p>教學影片中雖然是使用 <a href="https://pusher.com/">Pusher</a> 服務來做事件推送，不過 Laravel 也可以搭配 <a href="http://redis.io/">Redis</a> 來做到同樣的事情。考量到未來的系統發展，我打算採用 Redis 來當做事件推送伺服器，所以本文也會在此基礎進行說明。</p>
<p>以下就來介紹如何用 Laravel 的 Events Broadcasting 來實作一個簡單的聊天室。</p>
<!-- raw HTML omitted -->
<h2 id="原理">原理</h2>
<p>簡單說明一下本文的實作原理：</p>
<ol>
<li>啟動 Redis 伺服器來監聽 Laravel 發送出來的事件。</li>
<li>透過 Node Express 建立一個 Socket.IO Server ，並且接收 Redis Server 推送過來的事件，然後將它廣播到 WebSocket 上。</li>
<li>瀏覽器上建立與 Socket.IO Server 的連結，透過 WebSocket 接收事件來完成即時互動。</li>
</ol>
<h2 id="開發環境">開發環境</h2>
<p>Laravel 官方推薦開發者使用 Homestead ，原因是它已經幫我們安裝好所有 Laravel 需要的執行環境，例如 Redis 與 Node.js 。在繼續下去之前，請先依照<a href="http://laravel.com/docs/5.1/homestead">官方說明</a> (<a href="http://laravel.tw/docs/5.1/homestead">中文版</a>) 將 Homestead 安裝好。</p>
<p>然後利用 <code>homestead edit</code> 打開 Homestead 的設定檔：</p>
<blockquote>
<p>註：以下指令中，開頭的 <code>$ </code> 為系統提示符號，不用輸入。</p>
</blockquote>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ homestead edit
</code></pre></div><p>確認本機路徑 <code>~/Projects</code> 有正確 mount 到 Homestead 的 <code>/home/vagrant/Projects</code> 上。</p>
<div class="highlight"><pre class="chroma"><code class="language-yaml" data-lang="yaml"><span class="k">folders</span><span class="p">:</span><span class="w">
</span><span class="w">    </span>- <span class="k">map</span><span class="p">:</span><span class="w"> </span>~/Projects<span class="w">
</span><span class="w">      </span><span class="k">to</span><span class="p">:</span><span class="w"> </span>/home/vagrant/Projects<span class="w">
</span></code></pre></div><p>要連上 Homestead 裡的虛擬站台 (Virtual Host) ，必須要讓本機認得專案對應的 hostname 。所以要編輯本機的 <code>/etc/hosts</code> ，加入 IP 與 hostname 的對應：</p>
<div class="highlight"><pre class="chroma"><code class="language-text" data-lang="text">192.168.10.10 chat-room.app
</code></pre></div><p><code>192.168.10.10</code> 是在 Homestead.yml 中設定的 IP 。</p>
<p>啟動 Homestead ，並用 ssh 連入 Homestead ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ homestead up
$ homestead ssh
</code></pre></div><p>檢查 node.js 版本：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ node -v
v1.8.1
</code></pre></div><blockquote>
<p>註： Homestead 是透過 nvm 安裝 io.js ，所以可以自行升級 io.js 到最新版。</p>
</blockquote>
<p>檢查 redis 是否啟動：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ redis-cli ping
PONG
</code></pre></div><p>新增一個虛擬站台：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ serve chat-room.app ~/Projects/chat-room/public
</code></pre></div><h2 id="建立應用程式">建立應用程式</h2>
<p>在 Homestead 中，利用 composer 來下載已經設定好的 Laravel 5.1 Boilerplate ，執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ <span class="nb">cd</span> ~/Projects
$ composer create-project jaceju/b5 chat-room -s dev
</code></pre></div><p>程式就會開始下載並進行安裝。完成後進入專案資料夾，以便進行後續操作。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ <span class="nb">cd</span> chat-room
</code></pre></div><h3 id="調整-gulpfilejs">調整 gulpfile.js</h3>
<p>在 Homestead 上開發時，不需要啟動 Web Server ，因此要調整 <code>gulpfile.js</code> 。</p>
<p>編輯 <code>gulpfile.js</code> ，把 <code>port</code> 變數與 <code>serve</code> task 移除，並將 <code>proxy</code> 改到 <code>chat-room.app</code> ，完成後如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="c1">// ... (略)
</span><span class="c1"></span>
<span class="nx">elixir</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">mix</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">mix</span><span class="p">.</span><span class="nx">clean</span><span class="p">()</span>
        <span class="p">.</span><span class="nx">sass</span><span class="p">(</span><span class="s1">&#39;*.scss&#39;</span><span class="p">)</span>
        <span class="p">.</span><span class="nx">wiredep</span><span class="p">()</span>
        <span class="p">.</span><span class="nx">jshint</span><span class="p">()</span>
        <span class="p">.</span><span class="nx">sync</span><span class="p">(</span><span class="s1">&#39;resources/assets/js/**/*.js&#39;</span><span class="p">,</span> <span class="s1">&#39;public/js&#39;</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="nx">elixir</span><span class="p">.</span><span class="nx">config</span><span class="p">.</span><span class="nx">production</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">mix</span><span class="p">.</span><span class="nx">useref</span><span class="p">({</span> <span class="nx">src</span><span class="o">:</span> <span class="kc">false</span> <span class="p">})</span>
            <span class="p">.</span><span class="nx">version</span><span class="p">([</span><span class="s1">&#39;js/*.js&#39;</span><span class="p">,</span> <span class="s1">&#39;css/*.css&#39;</span><span class="p">])</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div><h2 id="安裝必要套件">安裝必要套件</h2>
<p>Laravel 操作 Redis 是透過 <a href="https://github.com/nrk/predis">Predis</a> 套件，所以我們要透過 composer 安裝：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ composer require predis/predis
</code></pre></div><p>接下來要透過 Npm 與 Bower 安裝 Socket.IO Server 相關套件，包含前後端：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ npm install express ioredis socket.io --save
$ bower install socket.io-client --save
</code></pre></div><h2 id="建立-socketio-server">建立 Socket.IO Server</h2>
<p>接下來要利用 Node.js 的 express 和 http 模組建立一個 Web Server ，然後讓 Socket.IO 透過這個 WebServer 來廣播從 Redis 接收到的事件。先建立 <code>socket.js</code> ，內容為：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;express&#39;</span><span class="p">)();</span>
<span class="kd">var</span> <span class="nx">http</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;http&#39;</span><span class="p">).</span><span class="nx">Server</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">io</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;socket.io&#39;</span><span class="p">)(</span><span class="nx">http</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">Redis</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;ioredis&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">redis</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Redis</span><span class="p">();</span>

<span class="c1">// Redis 訂閱 `chat-channel` 頻道
</span><span class="c1"></span><span class="nx">redis</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="s1">&#39;chat-channel&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">count</span><span class="p">)</span> <span class="p">{</span>
<span class="p">});</span>

<span class="c1">// 當 Redis 有事件發生時，透過 Socket.IO Server 發送事件
</span><span class="c1"></span><span class="nx">redis</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;message&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">channel</span><span class="p">,</span> <span class="nx">message</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">message</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
    <span class="nx">io</span><span class="p">.</span><span class="nx">emit</span><span class="p">(</span><span class="nx">channel</span> <span class="o">+</span> <span class="s1">&#39;:&#39;</span> <span class="o">+</span> <span class="nx">message</span><span class="p">.</span><span class="nx">event</span><span class="p">,</span> <span class="nx">message</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
<span class="p">});</span>

<span class="c1">// 讓用戶端可以透過 Port 3000 連接 Socket.IO Server
</span><span class="c1"></span><span class="nx">http</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">3000</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Listening on Port 3000&#39;</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div><p>然後在 Homestead 上啟動 Socket.IO Server ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ node socket.js <span class="p">&amp;</span>
Listening on Port <span class="m">3000</span>
</code></pre></div><h2 id="修改設定">修改設定</h2>
<p>Laravel 5.1 預設是使用 Pusher 做為事件推送伺服器，可以在 <code>config/broadcasting.php</code> 裡看到這個設定：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">&#39;default&#39; =&gt; env(&#39;BROADCAST_DRIVER&#39;, &#39;pusher&#39;),
</code></pre></div><p>因為這裡使用了 <code>env</code> 函式，所以我們可以編輯 <code>.env</code> ，加入以下設定來改用 Redis 伺服器：</p>
<div class="highlight"><pre class="chroma"><code class="language-ini" data-lang="ini"><span class="na">BROADCAST_DRIVER</span><span class="o">=</span><span class="s">redis</span>
</code></pre></div><h2 id="建立-event-類別">建立 Event 類別</h2>
<p>接下來就要讓 Laravel 能夠發送事件了，在 Laravel 5.0 以後的版本提供了 <code>make:event</code> 這個指令可以協助我們建立 Event 類別。首先我們的聊天室需要一個「訊息被建立」的事件，所以執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ php artisan make:event MessageCreated
</code></pre></div><p>這樣就會建立 <code>app/Events/MessageCreated.php</code> 。</p>
<p>接著我們要讓 <code>MessageCreated</code> 類別能夠被 Redis 推送，所以編輯 <code>app/Events/MessageCreated.php</code> ，讓它實作 <code>Illuminate\Contracts\Broadcasting\ShouldBroadcast</code> 介面。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class MessageCreated extends Event implements ShouldBroadcast
</code></pre></div><p>在推送事件時，我們可以附帶一組要傳送的資料，稱為 payload 。通常我們會在 Event 類別的建構子中帶入 payload 。修改 <code>MessageCreated</code> 類別，加入 <code>$username</code> 與 <code>$message</code> 屬性，並在建構子中注入：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    private $username;

    private $message;

    public function __construct($username, $message)
    {
        $this-&gt;username = $username;
        $this-&gt;message = $message;
    }
</code></pre></div><p>接著我們要讓它能在推送事件時，把這兩個屬性一起傳送出去；主要是透過 <code>broadcastWith</code> 這個方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function broadcastWith()
    {
        return [
            &#39;username&#39; =&gt; $this-&gt;username,
            &#39;message&#39; =&gt; $this-&gt;message,
        ];
    }
</code></pre></div><p>最後我們需要一個頻道來廣播事件，這是因為要讓 Redis 可以識別要廣播的對象。我們可以在 <code>broadcastOn</code> 方法回傳 Redis 訂閱的頻道名稱，即為前面指定的 <code>chat-channel</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function broadcastOn()
    {
        return [&#39;chat-channel&#39;];
    }
</code></pre></div><blockquote>
<p>註：一個事件可以廣播給數個頻道，所以這裡要回傳一個陣列。</p>
</blockquote>
<p>完成後的程式碼如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>

<span class="k">namespace</span> <span class="nx">App\Events</span><span class="p">;</span>

<span class="k">use</span> <span class="nx">Illuminate\Contracts\Broadcasting\ShouldBroadcast</span><span class="p">;</span>
<span class="k">use</span> <span class="nx">Illuminate\Queue\SerializesModels</span><span class="p">;</span>

<span class="k">class</span> <span class="nc">MessageCreated</span> <span class="k">extends</span> <span class="nx">Event</span> <span class="k">implements</span> <span class="nx">ShouldBroadcast</span>
<span class="p">{</span>
    <span class="k">use</span> <span class="nx">SerializesModels</span><span class="p">;</span>

    <span class="k">private</span> <span class="nv">$username</span><span class="p">;</span>

    <span class="k">private</span> <span class="nv">$message</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span><span class="nv">$username</span><span class="p">,</span> <span class="nv">$message</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">username</span> <span class="o">=</span> <span class="nv">$username</span><span class="p">;</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">message</span> <span class="o">=</span> <span class="nv">$message</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">broadcastWith</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span>
            <span class="s1">&#39;username&#39;</span> <span class="o">=&gt;</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">username</span><span class="p">,</span>
            <span class="s1">&#39;message&#39;</span> <span class="o">=&gt;</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">message</span><span class="p">,</span>
        <span class="p">];</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">broadcastOn</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="s1">&#39;chat-channel&#39;</span><span class="p">];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><h2 id="建立-routes-與-controller">建立 Routes 與 Controller</h2>
<p>這裡我們只需要兩個 route ：聊天室頁面，以及發送訊息。改寫 <code>app/Http/routes.php</code> ，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="s1">&#39;ChatController@index&#39;</span><span class="p">);</span> <span class="c1">// 聊天室頁面
</span><span class="c1"></span><span class="nx">post</span><span class="p">(</span><span class="s1">&#39;send-message&#39;</span><span class="p">,</span> <span class="s1">&#39;ChatController@sendMessage&#39;</span><span class="p">);</span> <span class="c1">// 發送訊息
</span></code></pre></div><p>然後要建立 <code>ChatController</code> 來處理程式流程，在 Terminal 中執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ php artisan make:controller ChatController --plain
</code></pre></div><p>編輯新建立的 <code>app/Http/Controllers/ChatController.php</code> ，先加入 <code>index</code> 方法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function index()
    {
        srand(time()); // 亂數種子
        $username = sprintf(&#39;user%06d&#39;, rand(1, 100000)); // 決定 user 名稱 (註)
        return view(&#39;chat&#39;, compact(&#39;username&#39;));
    }
</code></pre></div><p>在 <code>index</code> 方法中，主要是產生一個隨機的使用者名稱，並顯示在首頁樣版裡。</p>
<blockquote>
<p>註：這裡產生 <code>username</code> 的方法並不嚴謹，沒有考慮到名稱重複的問題，但現階段先暫時這樣。</p>
</blockquote>
<p>接下來我們要接收使用者建立的訊息，然後發送一個「訊息被建立」的事件，所以新增 <code>sendMessage</code> 方法，內容為：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function sendMessage(Request $request)
    {
        $username = $request-&gt;get(&#39;username&#39;);
        $message = $request-&gt;get(&#39;message&#39;);
        event(new MessageCreated($username, $message));
        return &#39;message sent&#39;;
    }
</code></pre></div><h2 id="建立樣版頁面">建立樣版頁面</h2>
<p>切換到前端開發模式，我們要修改一下介面的呈現。</p>
<p>先處理 HTML 的部份，將原來的 <code>resources/views/welcome.blade.php</code> 重新命名為 <code>resources/views/chat.blade.php</code> ，將 <code>div.container</code> 的內容修改如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;container&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;row&#34;</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;col-md-6 col-md-offset-3&#34;</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Chat Room Demo<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>

            <span class="c">&lt;!-- 訊息列表框 --&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;chat-room&#34;</span><span class="p">&gt;</span>

            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

            <span class="c">&lt;!-- 輸入訊息的表單 --&gt;</span>
            <span class="p">&lt;</span><span class="nt">form</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;send-message&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;post&#34;</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;/send-message&#34;</span><span class="p">&gt;</span>
                {!! csrf_field() !!}
                <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;username&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{{ $username }}&#34;</span> <span class="p">/&gt;</span>
                <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;input-group&#34;</span><span class="p">&gt;</span>
                    <span class="p">&lt;</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;input-group-addon&#34;</span><span class="p">&gt;</span>{{ $username }}<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
                    <span class="p">&lt;</span><span class="nt">input</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;message&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;form-control&#34;</span> <span class="p">/&gt;</span>
                    <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;input-group-btn&#34;</span><span class="p">&gt;</span>
                        <span class="p">&lt;</span><span class="nt">button</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;btn btn-success&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;send&#34;</span><span class="p">&gt;</span>Send<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
                    <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
                <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p>這樣會讓畫面上有一個訊息列表框以及一個輸入訊息的表單，如下圖所示。</p>
<p><img src="/resources/chat-room-demo/interface.png" alt="介面"></p>
<p>接下來稍微調整介面的樣式，編輯 <code>resources/assets/sass/app.scss</code> ，將內容修改如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-scss" data-lang="scss"><span class="k">@import</span> <span class="s2">&#34;../../../public/bower_components/bootstrap-sass/assets/stylesheets/bootstrap&#34;</span><span class="p">;</span>

<span class="nt">html</span><span class="o">,</span> <span class="nt">body</span> <span class="p">{</span>
  <span class="nt">height</span><span class="nd">:</span> <span class="nt">100</span><span class="err">%</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// 訊息列表框
</span><span class="c1"></span><span class="nn">#chat-room</span> <span class="p">{</span>
  <span class="nt">border</span><span class="nd">:</span> <span class="nt">1px</span> <span class="nt">solid</span> <span class="nn">#ccc</span><span class="p">;</span>
  <span class="nt">height</span><span class="nd">:</span> <span class="nt">20rem</span><span class="p">;</span>
  <span class="nt">padding</span><span class="nd">:</span> <span class="nt">1rem</span><span class="p">;</span>
  <span class="nt">overflow-x</span><span class="nd">:</span> <span class="nt">hidden</span><span class="p">;</span>
  <span class="nt">overflow-y</span><span class="nd">:</span> <span class="nt">auto</span><span class="p">;</span>

  <span class="c1">// 單則訊息
</span><span class="c1"></span>  <span class="nc">.message</span> <span class="p">{</span>
    <span class="nt">padding</span><span class="nd">:</span> <span class="nt">1rem</span><span class="p">;</span>
    <span class="nt">margin-bottom</span><span class="nd">:</span> <span class="nt">1rem</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 輸入訊息的表單
</span><span class="c1"></span><span class="nn">#send-message</span> <span class="p">{</span>
  <span class="nt">margin-top</span><span class="nd">:</span> <span class="nt">-1px</span><span class="p">;</span>

  <span class="nc">.input-group-addon</span> <span class="p">{</span>
    <span class="nt">border-top-left-radius</span><span class="nd">:</span> <span class="nt">0</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nc">.input-group-btn</span> <span class="o">&gt;</span> <span class="nc">.btn</span> <span class="p">{</span>
    <span class="nt">border-top-right-radius</span><span class="nd">:</span> <span class="nt">0</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>最後透過 JavaScript 讓所有東西串在一起，</p>
<p>編輯 <code>resources/assets/js/app.js</code> ，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="s1">&#39;use strict&#39;</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">$chatRoom</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;#chat-room&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">$sendMessage</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;#send-message&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">$messageInput</span> <span class="o">=</span> <span class="nx">$sendMessage</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">&#39;input[name=message]&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">io</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">io</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">socket</span> <span class="o">=</span> <span class="nx">io</span><span class="p">(</span><span class="s1">&#39;http://chat-room.app:3000&#39;</span><span class="p">);</span>

<span class="c1">// 當送出表單時，改用 Ajax 傳送，並清空輸入框。
</span><span class="c1"></span><span class="nx">$sendMessage</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;submit&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">action</span><span class="p">,</span> <span class="nx">$sendMessage</span><span class="p">.</span><span class="nx">serialize</span><span class="p">());</span>
    <span class="nx">$messageInput</span><span class="p">.</span><span class="nx">val</span><span class="p">(</span><span class="s1">&#39;&#39;</span><span class="p">);</span>
    <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
<span class="p">});</span>

<span class="c1">// 當接收到訊息建立的事件時，將接收到的 payload
</span><span class="c1"></span><span class="nx">socket</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;chat-channel:App\\Events\\MessageCreated&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">payload</span><span class="p">)</span> <span class="p">{</span>

    <span class="kd">var</span> <span class="nx">html</span> <span class="o">=</span> <span class="s1">&#39;&lt;div class=&#34;message alert-info&#34; style=&#34;display: none;&#34;&gt;&#39;</span><span class="p">;</span>
    <span class="nx">html</span> <span class="o">+=</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">username</span> <span class="o">+</span> <span class="s1">&#39;: &#39;</span><span class="p">;</span>
    <span class="nx">html</span> <span class="o">+=</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">message</span><span class="p">;</span>
    <span class="nx">html</span> <span class="o">+=</span> <span class="s1">&#39;&lt;/div&gt;&#39;</span><span class="p">;</span>

    <span class="kd">var</span> <span class="nx">$message</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="nx">html</span><span class="p">);</span>
    <span class="nx">$chatRoom</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="nx">$message</span><span class="p">);</span>
    <span class="nx">$message</span><span class="p">.</span><span class="nx">fadeIn</span><span class="p">(</span><span class="s1">&#39;fast&#39;</span><span class="p">);</span>
    <span class="nx">$chatRoom</span><span class="p">.</span><span class="nx">animate</span><span class="p">({</span><span class="nx">scrollTop</span><span class="o">:</span> <span class="nx">$chatRoom</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">scrollHeight</span><span class="p">},</span> <span class="mi">1000</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div><h2 id="執行測試">執行測試</h2>
<p>在 Homestead 上執行：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">$ gulp
</code></pre></div><p>然後在本機分別開啟兩個瀏覽器視窗瀏覽 <code>http://chat-room.app</code> ，然後在輸出框上輸入文字後按 <code>Send</code> ，應該就會讓兩個瀏覽器同時出現相同的文字。</p>
<p><img src="/resources/chat-room-demo/in-use.png" alt="展示"></p>
<p>完成的範例可以在我的 <a href="https://github.com/jaceju/example-laravel-chat-room">GitHub</a> 上找到。</p>
<h2 id="結論">結論</h2>
<p>這個 Demo 如果真的要在實務上使用，還有很多地方要考慮，例如訊息歷史、使用者登入等等。不過這已經足夠讓我們瞭解 Laravel 5.1 在實作 Broadcasting 時有多麼輕鬆，使得我們更容易在專案前期就先實現很多想法。</p>
<p>希望這個簡單的教學，能對大家使用 Laravel 來開發即時系統時有所幫助。</p>
<h2 id="參考">參考</h2>
<ul>
<li><a href="https://laracasts.com/discuss/channels/general-discussion/step-by-step-guide-to-installing-socketio-and-broadcasting-events-with-laravel-51">Step by Step Guide to Installing Socket.io and Broadcasting Events with Laravel 5.1 </a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>Laravel 5.1 正式釋出</title>
			<link>https://jaceju.net/laravel-5-1/</link>
			<pubDate>Wed, 10 Jun 2015 00:34:37 +0800</pubDate>
			
			<guid>https://jaceju.net/laravel-5-1/</guid>
			<description>經過了幾個月的等待， Laravel 5.1 終於在美國時間 6/9 正式釋出了。同時 Laracasts 也推出了一系列的 Laravel 5.1 新功能介紹，絕對是每位 Artisan 必看的影片。 官方也介紹了 5.1 有哪些新特色</description>
			<content type="html"><![CDATA[<p>經過了幾個月的等待， Laravel 5.1 終於在美國時間 6/9 <a href="https://laravel-news.com/2015/06/laravel-5-1-released/">正式釋出</a>了。同時 Laracasts 也推出了一系列的 <a href="https://laracasts.com/series/whats-new-in-laravel-5-1">Laravel 5.1 新功能介紹</a>，絕對是每位 Artisan 必看的影片。</p>
<p>官方也介紹了 5.1 有哪些<a href="http://laravel.com/docs/5.1/releases">新特色</a> ，以下我會簡單介紹它們。</p>
<!-- raw HTML omitted -->
<h2 id="不再支援-php-559-以前的版本">不再支援 PHP 5.5.9 以前的版本</h2>
<p>從推出以來，我個人認為 Laravel 的作者似乎都一直在依循著 PHP 的新特色來開發 Laravel ，讓整個 Framework 不論在程式架構或效能上，都能保持在一定的水準。這次 5.1 版也是如此，直接就告訴大家：該把 PHP 升級到 5.5.9 以上的版本了，這樣才能使用新版的 Laravel 所帶來的好處。</p>
<p>很難想像這樣先進的 PHP Framework 的開發作者，原來是寫 .Net 的；而在開發 Laravel 之前完全沒學過 PHP ，真是令我汗顏。</p>
<h2 id="第一個-lts-版本">第一個 LTS 版本</h2>
<p>當然隨著 PHP 持續演進的過程中，勢必就得有所取捨；這樣的特色讓許多依賴舊的 Laravel 版本所開發的專案，在穩定與升級之間面臨很大的抉擇。到了 Laravel 5.1 ，作者終於正式宣佈它是一個 LTS (Long Term Support) 版本，將會讓企業安心用它來開發需要穩定的長期專案。</p>
<p>也就是說現在用 Laravel 5.1 開發的專案，在未來的兩、三年內可以不用再怕沒人修核心的 bug 了。</p>
<h2 id="新的文件">新的文件</h2>
<p>在 5.1 將要發佈之前，作者花了很多心力在完善文件。例如 <a href="http://laravel.com/docs/5.1/authentication">Authentication</a> 的部份就做了很完整的 Quickstart 範例，讓開發者瞭解如何去實作自己的認證機制。另外即時搜尋也是這次文件系統的新特色，讓開發者可以很快地用關鍵字來查閱相關說明。</p>
<p>不過我還是覺得字太小，而且每次要看其他章節都得捲回去頁首。</p>
<h2 id="psr-2">PSR-2</h2>
<p>在 Laravel 5.1 之前的版本，讓我最困擾的就是作者自己弄了一套 Coding Style ，而不是照著 PSR 標準走。終於在眾開發者的要求下，作者從善如流，讓核心代碼以及產生器所產生的程式碼，都遵守了 PSR-2 標準。</p>
<p>這個結果，我只能拍手叫好了；因為即便 PSR-2 再怎麼不如己意，但它終歸是大家討論出來的標準。 (公司的 coding style 也不正是如此嗎？)</p>
<ul>
<li>相關影片： <a href="https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/1">Adopting PSR-2</a></li>
</ul>
<h2 id="在樣版中注入服務">在樣版中注入服務</h2>
<p>在 Blade 引擎中也新增了一個 <code>@inject</code> 指令，讓開發者可以即時地注入一個物件。</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html">@inject(&#39;metrics&#39;, &#39;App\Services\MetricsService&#39;)

<span class="p">&lt;</span><span class="nt">div</span><span class="p">&gt;</span>
    Monthly Revenue: {{ $metrics-&gt;monthlyRevenue() }}.
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p>這樣一來應該就可以不必在 Controller 或 View Composer 中引入變數了。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/blade#service-injection">Blade Templates - Service Injection</a></li>
<li>相關影片： <a href="https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/2">Injecting Services With Blade</a></li>
</ul>
<h2 id="middleware-參數">Middleware 參數</h2>
<p>原本我們只能從傳入 request 物件給 middleware ，再從 request 物件中取得參數給 middleware 使用；在新版本終於可以自訂 middleware 的參數，再從 route 的定義裡傳入參數給 middleware 。</p>
<p>例如在製作權限系統時，就可以在 route 傳入指定的角色，讓管理權限的 middleware 判斷使用者是否符合這個角色。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class RoleMiddleware
{
    public function handle($request, Closure $next, $role)
    {
        if (! $request-&gt;user()-&gt;hasRole($role)) {
            // Redirect...
        }

        return $next($request);
    }
}

Route::put(&#39;post/{id}&#39;, [&#39;middleware&#39; =&gt; &#39;role:editor&#39;, function ($id) {
    //
}]);
</code></pre></div><p>另外 Middleware 的設計也因為底層的 Symfony 採用了剛定案的 PSR-7 標準，所以也符合 PSR-7 的標準。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/middleware#middleware-parameters">HTTP Middleware - Middleware Parameters</a></li>
</ul>
<h2 id="事件廣播">事件廣播</h2>
<p>Laravel 原本已經包含了一個強大的事件系統，現在加上實作更簡單的事件廣播功能，能讓開發者搭配特定的服務 (例如 Redis) 以廣播 WebSocket 事件，便可以更有效率地做出即時應用程式。</p>
<p>原本我打算用 <a href="http://reactphp.org/">ReactPHP</a> 的說，看來又要改變主意了。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/events#broadcasting-events">Events - Broadcasting Events</a></li>
<li>相關影片： <a href="https://laracasts.com/series/intermediate-laravel/episodes/3">The Power of Eventing</a> (需付費)</li>
</ul>
<h2 id="更完整的測試框架">更完整的測試框架</h2>
<p>先前的版本所提供的測試工具提供的功能有限，僅能做到部份的整合測試。現在 Laravel 5.1 引入了 laracasts 的測試套件，讓整合測試變得更易讀。</p>
<p>我才剛把 <a href="http://codeception.com/">Codeception</a> 引入而已，這套要不要用可能要再研究一下。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/testing">Testing</a></li>
</ul>
<h2 id="小結">小結</h2>
<p>據作者說， Laravel 5.1 是他目前最滿意的版本；當然這或許有些老王賣瓜的感覺，但從 Laravel 3 追到現在，我也覺得 Laravel 真的是與時俱進，不斷地結合許多先進的開發觀念。</p>
<p>接下來我會把一些目前正在執行的專案都換成 5.1 ，再來看看它是不是真的有如作者所說的這麼強大。</p>
]]></content>
		</item>
		
		<item>
			<title>在 Mac OS X 上搭建 Selenium 測試環境</title>
			<link>https://jaceju.net/selenium-on-mac/</link>
			<pubDate>Wed, 03 Jun 2015 10:21:45 +0800</pubDate>
			
			<guid>https://jaceju.net/selenium-on-mac/</guid>
			<description>在開發網站的過程中，因為需要測試介面以模擬使用者的操作，最理想的工具就是 Selenium 了。 現在我是用 Mac 來當做開發環境，所以簡單記錄一下如何在 OS X 上建立 Selenium</description>
			<content type="html"><![CDATA[<p>在開發網站的過程中，因為需要測試介面以模擬使用者的操作，最理想的工具就是 <a href="http://www.seleniumhq.org/">Selenium</a> 了。</p>
<p>現在我是用 Mac 來當做開發環境，所以簡單記錄一下如何在 OS X 上建立 Selenium 測試環境。</p>
<p>Selenium 的簡單原理與應用可以參考：<a href="http://jaceju.net/2015/05/23/skilltree-tdd-2/">自動測試與 TDD 實務開發 - 上課心得 (中)</a></p>
<!-- raw HTML omitted -->
<h2 id="準備工作">準備工作</h2>
<ol>
<li>確認可以執行 Java ，因為 Selenium Server 需要用 Java 執行；如果沒有 Java 的話，可以去 Oracle 官方網站<a href="http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html">下載</a> 後安裝。</li>
<li>確認可以執行 <a href="http://brew.sh/">homebrew</a> ，因為稍後有幾個套件會使用它來安裝。</li>
<li>在適合的位置建立一個新資料夾，例如 <code>~/Selenium</code> ，接下來的工作都會在這裡進行。</li>
<li>最後在 Terminal 中下載我寫好的 script 並執行它，它會下載 selenium server 並安裝 ChromeDriver 及建立 Firefox Profile。</li>
</ol>
<p>完整的指令如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">mkdir -p ~/Selenium <span class="o">&amp;&amp;</span> <span class="nb">cd</span> <span class="nv">$_</span>
curl -S -s -L https://goo.gl/s519kT &gt; run-selenium
chmod +x run-selenium <span class="o">&amp;&amp;</span> mv run-selenium /usr/local/bin
run-selenium init
</code></pre></div><h2 id="設定瀏覽器">設定瀏覽器</h2>
<p>先確認好已經安裝 <a href="http://mozilla.com.tw/">Firefox</a> 、 <a href="https://www.google.com.tw/chrome/">Google Chrome</a> 等瀏覽器， Safari 則已內建。</p>
<h3 id="google-chrome">Google Chrome</h3>
<p><a href="https://sites.google.com/a/chromium.org/chromedriver/">ChromeDriver</a> 可以讓 Selenium Server 呼叫 Google Chrome 執行；如果前面已經執行過 <code>./run-selenium init</code> 的話，就已經安裝好了。可以在 Terminal 執行 <code>chromedriver -v</code> 來驗證是否正確安裝。</p>
<h3 id="safari">Safari</h3>
<p><a href="https://github.com/SeleniumHQ/selenium/wiki/SafariDriver">SafariDriver</a> 則可以讓 Selenium Server 呼叫 Safari 執行，它是一個 Safari Extension ，必須手動安裝。</p>
<ol>
<li>在 Selenium 官網<a href="http://www.seleniumhq.org/download/">下載頁</a>找到 <code>SafariDriver</code> ，下載 <code>Latest release</code> 連結的 <code>SafariDriver.safariextz</code> 檔。</li>
<li>用滑鼠雙擊 <code>SafariDriver.safariextz</code> 檔， Safari 會提示是否安裝 <code>WebDriver</code> ，選「安裝」。</li>
<li>開啟 Safari ，在「偏好設定」裡面切換到「延伸功能」頁籤。</li>
<li>在「延伸功能」頁籤畫面上，應該就會有 <code>WebDriver</code> ，確認它有被啟用。</li>
<li><strong>最後在「安全性」頁籤畫面上，將「阻擋彈出式視窗」取消勾選，避免阻擋測試程式執行。</strong></li>
</ol>
<h2 id="啟用並停用-selenium-server">啟用並停用 Selenium Server</h2>
<p>在要測試之前，啟用 Selenium Server ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">run-selenium start
</code></pre></div><p>要結束 Selenium Server 則是：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">run-selenium stop
</code></pre></div><h2 id="測試用範例">測試用範例</h2>
<p>這邊已經有個我寫好的<a href="https://github.com/jaceju/selenium-demo">範例</a>，它整合了以下幾個 Selenium Server 的用法：</p>
<ul>
<li><a href="https://github.com/giorgiosironi/phpunit-selenium">PHPUnit Selenium</a></li>
<li><a href="https://github.com/instaclick/php-webdriver">PHP WebDriver</a></li>
<li><a href="http://codeception.com/docs/modules/WebDriver">Codeception WebDriver Module</a></li>
</ul>
<p>用法請參考 <a href="https://github.com/jaceju/selenium-demo/blob/master/README.md">README</a> 說明。</p>
<h2 id="參考">參考</h2>
<ul>
<li><a href="http://www.hashbangcode.com/blog/automating-headless-selenium-phpunit-tests">Automating Headless Selenium PHPUnit Tests</a></li>
<li><a href="http://elementalselenium.com/tips/69-safari">Elemental Selenium - How To Use Safari</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>自動測試與 TDD 實務開發 - 上課心得 (下)</title>
			<link>https://jaceju.net/skilltree-tdd-3/</link>
			<pubDate>Sun, 31 May 2015 10:10:10 +0800</pubDate>
			
			<guid>https://jaceju.net/skilltree-tdd-3/</guid>
			<description>第三週是這門課程的最後一堂課，上完課的我心裡其實有很大的衝擊，一直不知道該怎麼整理這最後的心得；就好像美妙的音樂感動了你的心靈，但自己一時之</description>
			<content type="html"><![CDATA[<p>第三週是這門課程的最後一堂課，上完課的我心裡其實有很大的衝擊，一直不知道該怎麼整理這最後的心得；就好像美妙的音樂感動了你的心靈，但自己一時之間卻很難重現那樣的弦律。</p>
<p>前兩週講師介紹了<a href="http://jaceju.net/2015/05/17/skilltree-tdd/">單元測試基礎</a>以及<a href="http://jaceju.net/2015/05/23/skilltree-tdd-2/">如何重構舊有程式</a>之後，但我們依舊面臨了一個很大的問題，那就是：「我們寫出來的程式，不見得就是我們所想的；而就算符合我們想的，也不見得是需求要的。」</p>
<pre><code>「嘿！你沒睡好嗎？上星期提的搜尋功能你應該完成了吧？」
「嗯，我昨天加班完成了，還加上了測試，應該很完美，你試試。」

不久後...

「怎麼我搜尋『特價』這個字，沒有出現特價商品的類別頁？」
「不是吧？搜尋結果應該是獨立的搜尋列表呀！這不是 common sense 嗎？」
「不對不對，你應該在我搜尋『特價』的時候，直接導到特價商品頁就好了。」
「靠！這兩回事吧？而且你給的的需求只有搜尋商品而已，剩下的要我自己腦補喔？」
</code></pre><p>如果這種戲碼常常在辦公室上演的話，浪費的可不只是開發人員的時間呀！面對不清不楚的需求，或是雙方對需求的認知上有所誤解，甚至開發人員「善意地」加入根本不在需求裡的程式碼，這些問題都將可能拖垮整個專案的進度！</p>
<p>所以最後一週，講師為我們帶來了整套課程的高潮：不僅僅是以規格來完成程式碼，更能<strong>用規格來自動驗證你的程式碼！</strong></p>
<!-- raw HTML omitted -->
<h2 id="測試驅動了開發那什麼驅動了測試">測試驅動了開發，那什麼驅動了測試？</h2>
<p>自從大師們對 TDD 進行一場論戰後，很多人發現自己誤解了 TDD 的真義，那就是：所謂的測試驅動從來不是為了「讓系統因為有寫測試而顯得專業」或是「儘可能寫出完美而獨立的測試程式」，而是「從有目標的測試中來完成程式碼」。</p>
<p>TDD 雖然能驅動著開發者專注在測試所在乎的目標上，讓程式碼不會有過度設計的問題；但當目標錯誤的話，即便測試寫得再完整也是白搭。然而測試的目標到底是什麼呢？想當然爾就是我們的需求規格；不過這兩者之間還是存在著巨大的門檻，所以我們要面對的問題是：到底要怎麼讓需求轉換成測試的目標？而且隨著需求不斷地成長，也要能驅動我們的測試持續演進？</p>
<h2 id="需求該如何描述">需求該如何描述？</h2>
<p>「網站這麼大，使用者很難一下子就找到他們要的商品，我們是不是應該有個搜尋商品的功能？」當客戶提出這樣的需求時，他們通常不明白也不需要明白程式是怎麼做的 (他們只知道這樣的描述可以讓工程師累垮) 。為了避免客戶與工程師之間有認知上的落差，專案負責人 (PO, Product Owner) 可能就會想辦法生出一大堆文件，來描述詳細的系統規格。</p>
<p>然而我們都知道在大環境的變化下，需求可能會被改得面目全非，先前寫好的文件已然成為一堆廢紙。為了不要大家做白工，敏捷開發提出了 User Stories 這個方式，讓真正有價值的需求能被先簡約地描述出來，而不是一開始就陷在文件地獄裡。</p>
<p>通常一個 User Story 可以用以下的格式來描述：</p>
<pre><code>As a &lt;role&gt;, I want to &lt;action&gt; because of &lt;business value&gt;.
(「某個角色」可以「做某件事」來得到「有商業價值的結果」)
</code></pre><p>例如：</p>
<pre><code>「使用者」可以「輸入關鍵字」來「找出站內的商品」
</code></pre><p>這樣就是一個 User Story ，它明白地指出需求的目標是什麼，而不會包含技術細節。基本上，我們可以把 User Story 想成是用來確認產品功能的大綱，實作細節可以在之後補上，就不會因為需求的變化而浪費大家的時間。更詳細的 User Story 介紹可以參考以下文章：</p>
<ul>
<li><a href="https://ihower.tw/blog/archives/2090">User Stories (1) 什麼是 User Story? by ihower</a> (結果這系列好像富奸了&hellip;)</li>
<li><a href="http://kojenchieh.pixnet.net/blog/post/386322818">撰寫使用者故事常見的問題 by David Ko</a></li>
</ul>
<p>當然只靠 User Story 的描述對開發者來說是不友善的，所以我們需要靠一些方法讓 User Story 和我們的程式有所繫結；也就是說我們要從 User Story 中找出更詳細的規格，進而整合到前面 TDD 的測試目標裡。當通過了以規格所建立出來的測試，我們的程式碼也才真正地符合需求。</p>
<p>為了能達到開發、測試與需求三位一體的目標，所以 <a href="http://goo.gl/gyuAWY">BDD - Behavior Driven Development (行為驅動開發)</a> 就誕生了。</p>
<h2 id="告訴我這個功能會怎麼被使用">告訴我，這個功能會怎麼被使用</h2>
<p>BDD 定義了需求方如何撰寫 User Story ，以及開發人員如何把 User Story 轉換成測試；所以 BDD 的重點並不是測試，而是在定義需求的規格。 BDD 在不同的程式語言中都有實作，例如：</p>
<ul>
<li><a href="http://jbehave.org/">JBehave</a> - Java 上的 BDD 框架，同時也是最早的 BDD 框架。</li>
<li><a href="http://rspec.info/">RSpec</a> - Ruby 上的 BDD 框架，使用 ruby 來直接描述規格。</li>
<li><a href="http://www.phpspec.net/">PHPSpec</a> - PHP 上的 BDD 框架，使用 php 來直接描述規格。</li>
<li><a href="https://cucumber.io/">Cucumber</a> - 一個用 Ruby 寫的 BDD 框架，後來因為推出 Cucumber-JVM 後，讓其他 JVM-based 語言也可以使用。它的特色是用文字格式的規格檔案來執行測試，後來就變成了一個業界非成文的標準，後來有<a href="https://cucumber.io/docs#cucumber-implementations">很多 BDD 框架</a>就參考它的運作方式來實作了。</li>
<li><a href="http://www.specflow.org/">SpecFlow</a> - .Net 上的 Cucumber 實作，同時也是這次上課所使用的框架。</li>
<li><a href="http://behat.org/">Behat</a> - PHP 上的 Cucumber 實作。</li>
</ul>
<p>Cucumber 利用 <a href="https://github.com/cucumber/cucumber/wiki/Gherkin">Gherkin</a> 語法來描述需求規格，所以前面的 User Story 就會變成一個 feature 檔：</p>
<pre><code># features/products_searching

Feature: 使用者可以輸入關鍵字來找出站內的商品
    In order to 找出指定的商品
    As a 使用者
    I want to 輸入關鍵字
</code></pre><ul>
<li>註：這裡我不會用課程裡的範例，一是我希望自己是真的理解了 BDD ，所以自己試著如何去應用；二是課程裡的範例更加有挑戰性，我不想破了講師的哏。</li>
</ul>
<p>接著要定義出這個需求的使用場景，也就是更明確地說明這個需求的功能「要怎麼被使用」，我們稱為 Scenario 。</p>
<pre><code>Scenario: 搜尋 &quot;iphone&quot; ，找出的商品包含了 5 個名稱中符合 &quot;iphone&quot; 的商品
    Given 在搜尋輸入框中輸入 &quot;iphone&quot;
    When 我按下搜尋按鈕
    Then 在搜尋頁得到 5 個名稱中符合 &quot;iphone&quot; 的商品

Scenario: 搜尋 &quot;特價&quot; ，會導向特價商品活動頁
    Given 在搜尋輸入框中輸入 &quot;特價&quot;
    When 我按下搜尋按鈕
    Then 導向特價商品活動頁
</code></pre><p>每個 Scenario 都是一條完整的功能執行路徑，只要有路徑分叉 (例如 <code>if</code> 判斷) 的話，就要有不同的 Scenario 。其中 Given-When-Then 就是用來描述一個 Scenario 的三要角，它們組合起來的意思是：</p>
<pre><code>假定 (Given) 在某個條件下，當 (When) 我做了某個動作，然後 (Then) 就會發生什麼結果。
</code></pre><p>是不是有種既視感？沒錯！ Given-When-Then 剛好對應到 3A 原則的： Arrange-Act-Assert ，所以 Scenario 可以被轉換成測試！</p>
<h3 id="我想用中文">我想用中文</h3>
<p>前面在 feature 檔案保留英文單字，是為了讓 Gherkin 的語法解釋器能夠辨別；如果不喜歡這種寫法，也可以用中文，只要在檔案開頭加上 <code># language: zh-TW</code> 就可以。</p>
<p>然後原來的 feature 檔就可以改成：</p>
<pre><code># features/product_searching.feature
# language: zh-TW

功能: 使用者可以輸入關鍵字來找出站內的商品
    為了 找出指定的商品
    身為 &quot;使用者&quot;  # 註：這裡以「者」結尾的話在 behat 裡會變亂碼，所以特別處理
    我要 輸入關鍵字

場景: 搜尋 &quot;iphone&quot; ，找出的商品包含了 5 個名稱中符合 &quot;iphone&quot; 的商品
    假定 在搜尋輸入框中輸入 &quot;iphone&quot;
    當 我按下搜尋按鈕
    那麼 在搜尋頁得到 5 個名稱中符合 &quot;iphone&quot; 的商品

場景: 搜尋 &quot;特價&quot; ，會導向特價商品活動頁
    假定 在搜尋輸入框中輸入 &quot;特價&quot;
    當 我按下搜尋按鈕
    那麼 導向特價商品活動頁
</code></pre><p>註：這裡因為我是用 Behat ，所以關鍵字的部份是採用 Behat 的語法 (可以用 <code>behat --story-syntax --lang zh-TW</code> 看到範例) 。</p>
<h2 id="讓測試程式跟著需求跑">讓測試程式跟著需求跑</h2>
<p>課程裡是使用 SpecFlow ，這裡我改用 Behat 來練習。先在專案裡面初始化 Behat 的執行環境：</p>
<pre><code>behat --init
</code></pre><p>它會產生一個 <code>features/bootstrap/FeatureContext.php</code> ，而它就是連繫 feature 和測試的關鍵。</p>
<p>建立一個 <code>features/products_searching.feature</code> 檔，然後把前面定義的 featrue 內容貼上去，接著再執行 <code>behat</code> 指令就會得到以下輸出內容：</p>
<p><img src="/resources/skilltree-tdd/behat-01.png" alt="Behat 執行結果"></p>
<p>可以看到 Behat 把 feature 檔裡的 Given-When-Then 都變成了方法，它們稱為 Step Definition 。 Step Definition 是可以被重複使用的，所以可以看到兩個 Scenario 共用了其中兩個方法。不過因為 PHP 不支援用中文當做方法名稱，因此 Behat 幫我們用很奇怪的拼音組成方法名稱。</p>
<p>把 Behat 產生的方法複製到 <code>FeatureContext.php</code> 裡，然後把方法名稱改成易懂的英文，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// ... 略 ...
class FeatureContext implements Context, SnippetAcceptingContext
{
    /**
     * @Given 在搜尋輸入框中輸入 :arg1
     */
    public function typingInSearchField($arg1)
    {
        throw new PendingException();
    }

    /**
     * @When 我按下搜尋按鈕
     */
    public function iEnterSearchButton()
    {
        throw new PendingException();
    }

    /**
     * @Then 在搜尋頁得到 :arg2 個名稱中符合 :arg1 的商品
     */
    public function getProductsThatIncludeInNameOnResultPage($arg1, $arg2)
    {
        throw new PendingException();
    }

    /**
     * @Then 導向特價商品活動頁
     */
    public function redirectToPageOfSpecialOffer()
    {
        throw new PendingException();
    }
}
</code></pre></div><p>接下來再執行一次 <code>behat</code> ，就會看到 Behat 要我們一步一步完成測試碼：</p>
<p><img src="/resources/skilltree-tdd/behat-02.png" alt="Behat 執行結果"></p>
<p>這實在是太酷了！現在我們已經讓測試跟規格文件繫結在一起，想要增加需求或修改需求，就是更改測試步驟。而且 Scenario 之間通常只是一兩個步驟的變化，所有已經測試過的步驟都可以重覆利用；而我們要做的就是隨著增長的規格，補上新增的 Step Definition 就可以！</p>
<p>至於怎麼寫測試呢？其實就和單元測試很像，只不過換了一種寫法，前兩週學的都可以應用在這裡面！這樣的開發方式真叫人欲罷不能！</p>
<p>註：心得不打算介紹太多實作，有機會我再補充 Behat 的做法。</p>
<h2 id="不再是對立而是能一起驗收需求">不再是對立，而是能一起驗收需求</h2>
<p>定義好了 feature 檔，我們只要把一個個的 Scenario 完成；就像雕刻一樣，一刀一刀切，最後就會看到完整的成品。而 PO 驗收專案時只需要對照 feature 所對應的測試是否為綠燈，只要通過了，也就是符合需求！這樣一來辦公室就再也不會有劍拔弩張的氣氛了！</p>
<p>當然一開始不見得會這麼順利，身為 BDD 的導入者，或許我們需要花更多耐心來引導 PO 協助我們去撰寫 Scenario ；當他漸漸瞭解這樣做的好處時，就會知道用這個方式能讓需求更快被滿足。未來只要整個團隊認同並遵循這樣的做法，我相信 BDD 一定能為專案帶來莫大的益處。</p>
<h2 id="讓文件活下去">讓文件活下去</h2>
<p>基本上， 多數的 Developer 通常很討厭：</p>
<ul>
<li>寫文件</li>
<li>寫註解</li>
<li>寫測試</li>
</ul>
<p>但是更討厭：</p>
<ul>
<li>別的 Developer 都不寫文件</li>
<li>別的 Developer 都不寫註解</li>
<li>別的 Developer 都不寫測試</li>
</ul>
<p>現在有了 feature 檔，也就同時有了文件和測試，更酷的是這兩者是隨著程式碼一起成長的！</p>
<p>而這門課最有價值的一節，就是教你如何把 feature 執行的結果轉換成 HTML 及 Word 文件，這樣一來只要搭配 CI 就可以讓所有作業一氣呵成！這樣的快感，是工程師夢寐以求的呀！</p>
<p>詳細方法當然還是上課才有的福利囉，至於 PHP 的方法我還在找尋中，這時就不得不說學 .Net 的朋友真是幸福。</p>
<h2 id="總結">總結</h2>
<p>不要一直想著要寫出完美的測試，因為那通常已經偏離了真正的需求。讓程式符合需求才是開發者的最終目標，只是完整而詳細的需求卻不會自己長腳跑來。</p>
<p>讓真正瞭解需求的人協助你一起完成規格，讓這些規格協助你建立測試，而且執行測試。最後我們唯一要關注的，就是如何去滿足這些測試的目標。對整個團隊來說，真的能改善不少浪費的問題。</p>
<p>如果有機會再開課的話，我強烈推薦大家報名參加呀！趕快關注「<a href="http://skilltree.my/events/ebg">自動測試與 TDD 實務開發</a>」的下一梯課程吧！</p>
]]></content>
		</item>
		
		<item>
			<title>自動測試與 TDD 實務開發 - 上課心得 (中)</title>
			<link>https://jaceju.net/skilltree-tdd-2/</link>
			<pubDate>Sat, 23 May 2015 16:19:43 +0800</pubDate>
			
			<guid>https://jaceju.net/skilltree-tdd-2/</guid>
			<description>曾經有個工程師對著已經上線的網站說：「別說使用者不曉得這個系統是怎麼運作的，其實已經接手那麼久的我也不知道。」 如果你對這句話心有戚戚焉的話，</description>
			<content type="html"><![CDATA[<p>曾經有個工程師對著已經上線的網站說：「別說使用者不曉得這個系統是怎麼運作的，其實已經接手那麼久的我也不知道。」</p>
<p>如果你對這句話心有戚戚焉的話，那你真的不孤單。其實有很多維護維護前人程式碼的工程師在在接到新的需求而去修改程式碼時，常常是很戰戰競競的，然後辦公室裡就會響起這樣的聲音：</p>
<pre><code>「我記得前一個工程師說這邊加幾行程式碼就可以了，你覺得呢？」
「我直覺這樣改會出問題...有環境讓我先測試看看嗎？」
「先不管，後天活動要上了，你先改完讓它上線後再說。」
「沒有測試的話...」
「閉嘴！快去改。」
不久後...
「慘了！訂單出大問題了！你可以先還原原來的程式碼嗎？」
「不對呀！我改這邊怎麼會讓那裡出錯？」
又過了一會兒...
「你有還原嗎？！整個訂單資料大亂了呀！你一定有動到什麼東西了！」
「我全還原了！現在跑的是我沒改過前的版本！」
「不管！加班修好它！」
「我不就說沒測試的話會有問題嗎！？為什麼不聽！？」
「你老闆我老闆？再靠杯試試看！」
三天後...
「夠了！拎杯走！我不想再改這個系統了！ (摔杯子) 」
</code></pre><p>假設今天有時間讓你調整這個系統，在不知道線上系統整體如何運作的狀況下，你會怎麼讓它容易被維護與增添新功能呢？</p>
<p>上週所介紹的<a href="http://jaceju.net/2015/05/17/skilltree-tdd/">單元測試</a>其實還沒辦法可以讓我們立刻套用在這樣的系統上，所以本週課程的重點就在：如何為已經上線的 legacy code 加上測試。</p>
<p>為了達成這個目標，講師介紹了兩個招式： Web Testing 與 Refactoring 。以下我就以我的方式來介紹我所學到的心得。</p>
<!-- raw HTML omitted -->
<h2 id="web-testing">Web Testing</h2>
<p>看過聖鬥士星矢的話，應該都知道雅典娜說過：「你不是還有生命嗎？」</p>
<p><img src="/images/athena.jpg" alt="無良老闆雅典娜"></p>
<p>那麼回到問題點：現行的系統沒有測試怎麼辦？</p>
<p>對工程師來說，雅典娜的話就變成了：「你不是還有線上功能嗎？」</p>
<p>是呀！當系統是黑箱作業卻正常運作的時候，我們是從頁面 (介面) 上來確認的。換句話說，既然我們操作的方式是對的，而且畫面所得到結果也是我們所預期的，那麼對外部來說，這個已知功能就應該是正確的。所以我們可以先「對所有正確執行的功能建立測試」！只要在修改程式碼後，再跑相同的測試而沒有發生錯誤，就能證明我們的修改沒有影響到舊有程式碼。這實在是太酷了！</p>
<p>就網站來說，這樣的外部測試方式就稱為 Web Testing 。而如果所有的測試是以自動化腳本的形式存在，讓我們每次修改完程式碼後，可以方便地一次全部執行的話就太好了。最重要的，就是要有工具能幫我們產生這樣的腳本！而這種神一般的工具，就非 Selenium 莫屬了。</p>
<h3 id="selenium">Selenium</h3>
<p>Selenium 是一個讓瀏覽器自動化執行使用者操作流程的工具，通常用來當做測試的用途。它主要原理是利用 Selenium IDE 將使用者操作的過程錄製下來，並透過 Selenium IDE 或 Selenium WebDriver 執行該腳本，讓瀏覽器自動重現使用者操作過程。而它除了讓使用者操作的流程自動化之外，最厲害的地方是能夠把錄製下來的腳本轉換成不同的程式語言格式，融入到各語言的自動測試框架裡。</p>
<p>簡單介紹 Selenium 幾個要角：</p>
<ol>
<li>Selenium IDE ： Firefox 的 Add-on ，用來錄製使用者在操作頁面時所有的動作與輸入，並且驗證輸出結果是否符合預期。而它的重播功能除了讓開發者快速驗證功能外，用來製作說明文件裡的操作範本也相當適合。</li>
<li>Selenium WebDriver ：告訴使用者的 IDE (例如 Visual Studio) 或 Selenium Server 如何啟動不同瀏覽器的中介層。常用的 WebDriver 包含了 IE 、 Firefox 、 Chrome 、 PhantomJS 等。 (原名 Selenium Remote Control)</li>
<li>Selenium Server (選備) ：用 Java 寫的 Daemon 服務；如果你的開發環境和測試用的瀏覽器不在同一台機器上，或是某些無法直接啟動瀏覽器的狀況下，你可能會需要透過 Selenium Server 來協助 WebDriver 啟動測試用的瀏覽器。 (原名 Selenium RC Server)</li>
</ol>
<p>註：由於課程是使用 Visual Studio 來做本機開發與測試，所以只要用 Nuget 安裝 Selenium WebDriver 套件即可，不需要 Selenium Server 。</p>
<p>整個 Selenium 的使用流程如下：</p>
<ol>
<li>在 Firefox 上安裝 <a href="http://release.seleniumhq.org/selenium-ide/2.9.0/selenium-ide-2.9.0.xpi">Selenium IDE</a> 。</li>
<li>視開發環境決定是否要啟動 Selenium Server ，或使用整合式的 Selenium WebDriver 。如果是 PHP 的話，不論是不是在本機執行，都一定要啟動 Selenium Server 。</li>
<li>啟動 Web 服務，讓 Firefox 連上測試網站。</li>
<li>針對所有功能錄製測試腳本 (包含操作流程與結果驗證) 。</li>
<li>重跑所有腳本，確認沒有操作上的問題。</li>
<li>利用 Selenium 的 Formatter 外掛，將 Selenium IDE 腳本轉換成對應的程式語言自動化測試框架的程式碼。</li>
<li>讓自動化測試框架透過 Selenium WebDriver 來完整執行所有的 Selenium 測試腳本，並確認各瀏覽器有被正常開啟。</li>
<li>接下來就可以繼續後續程式的新增功能或重構了。</li>
</ol>
<p>而 Selenium WebDriver 詳細的使用方式在不同的語言有不同的作法，請分別參考我兩位好友的介紹：</p>
<ul>
<li>Vistual Studio 走這邊： <a href="http://goo.gl/uIFwD">[30天快速上手TDD]Integration Testing &amp; Web UI Testing by Joey Chen</a> (作者名字很熟對不對？)</li>
<li>PHP 走這邊：<a href="https://www.youtube.com/watch?v=CtsH1n5-Xcc&amp;hd=1">PHP 也有Day #13 - 如何使用 Selenium 搞定前端測試 by Ricky Su</a></li>
</ul>
<p>註：之前我也寫過 <a href="http://jaceju.net/2010/07/12/1293/">Web UI 測試的好幫手 - Selenium</a> ，雖然內容有點舊，但原理沒差太多。</p>
<h3 id="常見的問題">常見的問題</h3>
<p>像 Web Testing 這種由外部建立測試的方法，雖然在遇到大多數沒有撰寫測試的 legacy 系統時非常有用，但也不能說毫無缺點。當系統對環境依賴度相當高時，想重建一個供測試用的系統會變得非常困難，而這通常也是必須先克服的問題之一。</p>
<p>然後規模稍大，功能點較多的網站就會面臨第二個問題：「誰來錄操作流程？」答案很簡單：當然不是工程師。通常 legacy 系統一定會有瞭解它是如何使用的人，這時候必須借重老闆的影響力，想辦法讓這些人能夠協助列出系統所有的功能流程。</p>
<p>由於 Selenium IDE 非常易用，可以先花點時間教會這些人學習怎麼錄製操作流程。接下來請他們在邊列功能時，順手將流程錄製起來；這樣一來當功能列完的同時，就有完整的測試可以使用，還順便帶有操作示範呢。</p>
<p>另外若是錯誤的操作會讓程式出現非預期的結果的話，就應該歸類在原本就有的 bug 。但這也表示我們知道有這個操作流程應該要被避免，因此也要特別為它建立一組測試；一來可以在修改程式後透過這組測試確認程式的修改是正確的，二來也確保未來不會再讓這個錯誤發生。就像「同樣的招式不能對聖鬥士使用第二次」，同樣的 bug 也不該在系統上發生第二次。</p>
<h3 id="fluentautomation-從愛開始">FluentAutomation 從愛開始</h3>
<p>Seleinum WebDriver 程式碼好難看懂怎麼辦？如果未來我加新功能而沒辦法錄製腳本時，要照著這樣寫測試嗎？當然不！我們需要讓測試腳本變得抽象化一些，看起來就像是用人話在描述規格一樣。</p>
<p>又是借重好用工具的時刻，在微軟體系上有個好用的 <a href="http://fluent.stirno.com/">FluentAutomation</a> 第三方套件，就是能將 Selenium 腳本語法包裝起來的語義化框架。</p>
<p>例如我們有這樣的規格：</p>
<div class="highlight"><pre class="chroma"><code class="language-csharp" data-lang="csharp"><span class="c1">// 登入成功
</span><span class="c1"></span>    <span class="c1">// 我打開 Login 的網頁
</span><span class="c1"></span>    <span class="c1">// 在 id 裡面輸入 user
</span><span class="c1"></span>    <span class="c1">// 在 password 裡面輸入 pass
</span><span class="c1"></span>    <span class="c1">// 按下登入
</span><span class="c1"></span>    <span class="c1">// 期望應該導到首頁
</span></code></pre></div><p>用 FluentAutomation 寫出來的話，就會是：</p>
<div class="highlight"><pre class="chroma"><code class="language-csharp" data-lang="csharp"><span class="k">private</span> <span class="kt">string</span> <span class="n">baseUrl</span> <span class="p">=</span> <span class="s">@&#34;http://localhost:29021/&#34;</span><span class="p">;</span>
<span class="na">
</span><span class="na">[TestMethod]</span>
<span class="k">public</span> <span class="k">void</span> <span class="n">TestLoginSuccess</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">I</span><span class="p">.</span><span class="n">Open</span><span class="p">(</span><span class="n">baseUrl</span> <span class="p">+</span> <span class="s">&#34;login&#34;</span><span class="p">)</span>
        <span class="p">.</span><span class="n">Enter</span><span class="p">(</span><span class="s">&#34;user&#34;</span><span class="p">).</span><span class="n">In</span><span class="p">(</span><span class="s">&#34;#id&#34;</span><span class="p">)</span>
        <span class="p">.</span><span class="n">Enter</span><span class="p">(</span><span class="s">&#34;pass&#34;</span><span class="p">).</span><span class="n">In</span><span class="p">(</span><span class="s">&#34;#password&#34;</span><span class="p">)</span>
        <span class="p">.</span><span class="n">Click</span><span class="p">(</span><span class="s">&#34;input[type=\&#34;submit\&#34;]&#34;</span><span class="p">)</span>
        <span class="p">.</span><span class="n">Assert</span><span class="p">.</span><span class="n">Url</span><span class="p">(</span><span class="n">baseUrl</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div><p>因為測試的主角是「我」，所以一切從 <code>I</code> 開始；而測試的對象是「網頁」，所以我們會有 <code>Open</code> (開啟網址) / <code>Enter</code> (輸入) / <code>Click</code> (點擊) 等操作方式；最後再使用 <code>Assert</code> 來驗證畫面輸出是否如我們所預期。整段程式碼是不是看起來更語意化呢？</p>
<p>除了語法更為抽象之外， FluentAutomation 執行的結果其實和 Selenium WebDriver 是一樣的。 FluentAutomation 能讓開發者融入測試的情境中，從使用者行為的角度出發去看待系統功能。一切就像用人話在說明功能，看起來就是這麼自然。</p>
<p>註： FluentAutomation 也<a href="http://fluent.stirno.com/docs/#multi-browser">支援多種不同瀏覽器</a>同時測試，相關的說明請參考<a href="http://fluent.stirno.com/docs/">官方文件</a>。</p>
<p>註： PHP 也有類似的框架，是包含在 <a href="http://codeception.com/">Codeception</a> 的 <a href="http://codeception.com/docs/modules/WebDriver">WebDriver</a> 模組中。</p>
<h3 id="page-objects-pattern">Page Objects Pattern</h3>
<p>課程中介紹另一個我覺得很棒的觀念是，雖然功能是一樣的，但如果頁面結構調整了怎麼辦？這時候就要用到物件導向最重要的觀念：封裝變化。</p>
<p>Page Objects Patterns 把頁面當成是一個物件，只讓它露出必要的行為，讓我們的主要流程只描述如何跟這個頁面互動，而不在乎它頁面上的細節。這樣一來不管頁面結構怎麼變化，只要主要功能不變，我們都只需要修改這個 Page Object 就好。</p>
<p>Page Objects 在 FluentAutomation 的實作範例可以參考官方說明的 <a href="http://fluent.stirno.com/docs/#pageobjects">PageObjects</a> 一節。</p>
<p>同樣地在 PHP 的 Codeception 也支援 Page Objects Patterns ，請參考 <a href="http://codeception.com/docs/07-AdvancedUsage#PageObjects">PageObjects</a> 一節。</p>
<h2 id="refactoring">Refactoring</h2>
<p>假設我們已經有了 Web Testing 來確保我們的功能都經過了外部測試，那麼我們就有機會對它進行重構了。</p>
<p>課程中的範例可以說是這兩週課程的精華呀，從一個很難撼動的程式碼，一步一步重構成包含了 Web Testing 和單元測試的可維護可擴充架構。</p>
<p>這裡很難把它用三言兩言完整描述出來，恕我偷懶一下，這裡請直接參考講師以前寫的 30 天快速上手 TDD 之重構系列文章：</p>
<ul>
<li><a href="http://goo.gl/59rTl">[30天快速上手TDD][Day 9]Refactoring legacy code 簡介</a></li>
<li><a href="http://goo.gl/4zdts">[30天快速上手TDD][Day 10]Refactoring 起手式 - 建立測試</a></li>
<li><a href="http://goo.gl/LWUDp">[30天快速上手TDD][Day 11]Refactoring - 讓程式碼說話</a></li>
<li><a href="http://goo.gl/U3nvF">[30天快速上手TDD][Day 12]Refactoring - 職責分離</a></li>
<li><a href="http://goo.gl/zRgyy">[30天快速上手TDD][Day 13]Refactoring - 告訴我，你要什麼</a></li>
<li><a href="http://goo.gl/A7EnM">[30天快速上手TDD][Day 14]Refactoring - 驗貨</a></li>
<li><a href="http://goo.gl/xMt7p">[30天快速上手TDD][Day 15]Refactoring - 食神歸位</a></li>
<li><a href="http://goo.gl/gcylXL">[30天快速上手TDD][Day 16]Refactoring - 介面導向</a></li>
<li><a href="http://goo.gl/2THME">[30天快速上手TDD][Day 17]Refactoring - Strategy Pattern</a></li>
<li><a href="http://goo.gl/vyHtI">[30天快速上手TDD][Day 18]Refactoring - Factory Pattern</a></li>
<li><a href="http://goo.gl/yzsQc">[30天快速上手TDD][Day 19]Refactoring - The End is the Beginning</a></li>
</ul>
<p>簡單說明一下整套重構流程的重點：</p>
<ol>
<li>用 Selenium 錄下你的 Web 測試，越完整越好，而且讓它可以隨時執行。</li>
<li>理解你要重構的 legacy 程式碼意圖，把註解補上去。不用管細節，只要知道某段程式打算做什麼就好。</li>
<li>重構：想辦法把邏輯和 UI 分離開來，這個可能會因為語言或框架的不同而有不同的做法。</li>
<li>重構：應用常用的 Extract Method 技巧來將程式碼拆分出易懂的方法。</li>
<li>重構：依照職責分離的原則，把剛剛分離出來的方法引到新的類別中。</li>
<li>獨立出類別後，就可以把單元測試補上去了。接著所有測試都跑跑看吧。</li>
<li>重構：為各相似的類別抽出介面 (Interface) ，讓主程式去依賴介面，而不要依賴剛剛的類別。</li>
<li>重構：在抽出介面後，利用 Factory Method 將生成物件 (<code>new</code>) 的職責獨立出去。</li>
</ol>
<p>要特別記住：</p>
<ul>
<li>每一步重構後都要測試，而且只要測試是綠燈，都是應該是能夠上線的狀態。</li>
<li>通過測試後，就把程式碼 commit 到 VCS 裡，別偷懶。</li>
<li>重構在搬移程式碼時，只修改 context ，而不要修改流程結構。</li>
<li>修改流程結構 (例如 <code>if</code> 換成 <code>switch</code> ) 要自成一個重構。</li>
</ul>
<h3 id="避免增加中介層的獨立測試">避免增加中介層的獨立測試</h3>
<p>這段是講師的壓箱寶，據說是這次課程才加入的，有聽有賺到。</p>
<p>這邊我打算用一句話來帶過：懶爸爸有個相依耦合的類別，把它抽出來放到可覆寫的方法中回傳；用個笨兒子來繼承懶爸爸，接著笨兒子用 DI 來注入相依物件的 stub object ；最後改測試笨兒子，結束。</p>
<p>如果看得懂上面這句，我想就能得到講師這段課程的精髓了。至於這個技巧多有用？如果發現類別中相依耦合度很高，卻又不確定這些類別被哪些程式用到，更擔心修改方法簽名就會影響整個系統時，你就會知道這個技巧的威力了。</p>
<h2 id="心得">心得</h2>
<p>我必須老實說，不論是補測試或是重構，技術上絕對不是什麼大問題；像這次的課程所介紹的技巧，都沒有什麼非常困難的部份。那麼為什麼很多人不做呢？我想是因為大家都拿「新功能都做不完了，哪來時間做重構？」做為藉口了。但回到最文章最開頭的情境，其實真的花你時間的，反而不是這些測試或重構，而是你跟同事因為系統出問題的爭執。</p>
<p>如果老闆有心解決舊有系統的問題，就想辦法說服他加上測試並重構吧。</p>
<p>想瞭解更多的話，請報名「<a href="http://skilltree.my/events/ebg">自動測試與 TDD 實務開發</a> 」。</p>
]]></content>
		</item>
		
		<item>
			<title>自動測試與 TDD 實務開發 - 上課心得 (上)</title>
			<link>https://jaceju.net/skilltree-tdd/</link>
			<pubDate>Sun, 17 May 2015 10:07:11 +0800</pubDate>
			
			<guid>https://jaceju.net/skilltree-tdd/</guid>
			<description>測試一直以來是很多開發者心中的痛，當被老闆問到：「你的程式碼是否都測試過了？」你是否能摸著自己的良心，並且拿出證據來說自己真的做過測試讓所有</description>
			<content type="html"><![CDATA[<p>測試一直以來是很多開發者心中的痛，當被老闆問到：「你的程式碼是否都測試過了？」你是否能摸著自己的良心，並且拿出證據來說自己真的做過測試讓所有功能都符合需求了？我相信絕大多數的開發者這時一定都是面有難色，因為其實你不但騙了老闆，還騙了你自己。</p>
<p>從我自己開始研究測試後，其實一直覺得自己還是沒有真正把測試當成是開發的一部份，更別說坊間很多程式書籍和教學課程能把這兩者真真實實地融合在一起，傳授給廣大的開發人員。</p>
<p>這次正好有機會去聽 Joey (91 哥) 在 <a href="http://skilltree.my/">SkillTree</a> 開的 <a href="http://skilltree.my/events/ebg">TDD 課程</a>，我心想這實在是太棒了！ Joey 是業界在 TDD 領域相當有研究，而且也已經在實務上經過千錘百鍊的高手，如果能親眼見到他是如何把測試融入開發中的話，一定能大大提升自己的經驗值！</p>
<p>不過課程中精采的部份太多了，恕我無法一一介紹；而且我也不覺得我能在這篇心得中，表達出講師在實務面的深厚功力。以下就讓我來為大家介紹這個課程為什麼值得你去聽的心得。</p>
<!-- raw HTML omitted -->
<h2 id="絕對有料的課程內容">絕對有料的課程內容</h2>
<p>我個人很喜歡這個課程一開始就把整個學習地圖展開來，讓學員知道接下來幾週裡會由淺入深地學到哪些技巧，例如本週 (第一週) 是單元測試的基礎。雖然說是基礎，但卻是最關鍵的一環！因為講師會將所有至今你對測試的錯誤認知重新洗掉，注入最正確的觀念！</p>
<p>而接下來講師就舉出了幾個血淋淋的真實案例，告訴你為什麼測試是重要的。測試不是用來應付流程的工具，而是紥紥實實讓你對自己的程式碼能更有信心的魔法。你不見得能在一開始就體會測試的重要性，但你一定會在出大包時懊悔：如果當時有測試就好了 (曾經待過 EC 產業的我感同身受) 。</p>
<p>講師也三不五時會讓學員用回答問題的方式，讓學員重新審視對剛剛所教授的內容是否有真的瞭解，講師也會適時地再補充。講師也會針對同學的發問，來分享自己在實務上遇到的問題。</p>
<h2 id="一定能懂的教學方式">一定能懂的教學方式</h2>
<p>有趣的是，這次課程講師是用 C# 教學，但在 PHP 中打滾的我卻對範例沒有絲毫的疏離感。因為講師舉的例子相當生活化，而且寫出來的程式碼非常平易近人，只要有寫過程式的人一定都能看懂。</p>
<p>課程中包含了讓學員動手的實戰練習，每個範例都是從實務中淬鍊出來的，像是如何測試亂數？如何測試只會在特定日期啟用的功能等等；而且範例經過縝密的安排，能循序漸進地讓學員瞭解如何在測試中套用課程中講授的原則。你會很驚訝原來這些我們以為難測試的問題，其實在測試中解決的方式竟然是這麼簡單。</p>
<p>當然對不會寫 C# 的我來說，原本以為沒辦法用 IDE 實際演練會是件憾事；但這些實例設計之精巧，卻能夠讓我用 PHP 來重新代入；所謂一法通萬法通，測試的觀念本來就不限程式語言，這樣讓人興奮的實戰訓練，會讓人有手停不下來的感覺。</p>
<h2 id="教你怎麼快速建立測試又能寫出好測試">教你怎麼快速建立測試又能寫出好測試</h2>
<p>我們都知道開發應該針對需求，然而測試也是。但我們卻容易想得太多，導致我們做了太多無謂的工作。在課程中，講師會告訴你如何讓測試去配合需求，而不是為了測試而測試。搭配測試的原則，我們就不會再認為測試是多出來的工，而應該是開發的一部份。</p>
<p>例如關注點分離、 FIRST 、 3A 這類的測試原則，都是寫測試時所應該遵守的，但很少人知道要怎麼去應用。然而透過這個課程的範例教學，你會發現這麼多名詞和原則，竟能從中得到驗證！因為它們其實都是從實務所演化出來的，只是我們一直以教條式的方法來學習它們；當真正看到這些原則是這麼輕鬆地被講師透過實例展現出來時，那種震憾是文字難以描述的。</p>
<p>當然我們在實際撰寫測試碼時，還是很容易覺得它是件麻煩事，這時候測試工具就非常重要了。一個好的測試框架會影響你撰寫測試的意願，如果這工具沒辦法很容易又很簡潔地寫出符合意圖的測試，那麼也就不能怪其他同事質疑寫測試是不是有幫助了。</p>
<p>而號稱地表最強 IDE 之一的 Visual Studio 在這方面當然不會落於人後，簡單幾個指令和操作，測試骨架就立刻出現。再搭配第三方的測試工具，輕輕鬆鬆地就在這些骨架加血添肉，轉眼間測試就完成了。寫測試這件事從此融入整套開發流程裡，怎麼可能讓人不興奮呢？</p>
<h2 id="實務上面臨的問題">實務上面臨的問題</h2>
<p>Demo (SkillTree 主辦人) 在課餘時間聊到：「即便寫測試變得容易，而我們也開始把它變成自己的習慣，但你還是一定會碰到同事有這樣的質疑：『這程式碼就是正確的呀！為什麼還需要測試？』」我想一定有想在團隊裡導入測試的朋友常會遇到這種狀況，在這個課程中也可以讓你學到用什麼方法可以處理這樣的質疑。</p>
<p>在導入測試時時，最重要的就是讓同事對測試有信心；當寫測試變得很容易時，你所要做的，就是怎麼讓同事也能體會寫測試能為他帶來什麼效益。這方面講師也會提出他的實務經驗供我們參考，而且不論是技巧上和人性上，都是相當值得參考的建議。</p>
<p>最後還有一個可能會面臨到的，就是測試對程式碼的涵蓋率。講師的回答其實讓我非常驚訝 (容我賣個關子) ，這其實讓我對「測試對團隊的影響力」這件事大大地改觀。雖然我自己也在某個部份應用上了類似的觀念，但我以為這只是影響了我自己，沒想到這確實是一種對人性的實務作法，不禁讓我對它更有信心；我相信透過這些小小的改變，就有機會讓整個團隊變得更有勇氣去面對測試這件事情。</p>
<h2 id="小結">小結</h2>
<p>上完第一週的課程，我腦海裡一直迴響著：『你必須對你的測試有信心，測試才能讓你對你的程式有信心。』</p>
<p>測試應該要做什麼？怎麼寫有效的測試？怎麼快速地寫測試？怎麼讓同事一起來寫測試？原本以為自己已經可以回答這些問題，卻在這個課程中讓我的觀念整個大洗牌，也開始讓我心中已經消逝很久的熱血又漸漸沸騰起來。</p>
<p>再次感謝講師 Joey 和 SkillTree 的 Demo 舉辦這麼實用且精采的課程，我越來越期待接下來幾週的內容了。</p>
]]></content>
		</item>
		
		<item>
			<title>理解 Dependency Injection 實作原理</title>
			<link>https://jaceju.net/php-di-container/</link>
			<pubDate>Sun, 27 Jul 2014 17:45:50 +0800</pubDate>
			
			<guid>https://jaceju.net/php-di-container/</guid>
			<description>現代較新的 Web Framework 都強調自己有 Dependency Injection (以下簡稱 DI ) 的特色，只是很多人對它的運作原理還是一知半解。 所以接下來我將用一個簡單的範例，來為各位介紹在 PHP 中</description>
			<content type="html"><![CDATA[<p>現代較新的 Web Framework 都強調自己有 Dependency Injection (以下簡稱 DI ) 的特色，只是很多人對它的運作原理還是一知半解。</p>
<p>所以接下來我將用一個簡單的範例，來為各位介紹在 PHP 中如何實現簡易的 DI 。</p>
<!-- raw HTML omitted -->
<h2 id="基本範例">基本範例</h2>
<p>這是一個應用程式的範例，它只包含了登入處理程序。在這個範例中， <code>App</code> 類別的建構式參考了新的 <code>Auth</code> 與 <code>Session</code> 的物件實體，並在 <code>App::login()</code> 中使用。</p>
<p>註：請特別注意，為了呈現重點，我忽略掉很多程式碼，同時也沒有進行良好的架構設計；所以請不要把這個範例用在你的程式中，或是對為什麼我沒有進行錯誤處理，以及為什麼要採用奇怪的設計提出質疑。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class App
{
    protected $auth = null;
    protected $session = null;

    public function __construct($dsn, $username, $password)
    {
        $this-&gt;auth = new Auth($dsn, $username, $password);
        $this-&gt;session = new Session();
    }

    public function login($username, $password)
    {
        if ($this-&gt;auth-&gt;check($username, $password)) {
            $this-&gt;session-&gt;set(&#39;username&#39;, $username);
            return true;
        }
        return false;
    }
}
</code></pre></div><p>而 <code>Auth</code> 類別是從資料庫驗證使用者身份，這裡我僅用簡單的描述來呈現效果。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Auth
{
    public function __construct($dsn, $user, $pass)
    {
        echo &#34;Connecting to &#39;$dsn&#39; with &#39;$user&#39;/&#39;$pass&#39;...\n&#34;;
    }

    public function check($username, $password)
    {
        echo &#34;Checking username, password from database...\n&#34;;
        return true;
    }
}
</code></pre></div><p><code>Session</code> 類別也是概念性的實作：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Session
{
    public function set($name, $value)
    {
        echo &#34;Set session variable &#39;$name&#39; to &#39;$value&#39;.&#34;;
    }
}
</code></pre></div><p>最後我們讓程式動起來， client 程式如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$app = new App(&#39;mysql://localhost&#39;, &#39;username&#39;, &#39;password&#39;);
$username = &#39;jaceju&#39;;
if ($app-&gt;login($username, &#39;password&#39;)) {
    echo &#34;$username just signed in.\n&#34;;
}
</code></pre></div><p>註：這裡的 client 程式指的是實際操作這些物件實體的程式。</p>
<p>各位可以先試著想想這個程式在可擴充性上有什麼問題？例如我想把身份認證方式換成第三方服務的機制，或是改用其他媒介來存放 session 內容等。</p>
<p>還有如果想在沒有資料庫連線、或是沒有 HTTP session 的環境下對 <code>App::login()</code> 方法的邏輯進行隔離測試，各位會怎麼做呢？</p>
<h2 id="解除依賴關係">解除依賴關係</h2>
<p>上面的範例因為 <code>App</code> 類別已經依賴了 <code>Auth</code> 類別和 <code>Session</code> 類別，而這兩個類別都有實作跟系統環境有關的程式邏輯，這麼一來就會讓 <code>App</code> 類別難以進行底層機制的切換或是隔離測試。所以接下來我們要做的，就是把它們的依賴關係解除。</p>
<p>修改後的 <code>App</code> 類別如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class App
{
    protected $auth = null;
    protected $session = null;

    public function __construct(Auth $auth, Session $session)
    {
        $this-&gt;auth = $auth;
        $this-&gt;session = $session;
    }
}

$auth = new Auth(&#39;mysql://localhost&#39;, &#39;username&#39;, &#39;password&#39;);
$session = new Session();
$app = new App($auth, $session);
</code></pre></div><p>首先我們在 <code>App</code> 類別的建構式 <code>__construct</code> 原本的資料庫設定參數移除，並將原來直接以 <code>new</code> 關鍵字所產生的物件實體，改用方法參數的方式來注入。而使用 <code>new</code> 關鍵字產生物件實體的程式碼，就移到 <code>App</code> 類別外。</p>
<p>這種「將依賴的類別改用方法參數來注入」的作法，就是我們說的「依賴注入 (Dependency Injection) 」。</p>
<p>常見依賴注入的方式有兩種： Constructor Injection 及 Setter Injection 。它們的實作形式並沒有什麼不同，差別只在於是不是類別建構式而已。</p>
<p>不過 Constructor Injection 必須在建立物件實體時就進行注入，而 Setter Injection 則是可以在物件實體建立後才透過 setter 函式來進行注入。而這裡為了方便解說，我採用的是 Constructor Injection 。</p>
<h2 id="依賴抽象介面">依賴抽象介面</h2>
<p>好了，現在的問題是 <code>Auth</code> 類別的實作還是依賴在資料庫上，所以我們也要讓 <code>Auth</code> 類別跟資料庫之間解除依賴關係，讓它成為一個抽象介面。</p>
<p>這裡的抽象介面是指觀念上的意義，而非語言層級上的抽象類別 (Abstract Class) 或介面 (Interface) 。至於在實作上該用抽象類別還是介面，在這個範例裡並沒有差別，大家可以自行判斷；這裡我用介面 (Interface) ，因為我僅需要 <code>Auth::check()</code> 這個介面方法的定義而已。</p>
<p>這一步首先我把原來的 <code>Auth</code> 類別重新命名為 <code>DbAuth</code> 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class DbAuth
{
    public function __construct($dsn, $user, $pass)
    {
        echo &#34;Connecting to &#39;$dsn&#39; with &#39;$user&#39;/&#39;$pass&#39;...\n&#34;;
    }

    public function check($username, $password)
    {
        echo &#34;Checking username, password from database...\n&#34;;
        return true;
    }
}
</code></pre></div><p>接著建立一個 <code>Auth</code> 介面，它包含了 <code>Auth::check()</code> 方法的定義：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">interface Auth
{
    public function check($username, $password);
}
</code></pre></div><p>然後讓 <code>DbAuth</code> 類別實作 <code>Auth</code> 介面：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class DbAuth implements Auth
{
    // ...
}
</code></pre></div><p>最後把原來初始化 <code>Auth</code> 類別的物件實體的程式碼，改為初始化 <code>DbAuth</code> 的物件實體。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$auth = new DbAuth(&#39;mysql://localhost&#39;, &#39;username&#39;, &#39;password&#39;);
$session = new Session();
$app = new App($auth, $session);
</code></pre></div><p>透過 <code>Auth</code> 介面的幫助，我們已經讓 <code>App</code> 類別與實際的資料庫操作類別分離開來了。現在只要是實作 <code>Auth</code> 介面的類別，都可以被 <code>App</code> 類別所接受，例如我們可能會改用 HTTP 認證來取代資料庫認證：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class HttpAuth implements Auth
{
    public function check($username, $password)
    {
        echo &#34;Checking username, password from HTTP Authentication...\n&#34;;
        return true;
    }
}

$auth = new HttpAuth();
$session = new Session();
$app = new App($auth, $session);
</code></pre></div><p>當然其他類型的認證方式也可以透過建立新的類別來使用，而不會影響到 <code>App</code> 類別的內部實作。</p>
<h2 id="di-容器">DI 容器</h2>
<p>現在又有個問題， client 程式還是依賴於 <code>DbAuth</code> 類別或是 <code>HttpAuth</code> 類別；通常這種狀況在需要編譯型的語言 (例如 Java ) 中，程式一旦編譯完成佈署出去後，就很難再進行修改。</p>
<p>如果我們可以改用設定的方式來告訴程式，在不同的狀況下對應不同的類別，然後讓程式自行判斷環境來產生需要的物件實體，這樣就可以解開 client 程式對實作類別的依賴關係。</p>
<p>這裡要引入一個技術，稱為 DI 容器 (Dependency Injection Container) 。 DI 容器主要的作用在於幫我們解決產生物件實體時，應該參考哪一個類別。我們先來看看用法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">Container::register(&#39;Auth&#39;, &#39;DbAuth&#39;, [&#39;mysql://localhost&#39;, &#39;username&#39;, &#39;password&#39;]);

$auth = Container::get(&#39;Auth&#39;);
$session = new Session();
$app = new App($auth, $session);
</code></pre></div><p>首先我們在 DI 容器中先以 <code>Container::register()</code> 方法來註冊 <code>Auth</code> 這個別名實際上要對應哪個類別，以及建立物件實體時會用到的初始化參數。要注意，這裡的別名並不是指真正的類別或介面，但我們可以用相同的名稱以避免認知上的問題。</p>
<p>然後我們用 <code>Container::get()</code> 方法取得別名所對應類別的物件實體，上面例子裡的 <code>$auth</code> 就是 <code>DbAuth</code> 類別的物件實體。</p>
<p>這麼一來，我們就可以把註冊的程式碼移出 client 程式之外，並將註冊參數改用設定檔引入，順利解開 client 程式對實作類別的依賴。</p>
<h2 id="di-容器原理">DI 容器原理</h2>
<p>那麼 DI 容器的原理是怎麼運作的呢？首先在 <code>Container::register()</code> 方法註冊的部份，它其實只是把參數記到 <code>$map</code> 這個類別靜態屬性裡。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Container
{
    protected static $map = [];

    public static function register($name, $class, $args = null)
    {
        static::$map[$name] = [$class, $args];
    }

    // ...
}
</code></pre></div><p>重點在 <code>Container::get()</code> 方法，它透過 <code>$name</code> 別名，把 <code>$map</code> 屬性中對應的類別名稱和初始化參數取出；接著判斷類別是不是存在，如果存在的話就建立對應的物件實體。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Container
{
    // ...

    public static function get($name)
    {
        list($class, $args) = isset(static::$map[$name]) ?
                              static::$map[$name] :
                              [$name, null];

        if (class_exists($class, true)) {
            $reflectionClass = new ReflectionClass($class);
            return !empty($args) ?
                   $reflectionClass-&gt;newInstanceArgs($args) :
                   new $class();
        }

        return null;
    }
}
</code></pre></div><p>比較特別的是，如果初始化參數不是空值 (<code>null</code>) 時，則必須透過 <code>ReflectionClass::newInstanceArgs()</code> 方法來建立物件實體。 <code>ReflectionClass</code> 類別可以映射出指定類別的內部結構，並提供方法來操作這個結構； Reflection 是現代語言常見的機制， PHP 在這方面也提供了完整的 API 供開發者使用，請參考： <a href="http://php.net/manual/en/book.reflection.php">PHP: Reflection</a> 。</p>
<p><code>Container::get()</code> 方法也可以在沒有註冊的狀況下，直接把別名當成類別名稱，然後協助我們初始化對應的物件實體；例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$session = Container::get(&#39;Session&#39;);
</code></pre></div><h2 id="手動注入">手動注入</h2>
<p>現在我們的 client 程式已經修改成以下的樣子：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$auth = Container::get(&#39;Auth&#39;);
$session = Container::get(&#39;Session&#39;);
$app = new App($auth, $session);
</code></pre></div><p>不過當初始化參數較多的狀況下，重複寫好幾次 <code>Container::get()</code> 看起來也是挺囉嗦的。</p>
<p>接下來我們實作一個 <code>Container::inject()</code> 方法，提供開發者可以一次注入所有依賴物件實體：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$app = Container::inject(&#39;Auth&#39;, &#39;Session&#39;, function ($auth, $session) {
    return new App($auth, $session);
});
</code></pre></div><p>這裡我們讓 <code>Container::inject()</code> 接受不定個數的參數，除了最後一個參數必須是 callback 型態外，其他都是要傳遞給 <code>Container::get()</code> 的參數。 <code>Container::inject()</code> 的實作方式如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Container
{
    // ...

    public static function inject()
    {
        $args = func_get_args();
        $callback = array_pop($args);
        $injectArgs = [];

        foreach ($args as $name) {
            $injectArgs[] = Container::get($name);
        }

        return call_user_func_array($callback, $injectArgs);
    }
</code></pre></div><p>在參數個數不定的狀況下，可以用 <code>func_get_args()</code> 函式來取得所有參數；而 <code>array_pop()</code> 可以取出最後一個參數值做為 callback 。剩下的參數就透過 <code>Container::get()</code> 來取得物件實體，最後再透過 <code>call_user_func_array()</code> 函式將處理好的參數傳遞給 callback 執行。</p>
<h2 id="自動解決所有依賴注入">自動解決所有依賴注入</h2>
<p>在我們的範例裡， <code>Container</code> 類別如果可以提供一個方法，自動為我們解決所有 <code>App</code> 類別依賴問題，那麼程式就可以更乾淨些。</p>
<p>要做到這點，我們就必須知道要注入的方法所需要參數的類型；而在 PHP 中的 <a href="http://php.net/manual/en/language.oop5.typehinting.php">Type Hinting</a> ，就可以告訴我們參數所對應的變數類型或類別。</p>
<p>回到 <code>App::__construct()</code> 建構子上，我們看到 <code>$auth</code> 與 <code>$session</code> 兩個參數的 type hint 分別對應到 <code>Auth</code> 與 <code>Session</code> 這兩個類別，剛好就可以用來當做我們做自動依賴注入的條件。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class App
{
    public function __construct(Auth $auth, Session $session)
    {
    }
}
</code></pre></div><p>接著我們為 <code>Container</code> 類別提供一個 <code>resolve()</code> 方法，它可以接受一個類別名稱用來建立物件實體，而不需要再使用 <code>new</code> 關鍵字。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$app = Container::resolve(&#39;App&#39;);
</code></pre></div><p>我們希望 <code>Container::resolve()</code> 方法會自動產生參數所對應的物件，解決這個類別建構子所需要的依賴關係。它的實作如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Container
{
    // ...

    public static function resolve($name)
    {
        if (!class_exists($name, true)) {
            return null;
        }

        $reflectionClass = new ReflectionClass($name);
        $reflectionConstructor = $reflectionClass-&gt;getConstructor();
        $reflectionParams = $reflectionConstructor-&gt;getParameters();

        $args = [];
        foreach ($reflectionParams as $param) {
            $class = $param-&gt;getClass()-&gt;getName();
            $args[] = static::get($class);
        }

        return !empty($args) ?
               $reflectionClass-&gt;newInstanceArgs($args) :
               new $class();
    }
}
</code></pre></div><p><code>Container::resolve()</code> 方法與 <code>Container::get()</code> 方法的原理類似，但較特別的是它使用了 <code>ReflectionClass::getConstructor()</code> 方法來取得類別建構子的 <code>ReflectionMethod</code> 實體；接著再用 <code>ReflectionMethod::getParameters()</code> 取出參數的 <code>ReflectionParameter</code> 物件集合 (陣列) 。</p>
<p>而後我們就可以在迴圈中一一透過 <code>ReflectionParameter::getClass()</code> 方法與 <code>ReflectionClass::getName()</code> 方法來取得 type hint 所指向的類別或介面名稱。當有了參數所對應的類別或介面名稱後，就可以用 <code>Container::get()</code> 方法來取得參數的物件實體。</p>
<p>最後把這些物件帶回建構子的參數裡，並初始化我們所需要的物件實體，就完成了 <code>App</code> 類別的自動依賴注入。</p>
<h2 id="深入思考">深入思考</h2>
<p>再強調一次，這裡的範例只是為了介紹 DI 容器的原理，並不能真正用在實務上。因為一個完整的 DI 容器還要考慮以下的問題：</p>
<ul>
<li>類別不存在時的處理。</li>
<li>與其他非類別的參數整合。</li>
<li>如何建立設定檔機制以便切換依賴關係。</li>
<li>遞迴地自動注入物件實體。</li>
<li>取得 Singleton 物件實體。</li>
<li>可以透過原始碼上的 DocBlock 註解來註明依賴關係。</li>
</ul>
<p>目前已經有很多 DI Framework 幫我們處理好這些事情了，建議大家如果真的需要在專案中使用 DI 時，應該採用這些 Framework 。</p>
<h2 id="總結">總結</h2>
<p>如果專案並不會有太多變化性，那麼依賴注入對我們來說就不是那麼重要。但是如果希望程式對特定類別的依賴性降低，只針對抽象介面實作，那麼依賴注入就有其必要性。</p>
<p>在 PHP 上的 DI 容器的基本實作原理也不複雜，透過 Reflection 機制就可以看到類別內部的結構，讓我們對它的建構子注入我們想要的參數值。</p>
<p>DI 容器要考量的部份也不少，但這些功能都已經有 Framework 實作，我們應該在專案中使用它們而儘可能不要自行開發。</p>
<p>希望透過以上的介紹，可以讓大家對 Framework 的依賴注入機制有基本的認知。</p>
<p>註：上述程式碼都可以在 <a href="https://github.com/jaceju/php-di-container-examples">php-di-container-examples
</a> 找到。</p>
]]></content>
		</item>
		
		<item>
			<title>整理一些常見的 PHP 錯誤</title>
			<link>https://jaceju.net/summary-of-common-php-mistakes/</link>
			<pubDate>Mon, 21 Jul 2014 10:51:03 +0800</pubDate>
			
			<guid>https://jaceju.net/summary-of-common-php-mistakes/</guid>
			<description>最近有數篇文章介紹了 PHP 開發者常見的錯誤，我順手整理如下： 10 Common PHP Coding Errors 在 foreach 迴圈中使用了迭代項目的參考。 誤用了 isset 。 搞混了回傳值是傳參考還是傳值。 在</description>
			<content type="html"><![CDATA[<p>最近有數篇文章介紹了 PHP 開發者常見的錯誤，我順手整理如下：</p>
<!-- raw HTML omitted -->
<p><a href="http://www.toptal.com/php/10-most-common-mistakes-php-programmers-make">10 Common PHP Coding Errors</a></p>
<ol>
<li>在 foreach 迴圈中使用了迭代項目的參考。</li>
<li>誤用了 isset 。</li>
<li>搞混了回傳值是傳參考還是傳值。</li>
<li>在迴圈中執行不必要的 SQL Query 。</li>
<li>一次取得太多結果，造成不必要的記憶體浪費。</li>
<li>忽略了 Unicode / UTF-8 問題。</li>
<li>總是假設 $_POST 會包含 POST 資料。</li>
<li>沒有注意 PHP 不支援 char 字元型態。</li>
<li>對編碼標準的忽略，尤其是 PSR 標準 (當然有些依據不見得好，但也是大家討論出來的標準) 。</li>
<li>誤用了 empty 函式。</li>
</ol>
<p><a href="http://afilina.com/common-php-mistakes/">Common PHP Mistakes</a></p>
<ol>
<li>忘了使用快取機制來減少大量 requests 對系統的衝擊。</li>
<li>沒有避開 SQL Injection 。</li>
<li>開發時關掉了錯誤回報。</li>
<li>表單 POST 後還停留在同一頁。</li>
<li>沒有善用已經存在的工具，而重造輪子。</li>
<li>關掉了 timeout 機制，讓 script 沒有停止的依據。</li>
<li>忽略了網站可用性。</li>
</ol>
<p><a href="http://www.sitepoint.com/7-mistakes-commonly-made-php-developers/">7 More Mistakes Commonly Made by PHP Developers</a></p>
<ol>
<li>使用過時的 mysql extention 。</li>
<li>沒有使用 PDO 的參數機制來避免 SQL Injection 。</li>
<li>沒有重寫網址，讓它符合現今的網址守則。</li>
<li>抑制錯誤訊息的發生。</li>
<li>在條件判斷式中賦值。</li>
<li>曝露太多有關系統所使用的 Framework 資訊。</li>
<li>沒有移除掉開發時的設定檔。</li>
</ol>
]]></content>
		</item>
		
		<item>
			<title>CSS3 動畫基礎</title>
			<link>https://jaceju.net/css3-animation-notes/</link>
			<pubDate>Tue, 03 Jun 2014 10:28:36 +0800</pubDate>
			
			<guid>https://jaceju.net/css3-animation-notes/</guid>
			<description>註：本文為作者發表於 OpenFoundry 之 CSS3 動畫基礎一文的備份。 在 JSConf.Asia 2013 ， Lea Verou 介紹了 CSS in the 4th dimension (影片) ，引發了整個 Web 界對 CSS 動畫的期盼；在 CSS動畫簡介一文也已經</description>
			<content type="html"><![CDATA[<p>註：本文為作者發表於 OpenFoundry 之 <a href="http://www.openfoundry.org/en/tech-column/9233-css3-animation">CSS3 動畫基礎</a>一文的備份。</p>
<!-- raw HTML omitted -->
<p>在 JSConf.Asia 2013 ， Lea Verou 介紹了 <a href="http://lea.verou.me/css-4d/#intro">CSS in the 4th dimension</a>  (<a href="https://www.youtube.com/watch?v=NTJUFQmHbvc">影片</a>) ，引發了整個 Web 界對 CSS 動畫的期盼；在 <a href="http://www.ruanyifeng.com/blog/2014/02/css_transition_and_animation.html">CSS動畫簡介</a>一文也已經把重點整理好了。</p>
<p>以下我們將會介紹主要兩個 CSS3 在動畫的屬性： Transition 與 Animation ，並配合實例來練習這些技術，後面我也會介紹一些不錯的相關開發工具。</p>
<!-- raw HTML omitted -->
<h2 id="transition">Transition</h2>
<p>在以往 HTML 元素在兩種外觀之間的變換，只能從一種外觀直接跳到另一種外觀，瀏覽者並沒有辦法感受到這兩種外觀中間平滑的轉換，造成了視覺上的不適。</p>
<!-- raw HTML omitted -->
<h3 id="基本的-transition">基本的 transition</h3>
<p>而 CSS 為了補足這方面的視覺轉換特效，特別加入 <code>transition</code> 屬性。 一個簡易的動畫效果就是在想要變化的狀態上，加入一個 <code>transition</code> 屬性，而其值為變化需歷時的秒數。</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="nt">div</span><span class="p">:</span><span class="nd">hover</span> <span class="p">{</span>
    <span class="err">...</span>
    <span class="k">transition</span><span class="p">:</span> <span class="mi">1</span><span class="kt">s</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>這麼一來， <code>transition</code> 就會自動幫我們補足中間的過場動畫。例如我們希望上面的例子能平順地轉換，歷時一秒：</p>
<!-- raw HTML omitted -->
<p>我們也可以讓高度以外的屬性有動畫效果，例如顏色：</p>
<!-- raw HTML omitted -->
<h3 id="transition-屬性詳解"><code>transition</code> 屬性詳解</h3>
<p><code>transition</code> 屬性其實跟 <code>font</code> 或 <code>background</code> 屬性一樣是簡寫屬性，它是以下四個屬性的總和：</p>
<ul>
<li><code>transition-property</code>: 要做變換的 CSS 屬性</li>
<li><code>transition-duration</code>: 變換需要的時間，單位為 <code>s</code> 或 <code>ms</code></li>
<li><code>transition-delay</code>: 延遲多久後開始變換，單位為 <code>s</code> 或 <code>ms</code></li>
<li><code>transition-timing-function</code>: 稱為 Timing Funciton ，用名稱來定義變換時的加速度。</li>
</ul>
<p>這些屬性可以分開寫，也可以將它們的值同時寫在 <code>transition</code> 屬性裡；唯一要注意的是 <code>transition-duration</code> 與 <code>transition-delay</code> 的值寫在一起時有前述的順序關係。前面例子中的 <code>transition: 1s</code> ，其 <code>1s</code> 即為 <code>transition-duration</code> 的值。</p>
<h3 id="transition-property-可使用-transition-的屬性"><code>transition-property</code> 可使用 transition 的屬性</h3>
<p>不是所有 CSS 屬性都可以使用 <code>transition</code> ，可以參考這篇 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties">CSS animated properties</a> 得知有哪些屬性可以使用 <code>transition</code> 。</p>
<p><code>transition</code> 預設是對所有可套用的屬性做轉場效果，也就是關鍵字 <code>all</code> ；但其實也可以只針對某個屬性做 <code>transition</code> 變化，其他屬性則維持原來的直接變化。</p>
<!-- raw HTML omitted -->
<h3 id="針對不同屬性同時做-transition">針對不同屬性同時做 transition</h3>
<p>如果希望對兩個以上的屬性做 <code>transition</code> ，可是又不希望影響其他屬性時，可以用逗號 <code>,</code> 將要做 transition 的屬性分隔開來。</p>
<!-- raw HTML omitted -->
<h3 id="transition-delay-延遲變換"><code>transition-delay</code> 延遲變換</h3>
<p>有時候我們需要先變換一個屬性，再變換另一個屬性，這時候就需要對後者加入一個延遲時間；它需要加在原先我們定義好的歷時時間之後。</p>
<!-- raw HTML omitted -->
<h3 id="transition-timing-function-timing-funciton"><code>transition-timing-function</code> Timing Funciton</h3>
<p>Timing Funciton 包含數種模式，下圖可以看出它們的加速度曲線。</p>
<p><img src="http://letrainde13h37.fr/wp-content/uploads/2012/09/trTimingFn.png" alt=""></p>
<ul>
<li><code>linear</code>: 匀速</li>
<li><code>ease</code>: 急加速後減速 (預設值)</li>
<li><code>ease-in</code>: 加速</li>
<li><code>ease-out</code>: 减速</li>
<li><code>ease-in-out</code>: 較平緩的 <code>ease</code></li>
<li><code>cubic-bezier</code>: 自定義速度模式</li>
</ul>
<h3 id="cubic-bezier-函式">cubic-bezier 函式</h3>
<p>利用貝茲曲線函式來定義加速曲線，可以直接使用線上工具 <!-- raw HTML omitted -->cubic-bezier()<!-- raw HTML omitted --> 來找出需要的數值。</p>
<h3 id="雙向的-transition">雙向的 transition</h3>
<p>Transition 的效果只會作用在有加入 <code>transition</code> 屬性的那個狀態，一旦要回復至原來的狀態時，就會失去 Transition 的平順效果了。這時我們需要對原先的狀態，也加入 <code>transition</code> 。</p>
<!-- raw HTML omitted -->
<h3 id="transition-的限制">Transition 的限制</h3>
<p><code>transition</code> 的開始和結束都必須是具體數值；例如以下的 CSS 屬性值之間是無法被計算的，就無法使用 <code>transition</code> ：</p>
<ul>
<li><code>height: auto</code> (不確定的值) 至 <code>height: 100px</code> (具體數值)</li>
<li><code>display: none</code> 至 <code>display: block</code></li>
<li><code>background: url(foo.jpg)</code> 至 <code>background: url(bar.jpg)</code></li>
</ul>
<!-- raw HTML omitted -->
<p>另外 <code>transition</code> 需要事件來觸發它的動作，所以沒辦法在一進頁面自動產生效果。所以如果不透過 JavaScript 事件處理的話，就只能配合與事件有關的 Pseudo Classes (偽類別，即 <code>:hover</code> 、 <code>:focus</code> 等) 來呈現效果了。</p>
<!-- raw HTML omitted -->
<h3 id="搭配-jquery">搭配 jQuery</h3>
<p>如果搭配 jQuery 等可以操作 DOM 元素的 library ，我們就可以做更複雜的操作。</p>
<!-- raw HTML omitted -->
<h3 id="瀏覽器支援">瀏覽器支援</h3>
<p>目前包含 IE 10+ 的主流瀏覽器都已經支援 <code>transition</code> ，可參考 <a href="http://caniuse.com/#search=transition">Can I use</a> 。</p>
<h2 id="animation">Animation</h2>
<p>雖然 <code>transition</code> 屬性簡單易用，但也有上述的侷限。因此就有了 <code>animation</code> 這個屬性來彌補其不足。</p>
<h3 id="基本的-animation">基本的 Animation</h3>
<p>最基本的 <code>animation</code> 要指定動畫持續的時間，還有動畫的名稱。</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="nt">div</span><span class="p">:</span><span class="nd">hover</span> <span class="p">{</span>
  <span class="k">animation</span><span class="p">:</span> <span class="mi">1</span><span class="kt">s</span> <span class="n">fat</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>而動畫的定義則是用 <code>@keyframes</code> 這個屬性，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">@</span><span class="k">keyframes</span> <span class="nt">fat</span> <span class="p">{</span>
  <span class="nt">0</span><span class="o">%</span> <span class="p">{</span> <span class="k">width</span><span class="p">:</span> <span class="mi">100</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
  <span class="nt">50</span><span class="o">%</span> <span class="p">{</span> <span class="k">width</span><span class="p">:</span> <span class="mi">150</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
  <span class="nt">100</span><span class="o">%</span> <span class="p">{</span> <span class="k">width</span><span class="p">:</span> <span class="mi">200</span><span class="kt">px</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>在 <code>@keyframes</code> 中可以定義多個狀態，範圍可從 <code>0%</code> 至 <code>100%</code> 。另外 <code>0%</code> 可寫成 <code>from</code> ， <code>100%</code> 可寫成 <code>to</code> ，其他狀態還是使用數字百分比。</p>
<!-- raw HTML omitted -->
<h3 id="animation-屬性詳解"><code>animation</code> 屬性詳解</h3>
<p><code>animation</code> 屬性和 <code>transition</code> 屬性一樣，都是簡寫屬性。它代表以下屬性的總和：</p>
<ul>
<li><code>animation-name</code>: 動畫名稱</li>
<li><code>animation-duration</code>: 播放一次動畫需要的時間，單位為 <code>s</code> 或 <code>ms</code></li>
<li><code>animation-timing-function</code>: 動畫的加速度曲線</li>
<li><code>animation-delay</code>: 延遲多久後啟始動畫</li>
<li><code>animation-iteration-count</code>: 動畫播放次數，可用 <code>infinite</code></li>
<li><code>animation-direction</code>: 動畫播放方向</li>
<li><code>animation-fill-mode</code>: 指定動畫播放前後的狀態</li>
<li><code>animation-play-state</code>: 指定動畫播放或暫停</li>
</ul>
<p>其中 <code>animation-duration</code> 、 <code>animation-timing-function</code> 、 <code>animation-delay</code> 可參考上面 <code>transition</code> 相似屬性的介紹。</p>
<h3 id="animation-iteration-count-播放次數"><code>animation-iteration-count</code> 播放次數</h3>
<p>預設 <code>animation</code> 和 <code>transition</code> 一樣只會動作一次，但我們可以加入數字來指定動畫效果播放的次數。</p>
<!-- raw HTML omitted -->
<p>或是以 <code>infinite</code> 這個關鍵字來無限次播放。</p>
<!-- raw HTML omitted -->
<h3 id="animation-direction-播放方向"><code>animation-direction</code> 播放方向</h3>
<p>所謂的播放方向是指從動畫效果 0% 到 100% 的方向，同時也是預設的 <code>normal</code> 值。可供設定的值如下：</p>
<ul>
<li><code>normal</code> ：每次播放都是從 0% 至 100%</li>
<li><code>reverse</code> ：每次播放都是從 100% 至 0%</li>
<li><code>alternate</code> ：播放兩次以上的話，會從 0% 至 100% ，再從 100% 回到 0% ，以此類推</li>
<li><code>alternate-reverse</code> ：跟 <code>alternate</code> 相反，會先從 100% 開始播放</li>
</ul>
<p><code>animation-direction: reverse</code> ：</p>
<!-- raw HTML omitted -->
<p><code>animation-direction: alternate</code> ：</p>
<!-- raw HTML omitted -->
<p><code>animation-direction: alternate-reverse</code> ：</p>
<!-- raw HTML omitted -->
<h3 id="animation-fill-mode-動畫播放前後的狀態"><code>animation-fill-mode</code> 動畫播放前後的狀態</h3>
<p>如果想要控制動畫播放完後的最終狀態，可以用 <code>animation-fill-mode</code> 屬性，它可設定的值如下：</p>
<ul>
<li><code>none</code> ：回到未播放動畫效果前的狀態</li>
<li><code>forwards</code> ：停在動畫的最後一個狀態上</li>
<li><code>backwards</code> ：停在動畫的第一個狀態上 (實測不出來)</li>
<li><code>both</code> ：視 <code>animation-direction</code> 來決定停在哪一個狀態上。</li>
</ul>
<!-- raw HTML omitted -->
<p>註： <code>backwards</code> 這個值我在 Chrome 和 Firefox 都試不出來。</p>
<h3 id="animation-play-state-指定動畫播放或暫停"><code>animation-play-state</code> 指定動畫播放或暫停</h3>
<p><code>animation-play-state</code> 有兩個屬性值： <code>running</code> 及 <code>paused</code> ，其中 <code>running</code> 是預設值。</p>
<p>這個屬性必須獨立定義，無法被放在 <code>animation</code> 屬性裡。</p>
<!-- raw HTML omitted -->
<h3 id="瀏覽器支援-1">瀏覽器支援</h3>
<p><code>animation</code> 屬性目前在 IE 10+ 以上主流瀏覽器都可以執行，但採用 Webkit 引擎的瀏覽器必須加上 <code>-webkit-</code> 前綴字串。</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="nt">div</span><span class="p">:</span><span class="nd">hover</span> <span class="p">{</span>
  <span class="kp">-webkit-</span><span class="k">animation</span><span class="p">:</span> <span class="mi">1</span><span class="kt">s</span> <span class="n">name</span><span class="p">;</span>
  <span class="k">animation</span><span class="p">:</span> <span class="mi">1</span><span class="kt">s</span> <span class="n">name</span><span class="p">;</span>
<span class="p">}</span>

<span class="p">@</span><span class="k">-webkit-keyframes</span> <span class="nt">name</span> <span class="p">{</span>
    <span class="o">...</span>
<span class="p">}</span>

<span class="p">@</span><span class="k">keyframes</span> <span class="nt">name</span> <span class="p">{</span>
    <span class="o">...</span>
<span class="p">}</span>
</code></pre></div><h2 id="實例">實例</h2>
<p>接下來我們用 Animation 搭配 Transform 來做簡單的旋轉動畫。 Transform 是用來讓 HTML 元素變形的屬性，雖然跟動畫沒有直接的關係，但它是可以套用動畫效果的。這邊我不打算詳細介紹它，只會用到旋轉的效果。</p>
<p>它的語法如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="nt">div</span> <span class="p">{</span>
    <span class="k">transform</span><span class="p">:</span> <span class="nb">rotate</span><span class="p">(</span><span class="err">θ</span><span class="p">);</span>
    <span class="k">transform-origin</span><span class="p">:</span> <span class="n">x</span> <span class="n">y</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p><code>rotate(θ)</code> 是指讓指定元素以參考點為中心軸 2D 旋轉 θ 度， <code>transform-origin</code> 會將 <code>(x, y)</code> 設為參考點。當我們把 <code>transform: rotate(θ)</code> 放到 <code>@keyframes</code> 中時， <code>animation</code> 就會改變 <code>θ</code> 值來做出動畫效果。</p>
<p>以下模擬簡單的太陽、地球、月亮的週期變化。</p>
<!-- raw HTML omitted -->
<p>更酷的範例參考：</p>
<ul>
<li><a href="http://www.creativebloq.com/css3/animation-with-css3-712437">20 stunning examples of CSS3 animation</a></li>
<li><a href="http://neography.com/experiment/circles/solarsystem/">Our Solar System</a></li>
<li><a href="http://designscrazed.com/css3-animation-examples/">30 Best Creative CSS3 Animation Examples</a></li>
<li><a href="http://goo.gl/SHWmEx">Codepen.io CSS Animation</a></li>
</ul>
<h2 id="開發工具">開發工具</h2>
<h3 id="css-30-maker">CSS 3.0 Maker</h3>
<p><a href="http://www.css3maker.com">CSS 3.0 Marker</a> 可以讓我們調整 CSS3 相關屬性的參數，並預覽效果。確認後就可以產生對應的 CSS 碼，套用到專案上。</p>
<h3 id="animatecss">Animate.css</h3>
<p><a href="http://daneden.github.io/animate.css/">Animate.css</a> 這個 CSS framework 提供很多組已經定義好動畫效果的 CSS class ，讓我們可以直接套在 HTML 元素上，或是搭配 jQuery 來操作 class 來產生動畫效果。</p>
<!-- raw HTML omitted -->
<h3 id="animate-mixin-for-compasssass">Animate Mixin for Compass/SASS</h3>
<p><a href="http://thecssguru.freeiz.com/animate/">Animate Mixin for Compass/SASS</a> 提供了一組很棒的 CSS3 Animation mixins ，讓我們可以直接套用。它其實就是從 Animation.css 移植過來的。</p>
<h3 id="anijs">AniJS</h3>
<p><a href="http://anijs.github.io/">AniJS</a> 是一個宣告式的 CSS 動畫 library ，它讓我們可以在 HTML 元素中加入一個 <code>data-anijs</code> 屬性，並用敘述式來定義動作事件、動畫效果、以及要作用在哪個元素上。要特別注意的是，它也必須搭配 Animation.css 使用。</p>
<!-- raw HTML omitted -->
]]></content>
		</item>
		
		<item>
			<title>20 個實用的前端開發參考資訊整理</title>
			<link>https://jaceju.net/20-docs-guides-front-end-developers/</link>
			<pubDate>Fri, 25 Apr 2014 14:55:29 +0800</pubDate>
			
			<guid>https://jaceju.net/20-docs-guides-front-end-developers/</guid>
			<description>原文連結： 20 Useful Docs and Guides for Front-End Developers 看到上面的文章收集了許多前端開發參考資訊，覺得非常實用，故將重點整理如下： CSS Vocabulary 可以瞭解 CSS 中的名詞實際對應的部份。 Liquidapsive</description>
			<content type="html"><![CDATA[<p>原文連結： <a href="http://www.sitepoint.com/20-docs-guides-front-end-developers/">20 Useful Docs and Guides for Front-End Developers</a></p>
<p>看到上面的文章收集了許多前端開發參考資訊，覺得非常實用，故將重點整理如下：</p>
<!-- raw HTML omitted -->
<ol>
<li><a href="http://pumpula.net/p/apps/css-vocabulary/">CSS Vocabulary</a>
可以瞭解 CSS 中的名詞實際對應的部份。</li>
<li><a href="http://liquidapsive.com/">Liquidapsive</a>
介紹 Responsive / Adaptive / Liquid / Static 四種排版方式及其差異，網站本身即為實例。</li>
<li><a href="http://superherojs.com/">Superhero.js</a>
這個網站收集非常多有關 JavaScript 的文件、簡報或影片！</li>
<li><a href="http://howtocoffeescript.com/">HowToCoffeeScript.com</a>
把許多常用的 CoffeeScript 技巧整理成速查表。</li>
<li><a href="http://www.w3.org/html/landscape/">The HTML Landscape</a>
介紹 WHATWG / W3C’s HTML5.0 / W3C’s HTML5.1 三種 HTML 規格的差異。</li>
<li><a href="http://rawgithub.com/w3c/elements-of-html/master/index.html">The Elements of HTML</a>
把 2.0 ~ 5.1 各個版本的 HTML 元素整理出來了，非常詳盡！</li>
<li><a href="http://dorey.github.io/JavaScript-Equality-Table/">JavaScript Equality Table</a>
用二維表的形式來呈現 JavaScript 的 == / === / if 是如何比對值。</li>
<li><a href="http://a11yproject.com/checklist.html">Web Accessibility Checklist</a>
列出專案如果要達成無障礙所需要注意的項目。</li>
<li><a href="http://www.staticapps.org/">Static Web Apps — A Field Guide</a>
列出了常見的 Web Apps 開發注意事項或解決方案。</li>
<li><a href="http://qntm.org/files/re/re.html">Learn regular expressions in about 55 minutes</a>
列出正規表達式學習的重點，並輔以範例供參考。</li>
<li><a href="http://ref.openweb.io/CSS/">Open Web CSS Reference</a>
這個網站整理了 CSS 屬性與其進階特色的 W3C 連結。</li>
<li><a href="http://cssvalues.com/">CSS Values</a>
輸入 CSS 屬性後，可以看到它的屬性值參考、瀏覽器相容性及相關連結。</li>
<li><a href="https://github.com/lukehoban/es6features">ES6features</a>
整理了 ECMAScript 6 的特色。</li>
<li><a href="https://github.com/mozilla/servo/wiki/Relevant-spec-links">Relevant Spec Links</a>
列出許多有關前端技術的規格連結。</li>
<li><a href="http://overapi.com/">OverAPI.com</a>
幾乎把所有有關網站開發的語言或工具所使用的 API 都整理成速查表了。</li>
<li><a href="http://jstherightway.org/">JavaScript: The Right Way</a>
整理了所有有關 JavaScript 的開發相關資訊。</li>
<li><a href="http://html5index.org/">The HTML5 JavaScript API Index</a>
整理了 HTML5 在 JavaScript 的所有 API 。</li>
<li><a href="http://zealdocs.org/">Zeal</a>
類似 Mac 上的 Dash 參考文件整合軟體，是給 Linux / Windows 使用者。</li>
<li><a href="http://www.sketchingwithcss.com/samplechapter/cheatsheet.html">The Ultimate Flexbox Cheat Sheet</a>
整理有關 CSS FlexBox 的教學。</li>
<li><a href="http://jscode.org/">jsCode</a>
可以自訂並產生 JavaScript Coding Guideline 的服務。</li>
</ol>
]]></content>
		</item>
		
		<item>
			<title>ScrollSpy 簡介</title>
			<link>https://jaceju.net/about-scrollspy/</link>
			<pubDate>Fri, 21 Feb 2014 11:16:00 +0800</pubDate>
			
			<guid>https://jaceju.net/about-scrollspy/</guid>
			<description>在 Single Page Design 中，我們常會把落落長的頁面分成幾個區塊，然後在上方或側邊選單中以這些區塊的標題來做為選單項目。 而當我們點選選單項目時，頁面會自動跳到</description>
			<content type="html"><![CDATA[<p>在 Single Page Design 中，我們常會把落落長的頁面分成幾個區塊，然後在上方或側邊選單中以這些區塊的標題來做為選單項目。</p>
<!-- raw HTML omitted -->
<p>而當我們點選選單項目時，頁面會自動跳到 (或捲動到) 該區塊，而選單項目會反白。反過來如果我們捲動到該區塊時 (即該區塊佔了螢幕一定比例的大小，或是標頭到達某一特定位置) ，對應該區塊的選單項目也會自動反白。</p>
<p>這個效果就稱為「 ScrollSpy 」。</p>
<p>以下連結分享了許多已經採用 ScrollSpy 特效的網站，大家不妨參考看看：</p>
<p><a href="http://www.hongkiat.com/blog/scrollspy-navigation-websites/">http://www.hongkiat.com/blog/scrollspy-navigation-websites/</a></p>
<p>目前有很多套件可以做到 ScrollSpy 的效果了，例如 Bootstrap 3 就包含了這個套件：</p>
<p><a href="http://getbootstrap.com/javascript/#scrollspy">http://getbootstrap.com/javascript/#scrollspy</a></p>
<p>另外也有獨立的 jQuery Plugin ：</p>
<ul>
<li><a href="https://github.com/thesmart/jquery-scrollspy">https://github.com/thesmart/jquery-scrollspy</a></li>
<li><a href="https://github.com/sxalexander/jquery-scrollspy">https://github.com/sxalexander/jquery-scrollspy</a></li>
<li><a href="http://www.dynamicdrive.com/dynamicindex1/ddscrollspymenu.htm">http://www.dynamicdrive.com/dynamicindex1/ddscrollspymenu.htm</a></li>
</ul>
<p>如果你不想用 jQuery 或是想瞭解 ScrollSpy 的原理，以下便是一個使用純 JavaScript 所寫的 ScrollSpy 類別：</p>
<p><a href="https://gist.github.com/pascaldevink/2380129">https://gist.github.com/pascaldevink/2380129</a></p>
]]></content>
		</item>
		
		<item>
			<title>開發 Laravel 套件時的單元測試</title>
			<link>https://jaceju.net/unittest-in-laravel-package-development/</link>
			<pubDate>Thu, 12 Dec 2013 22:51:00 +0800</pubDate>
			
			<guid>https://jaceju.net/unittest-in-laravel-package-development/</guid>
			<description>在官方手上的有關開發 Laravel 4 套件的章節，內容其實寫得滿詳盡了。只是它缺少了有關單元測試的說明，以下我將介紹一些自己的做法和經驗。 前置作業 我們可以</description>
			<content type="html"><![CDATA[<p>在官方手上的<a href="http://laravel.com/docs/packages">有關開發 Laravel 4 套件的章節</a>，內容其實寫得滿詳盡了。只是它缺少了有關單元測試的說明，以下我將介紹一些自己的做法和經驗。</p>
<!-- raw HTML omitted -->
<h2 id="前置作業">前置作業</h2>
<p>我們可以用以下指令來建立一個新的 Laravel 套件：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">php artisan workbench --resource vendor/name
</code></pre></div><p>然後在專案目錄下的 <code>workbench/vendor/name</code> 路徑下找到我們的專案原始檔。</p>
<p>註： <code>vendor</code> 可以是公司名稱或開發者的名字，而 <code>name</code> 則是套件名稱。這裡雖然直接以這個名稱當範例，但實務上請不要這麼設定。</p>
<p>我們會看到目錄結構如下：</p>
<pre><code>.
├── composer.json
├── phpunit.xml
├── public
├── src
│   ├── Vendor
│   │   └── Name
│   │       └── NameServiceProvider.php
│   ├── config
│   ├── controllers
│   ├── lang
│   ├── migrations
│   └── views
└── tests
</code></pre><p>接下來不特別說明的話，所有操作都是在上述的路徑裡。</p>
<h2 id="composer-設定">Composer 設定</h2>
<p>我們假設套件會用到資料庫，所以第一步是把 <code>illuminate/database</code> 這個套件加進 <code>composer.json</code> 的 <code>require</code> 區段設定內：</p>
<pre><code>&quot;require&quot;: {
    ...
    &quot;illuminate/database&quot;: &quot;4.0.x&quot;
}
</code></pre><p>接著是 PHPUnit ，要將它寫在 <code>require-dev</code> 區段設定中：</p>
<pre><code>&quot;require-dev&quot;: {
    ...
    &quot;phpunit/phpunit&quot;: &quot;&gt;=3.7.0&quot;
}
</code></pre><p>然後執行 <code>composer update --prefer-dist</code> ，以安裝資料庫套件。</p>
<h2 id="model-類別基本結構">Model 類別基本結構</h2>
<p>這邊假設我們的 model 名稱為 <code>Ranger</code> ，它只有 <code>id</code> 和 <code>name</code> 兩個屬性。請在 <code>src/Vendor/Name</code> 這個路徑下建立一個 <code>Ranger.php</code> ，然後輸入以下內容：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">namespace Vendor\Name;

use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Schema\Blueprint as Table;

class Ranger extends Eloquent
{
    public static $tableName = &#39;rangers&#39;; // 自行加入的屬性

    protected $guarded = array(); // 視需求更改

    /**
     * @return callable
     */
    public static function getBlueprint() // 自行加入的方法
    {
        return function (Table $table) {
            $table-&gt;increments(&#39;id&#39;);
            $table-&gt;string(&#39;name&#39;, 100);
        };
    }
}
</code></pre></div><p>Laravel 的 Model 預設會自行找出類別名稱與資料表名稱的對應，然後將它設定在 <code>$table</code> 這個屬性中；不過為了稍後在做 migration 和單元測試時的需求，我自行加了一個 static 變數： <code>$tableName</code> 。</p>
<p>另一個 static 方法 <code>getBlueprint</code> 是回傳一個 <code>callback</code> 給 Schema Builder 使用，在後面我們會在製作 migration 與單元測試時會用到。</p>
<h2 id="單元測試">單元測試</h2>
<p>基本上單元測試的設定檔， Laravel 已經幫我們產生好了。我們只需要建立 model 對應的測試即可。</p>
<p>在 <code>tests</code> 目錄下再建立一個子資料夾 <code>NameTest</code> ，其中 <code>Name</code> 就是對應到我們的套件名稱。</p>
<p>然後在 <code>composer.json</code> 的 <code>psr-0</code> 區段中加入：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="s2">&#34;psr-0&#34;</span><span class="err">:</span> <span class="p">{</span>
    <span class="err">...</span>
    <span class="nt">&#34;Vendor\\NameTest&#34;</span><span class="p">:</span> <span class="s2">&#34;tests/&#34;</span>
<span class="p">}</span>
</code></pre></div><p>這會讓 composer 的自動載入找到我們的測試類別。</p>
<p>接著在 <code>NameTest</code> 資料夾中再建立一個 PHP 檔案 <code>RangerTest.php</code> ，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">namespace Vendor\NameTest;

use Vendor\Name\Ranger;
use Illuminate\Database\Capsule\Manager as DB;

class RangerTest extends \PHPUnit_Framework_TestCase
{

}
</code></pre></div><p>這邊比較關鍵的部份的是把 <code>Illuminate\Database\Capsule\Manager</code> 載入後，取別名為 <code>DB</code> 方便後續操作。</p>
<p>另外因為套件並沒辦法使用到 Laravel 在 application 中的 Facade 機制，所以直接用 <code>\PHPUnit_Framework_TestCase</code> 類別，而不是 Laravel 內建的 <code>TestCase</code> 類別。</p>
<h2 id="資料庫與資料表設定">資料庫與資料表設定</h2>
<p>在測試中，通常較為麻煩的是測試 Model 與資料庫之間的溝通。其實我們可以直接提供一個測試用的資料庫讓測試使用。</p>
<p>測試用的資料庫可以是 Laravel 所支援的任一類型關連式資料庫，這裡我們選用 sqlite 。</p>
<p>接著在 <code>RangerTest</code> 類別中加入以下程式碼：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">...

    protected static $db = null;

    public static function setUpBeforeClass()
    {
        static::connectTestDb();
        static::initTables();
    }

    protected static function connectTestDb()
    {
        static::$db = new DB();
        static::$db-&gt;addConnection(array(
            &#39;driver&#39;    =&gt; &#39;sqlite&#39;,
            &#39;database&#39;  =&gt; &#39;:memory:&#39;,
            &#39;prefix&#39;    =&gt; &#39;&#39;,
        ));
        static::$db-&gt;setAsGlobal();
        static::$db-&gt;bootEloquent();
    }

    protected static function initTables()
    {
        $conn = static::$db-&gt;getConnection();
        $builder = $conn-&gt;getSchemaBuilder();

        $builder-&gt;dropIfExists(Ranger::$tableName);
        $builder-&gt;create(Ranger::$tableName, Ranger::getBlueprint());
    }

</code></pre></div><p>在 PHPUnit 中提供了 <code>setUpBeforeClass</code> 這個方法，主要是在所有測試開始前要先做的動作；我們利用它來初始化資料庫連線及產生我們需要資料表。</p>
<p>註： <code>setUp</code> 是在每個 test case 啟動前做。</p>
<p><code>connectTestDb</code> 方法是對資料庫連線，讓接下來的 Model 可以直接操作，而不需要處理連線問題。這裡我們可以直接使用 sqlite 配合 <code>:memory:</code> 來使用記憶體當做測試資料庫，或是在 MySQL 或其他與正式機相同規格的資料庫伺服器上建立測試用的資料庫。</p>
<p><code>initTables</code> 用來建立資料表，由於我們在 model 的 <code>getBlueprint</code> 中會回傳 Schema 的資訊，所以就直接利用它來建資料表。</p>
<h2 id="開始測試">開始測試</h2>
<p>現在我們可以撰寫測試案例了，舉例如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">...

    public function testFind()
    {
        DB::table(&#39;rangers&#39;)-&gt;insert(array(
            &#39;name&#39; =&gt; &#39;Jace Ju&#39;,
        ));
        $ranger = Ranger::find(1);
        $this-&gt;assertEquals(&#39;Jace Ju&#39;, $ranger-&gt;name);
    }
</code></pre></div><p>接著開啟終端機，切換到 <code>workbench/vendor/name</code> 目錄下，執行：</p>
<pre><code>phpunit
</code></pre>
<p>就可以看到測試結果了。</p>
<h2 id="多個測試類別共用同一資料連線">多個測試類別共用同一資料連線</h2>
<p>目前我們是在 <code>RangerTest</code> 類別中連結資料庫，但通常我們會有很多 Model 需要被測試，所以需要共用上述的資料庫連結的部份。</p>
<p>先建立一個 <code>TestCase</code> 類別，它與 <code>RangerTest</code> 放在同一個目錄下。</p>
<p>再將先前的資料庫連結的部份複製到新的 <code>TestCase</code> 類別裡，成果如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">namespace Vendor\NameTest;

use Illuminate\Database\Capsule\Manager as DB;

class TestCase extends \PHPUnit_Framework_TestCase
{
    protected static $db = null;

    public static function setUpBeforeClass()
    {
        static::connectTestDb();
        static::initTables();
    }

    protected static function connectTestDb()
    {
        // ... 同上
    }

    protected static function initTables()
    {
        // ... 同上
    }
}
</code></pre></div><p>在 <code>initTables</code> 方法中，可以初始化所有會用到的資料表。</p>
<p>然後回到 <code>RangerTest</code> 類別，將連結資料庫的部份移除，並改為繼承 <code>TestCase</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">namespace Vendor\NameTest;

use Illuminate\Database\Capsule\Manager as DB;

class RangerTest extends TestCase
{
    // ...
}
</code></pre></div><p>這時候如果執行 <code>phpunit</code> 的話，會發現找不到 <code>TestCase</code> 這個類別。</p>
<p>解決方法是在 <code>composer.json</code> 裡面告訴 composer 要去哪裡找這個類別檔案：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json">    <span class="s2">&#34;classmap&#34;</span><span class="err">:</span> <span class="p">[</span>
        <span class="s2">&#34;tests/NameTest/TestCase.php&#34;</span>
    <span class="p">]</span><span class="err">,</span>
</code></pre></div><p>這麼一來， Model 與資料庫的測試就容易許多了。</p>
<h2 id="migration">Migration</h2>
<p>雖然我們可以測試 model 了，但實際作業還是需要把資料表建立在正式資料庫上。這裡就要透過 Laravel 的 migration 機制。</p>
<p>這裡就簡單說明一下剛剛在 <code>Ranger</code> 類別建立的 callback 如何使用：</p>
<p>首先要為 workbench 中 package 建立 migrations 資料夾：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">mkdir workbench/vendor/name/src/migrations
</code></pre></div><p>然後我們要為 package 建立一個 migration ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">php artisan migrate:make create_rangers_table --bench<span class="o">=</span>vendor/name
</code></pre></div><p>這會建立 <code>workbench/vendor/name/src/migrations/xxxx_xx_xx_xxxxxx_create_rangers_table.php</code> 這個檔案 (xx 會視建立時間而有所不同) ，其類別名稱為 <code>CreateRangersTable</code> 。</p>
<p>在 <code>CreateRangersTable</code> 中，我們可以直接利用先前在 <code>Ranger</code> 類別中定義的 <code>$tableName</code> 與 <code>getBlueprint</code> 來完成 migration 的 <code>up</code> 及 <code>down</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">use Illuminate\Database\Migrations\Migration;
use Vendor\Name\Ranger;

class CreateRangersTable extends Migration
{
    public function up()
    {
        Schema::create(Ranger::$tableName, Ranger::getBlueprint());
    }

    public function down()
    {
        Schema::drop(Ranger::$tableName);
    }
}
</code></pre></div><p>這麼一來就不需要重複定義 schema 的程式碼了。</p>
<h2 id="總結">總結</h2>
<p>最後歸納幾個重點：</p>
<ol>
<li>
<p>將 Schema Builder 需要的 callback 放在 Model 中，讓測試與 migration 可以共用同一個 schema 。</p>
</li>
<li>
<p>利用 sqlite 的 <code>:memory:</code> 來做為測試資料庫是非常方便的。</p>
</li>
<li>
<p>將資料庫連結的程式碼移到共用類別中。</p>
</li>
<li>
<p>利用在 model 定義的 <code>$tableName</code> 與 <code>getBlueprint</code> 來完成 table migration 。</p>
</li>
</ol>
<p>歡迎指正或提供更好的建議。</p>
]]></content>
		</item>
		
		<item>
			<title>網站架構與部署策略筆記</title>
			<link>https://jaceju.net/website-deploy-note/</link>
			<pubDate>Sat, 23 Mar 2013 03:26:00 +0800</pubDate>
			
			<guid>https://jaceju.net/website-deploy-note/</guid>
			<description>這陣子為了公司的網站搞得自己焦頭爛額，其實自己很清楚在系統這個方面的涉獵並不深，但既然接了這個任務就得想辦法做好。 不過也因為不夠熟悉，讓系統</description>
			<content type="html"><![CDATA[<p>這陣子為了公司的網站搞得自己焦頭爛額，其實自己很清楚在系統這個方面的涉獵並不深，但既然接了這個任務就得想辦法做好。</p>
<p>不過也因為不夠熟悉，讓系統出了很多問題。因此公司就請了我的好友 John 和他的師父來給我們一些系統層面的建議。這份筆記是主要是記錄我從他們口中所得到的心得，當然詳細的資訊還是需要花時間去研究與實作。而裡面有一些名詞是我沒有聽過的，所以可能記錯。</p>
<p>以下就是我個人覺得比較關鍵的部份：</p>
<!-- raw HTML omitted -->
<h2 id="virtual-machine">Virtual Machine</h2>
<p>機器一來時，別急著在上面建立相關的 Web 服務或資料庫系統。</p>
<p>其實一台實體主機上面可以跑多個虛擬機器 (Virtual Machine, VM)  只要視機器等級切出適當的 VM 數量，這樣我們就能有效建立出一個多節點的系統。</p>
<p>另外 VM 也非常容易管理，像是安裝好第一個基本的 VM 系統後就隨時可以複製出新的 VM ，或是在某個 VM 爛掉時刪除它；這樣的方式就可以大大減少部署機器所花費的心力。</p>
<p>剛開始可以先使用 VMware vSphere 的免費版本，它有 Windows 版本的管理 UI 介面。</p>
<ul>
<li><a href="http://www.tenlong.com.tw/items/986868921X?item_id=437567">VMware vSphere 5 企業建置教戰手扎</a></li>
</ul>
<h2 id="將-log-紀錄獨立存放">將 Log 紀錄獨立存放</h2>
<p>Log 檔的處理也是一個很大的學問，比較好的方法是建立一台 syslogd 伺服器，然後把 log 都往這邊丟。這台伺服器等級不需要太好，但磁碟空間一定要夠大。</p>
<p>用 syslogd 的一個好處是，它是射後不理的，我們的服務只要向它丟 Log 即可，就算它掛了也不會影響我們的所有服務。</p>
<p>另外可以寫一個 Watch Dog 來分析 Log 中的惡意連結，當出現關鍵字的 Pattern 時，就記錄下這個 IP ，再利用 script 將這個 IP 發送到各節點上，將它擋下！</p>
<ul>
<li><a href="http://www.oreillynet.com/pub/a/sysadmin/2006/10/12/httpd-syslog.html">Sending Apache httpd Logs to Syslog</a></li>
<li><a href="http://www.weithenn.org/cgi-bin/wiki.pl?Syslog-%E6%9E%B6%E8%A8%AD_Log_%E4%BC%BA%E6%9C%8D%E5%99%A8">Syslog-架設 Log 伺服器</a></li>
</ul>
<h2 id="高併發要求">高併發要求</h2>
<p>靜態內容網站一定要使用 Reverse Proxy ，它可以提高網站的吞吐量。而網站是動態內容的話，就一定要想辦法讓它分散到各節點上。</p>
<p>而一般大流量網站通常不會有防火牆，而是用機海來應付。因為防火牆在大流量的狀況下，反而容易掛點。</p>
<h2 id="系統擴展">系統擴展</h2>
<p>服務與資料庫一定要考慮到橫向擴充，在任何時候都可以將一台機器加入或移除，而不影響到整體運作。</p>
<p>以前的架構是一台 Master 對多台 Slave ，而 Master 掛掉後就由所有的 Slave 推舉出新的 Master ，但這樣一來就不知道誰是 Master 了。</p>
<p>改良的做法是環狀結構，讓每一台機器都是下一台機器的 Master ，同時也是上一台機器的 Slave 。這樣如果其中一台掛點，系統就會主動再將這個環重新連結。</p>
<h2 id="兩層式的程式部署">兩層式的程式部署</h2>
<p>程式的部署需要自動化，而兩層式部署是比較簡單的方式，一層為 testing ，一層為 release 。</p>
<p>方法是在版本控制系統裡面將程式分成 testing 及 release 兩個分支， testing 分支是目前最新的程式碼，而 release 分支則是最穩定的程式碼。</p>
<p>在 testing 環境裡的程式碼必須都是可以執行無誤的，不可以有未完成的功能。當 testing 環境做完壓力測試後，管理者就可以程式碼放到 release 分支中。而所有的 production 都會主動查看 release 分支，當有新版本時就會自動更新。</p>
<p>不過兩層式部署還是有風險在，所以通常還會有 release candidate 版本的分支。有些大公司甚至做到了 16 層，也就是說所有的功能都必須經過 16 道檢驗才能上線。</p>
<h2 id="壓力測試">壓力測試</h2>
<p>利用 Switch 的 port mirroring 功能，將線上流量複製到準備要上線的機器，看它是否能撐住。通常這樣的壓力測試要跑三天至一週，確保系統的穩定性。</p>
<p>甚至也可以再讓多個 port 做 mirroring ，利用兩倍以上的流量來測試。這樣的目的主要是找出該機器的極限，也同時瞭解需要多少機器才能撐住預估的流量。</p>
<h2 id="不影響運作的系統更新">不影響運作的系統更新</h2>
<p>系統在更新程式前，會先把自己從 Load Balance 裡移下來，等確定沒問題後自動上線。</p>
<p>在資料庫系統方面，有些 Plugin 可以做到當 Slave 機器加入服務，與 Master 做同步資料的時候，先將自己隱藏起來，等到同步完成後，再讓服務啟動並上線。</p>
<h2 id="心得">心得</h2>
<p>據 John 的師父說，他們已經做到將這些行為完全自動化，並且只要在 Dashboard 上就可以看到所有服務的狀態；甚至後來還以紅綠燈配合聲音來做警示，聽完後我下巴都掉了。</p>
<p>事實上在人力有限，卻要管理幾十台機器的狀況下，這樣的做法才是身為一個技術人員應該去做的。</p>
<p>再次感謝 John 和他師父，讓我學到不少有關系統管理方面的知識。</p>
]]></content>
		</item>
		
		<item>
			<title>網站技術發展史</title>
			<link>https://jaceju.net/webdev-history/</link>
			<pubDate>Wed, 21 Nov 2012 09:26:00 +0800</pubDate>
			
			<guid>https://jaceju.net/webdev-history/</guid>
			<description>前言 這篇主要是對 Web 技術的發展史做一個概略的介紹，讓大家對目前 Web 技術的演變能有初步的認知。 不過製作網站的技術很多，這裡我僅針對瀏覽器、 HTML 、 CSS 及</description>
			<content type="html"><![CDATA[<h2 id="前言">前言</h2>
<p>這篇主要是對 Web 技術的發展史做一個概略的介紹，讓大家對目前 Web 技術的演變能有初步的認知。</p>
<p>不過製作網站的技術很多，這裡我僅針對瀏覽器、 HTML 、 CSS 及 JavaScript 做粗略的演進說明，細節部份就請大家參考維基百科或其他更深入的資訊。</p>
<p>另外文章內容或多或少會帶有我個人的主觀意見，而我也儘可能透過網路上的資訊作查證，但一定會有錯誤及不足之處，還望大家能夠指正或補足。</p>
<!-- raw HTML omitted -->
<h2 id="1991">1991</h2>
<p>HTML 最早是由網際網路 (World Wide Web) 之父 <a href="http://en.wikipedia.org/wiki/Tim_Berners-Lee">Tim Berners-Lee</a> 所發明，而在 1991 年成為公開的文件規範。而該規範並不是 HTML 1.0 ，而是稱為 HTML Tags ；當時的 HTML 主要是用來表達資料，支援的標籤也不多。</p>
<h2 id="1994">1994</h2>
<p>HTML 實際上成為規範是從 1994 年由 IETF 制定的 HTML 2.0 開始。這時候 MOSAIC 是市場上主要的瀏覽器。另外這時 CSS 概念被提出，雖然在這之前也已經有人提出樣式的構想，不過還是由 CSS 的層疊概念出線了。</p>
<h2 id="1995">1995</h2>
<p>除了 MOSAIC ，1995 年有三個瀏覽器也問市了： Opera 、 Netscape 和微軟的 IE 。而 Netscape 為了能讓網頁可以跟使用者互動，因此找了 <a href="http://en.wikipedia.org/wiki/Brendan_Eich">Brendan Eich</a> 設計一個腳本語言，當時稱為 LiveScript 。後來 Netscape 在與 Sun 合作之後，便將它改名為 JavaScript 。</p>
<p>JavaScript 最初是受到 Java 啟發而開始設計的，目的之一就是「看上去像 Java 」，但它實際上跟 Java 一點關係也沒有。而且早期的 JavaScript 並不是很完整的程式語言，只是用來給網頁開發者作一些動態選單、圖片特效的網頁小程式。</p>
<h2 id="1996">1996</h2>
<p>由於 JavaScript 作為網頁的客戶端腳本語言非常成功，微軟推出了 IE 3.0 ，上面搭載了跟 JavaScript 相容的 JScript 。</p>
<p>而後 Netscape 將 JavaScript 提交給 ECMA (歐洲計算機製造商協會) 進行標準化，因而建立了 ECMA-262 標準，也就是 ECMAScript 。</p>
<p>在這之前由 W3C 召開了一次 CSS 討論會，由原提出者 <a href="http://en.wikipedia.org/wiki/H%C3%A5kon_Wium_Lie">Håkon Wium Lie</a> 當做主要技術負責人，於是 CSS 1.0 規格出版；而 IE 3.0 也是第一個正式支援 CSS 的瀏覽器。</p>
<h2 id="1997">1997</h2>
<p>隨著 Windows 95 的推出，微軟也將 Internet Explorer 4.0 整合進去，讓作業系統跟瀏覽器核心綁在一起，也造成日後的瀏覽器大戰。</p>
<p>這時 HTML 4.0 被列為推薦規範， ECMAScript 也推出正式的 1.0 版。</p>
<h2 id="1998">1998</h2>
<p>CSS 2.0 在 1998 年正式推出，並且隨著 HTML 4.0 支配了網站設計這個領域。而這時 XML 也正式成為 W3C 推薦標準，為後來的 XHTML 開始鋪路。</p>
<p>有鑑於這時各家瀏覽器實作 HTML 及 CSS 有所差異，因此 Web 標準計劃 (The Web Standards Project) 創立了。目的是希望讓網頁在瀏覽器上的呈現能夠有一致性。只要照標準寫的網頁，可以在各家瀏覽器上呈現出一致的效果。</p>
<p>AJAX 的前身技術 XMLHTTP 也隨著微軟的 Outlook Web Access 出現。</p>
<h2 id="1999">1999</h2>
<p>到了 1999 年， .COM 泡沫發展到了極致，也是瀏覽器大戰最火熱的時候， IE 5.0 與 Windows 98 第二版的結合開始瓜分了 Netscape 的市場。</p>
<p>這時 HTML 4.01 推薦版本也推出了，跟 HTML 3.2 及 HTML 4.0 相同，它們都是針對已經上市的規格所做的追溯版本。</p>
<p>接下來 W3C 不打算再維護 HTML ，而是把重心轉移到 XHTML 上。</p>
<h2 id="2000">2000</h2>
<p>2000 年時，我們平安渡過千禧夜。這時以 IE 5.5 為展示平台的 DHTML 動態網頁技術被大量使用在網站上。</p>
<p>也在這一年 XHTML 1.0 推出了，它的語法要求很嚴格，如果瀏覧器用這個規範來看網頁的話，會使得現在很多網站會無法運作。</p>
<h2 id="2001">2001</h2>
<p>接下來 .COM 泡沫一個接一個的破了，但 Microsoft.com 靠著 Windows 活了下來。</p>
<p>瀏覽器大戰也由 Windows 內建的 IE 6.0 獲勝，堪稱網站技術史最光明的時期，因為我們只需要專注一個瀏覽器。</p>
<p>XHTML 正式的規範到 1.1 ，不過限制也更為嚴格，連 mime-type 都必須是 application/xhtml+xml 。</p>
<p>也在這一年，由 JavaScript 大師 <a href="http://en.wikipedia.org/wiki/Douglas_Crockford">Douglas Crockford</a> 提出了用 JSON 格式來表達資料內容。</p>
<h2 id="2002">2002</h2>
<p>2002 年，這時候的 W3C 打算推出更嚴格的 XHTML 2.0 。</p>
<p>而且 Web 標準意識開始抬頭，很多網站技術的先驅開始推行符合 Web 標準的網站技術。</p>
<p>不過為了照顧高市佔率的 IE 5.5 及 IE 6.0 ，使得許多 CSS Hack 技術不斷地被研究出來。</p>
<h2 id="2003">2003</h2>
<p>這時候 IE 6.0 已經獨霸市場，幾乎沒有對手可以相比。在贏得瀏覽器大戰的微軟，開始輕忽網站平台這個領域，他們把重心放在了 .NET ，他們認為這個是次世代的技術。</p>
<p>不過這時候其他瀏覽器廠商開始準備反攻了，他們的武器就是 Web 標準。其中與 Netscape 同時期誕生的 Opera 可以說是 Web 標準的推手。以 Webkit 為核心的 Safari 也正式推出 1.0 版。</p>
<h2 id="2004">2004</h2>
<p>Mozilla 因為重新改寫 Netscape 並開放了原始碼，進而在 2004 年推出了 Firefox 1.0 。對於擁抱 Web 標準的開發者來說是個好消息，這使得 Firefox 的市佔率在一推出就出乎預料地高。</p>
<p>這一年 CSS 2.1 草案推出，主要是修正 CSS 2.0 的錯誤，並且去除掉一些瀏覽器沒有實作的功能。</p>
<p>這時 <a href="http://en.wikipedia.org/wiki/Tim_O%27Reilly">Tim O&rsquo;Reilly</a> 提出了我們所熟知的 Web 2.0 ；因為 Web 2.0 會用到大量的使用者互動，因此這時 JavaScript 又開始被重視了。</p>
<p>但這時候的 HTML 及 CSS 實作實在是太雜亂，又加上 W3C 打算用 XHTML 整治的手段太過激進，因此三家瀏覽器廠商 Apple 、 Mozilla 、 Opera 就跳出來成立了 WHATWG 這個組織。 WHATWG 的目的是把現有的 HTML 實作做一個完整的規範，並且與當代流行的技術做結合。不久，他們就推出了 Web Application 1.0 。</p>
<h2 id="2005">2005</h2>
<p>由於 IE 樹大招風，許多木馬及蠕蟲都透過 IE 的漏洞入侵，因此許多瀏覽器用戶轉而投向了 Firefox 的懷抱。</p>
<p>AJAX 這個技術被 <a href="http://en.wikipedia.org/wiki/Jesse_James_Garrett">Jesse James Garrett</a> 正式命名，隨後也被納入 Web 2.0 關鍵技術中，從此網站技術正式邁進 AJAX 時代。</p>
<h2 id="2006">2006</h2>
<p>除了 IE 6.0 之外，其他瀏覽器因為起跑很久了，所以幾乎都可以在很小的工作量下就可以符合 Web 標準。但因為 IE 6.0 的核心一直都沒有進步，結果被網站開發者罵到臭頭。微軟也發現 Firefox 逐漸吃掉瀏覽器市場，所以推出了 IE 7.0 。但基本上 IE 7.0 也只是 IE 6.0 的功能加強版，跟 Web 標準落差還是很大。</p>
<p>至於 W3C 的 XHTML 2.0 根本沒有瀏覽器廠商願意實作，所以宣告失敗。</p>
<p>CSS 3 的部份規格已經被某些瀏覽器實作了，而 IE 仍然不直接支援，只能用自家的技術來模擬。</p>
<h2 id="2007">2007</h2>
<p>因為 XHTML 2.0 的失敗， W3C 只好回頭接受 WHATWG 所制定的規格，並改稱為 HTML 5 。</p>
<p>HTML 5 時代正式來臨。這也告訴我們：只有被市場所接受的才是贏家。</p>
<h2 id="2008">2008</h2>
<p>以搜尋引擎起家的 Google 也利用 Webkit 核心推出了 Chrome 瀏覽器。</p>
<p>這在當時造成了不少話題，很多網站開發者都非常期待 Google 能為網站技術帶來一些新氣象。</p>
<h2 id="2009">2009</h2>
<p>許多報告都指出， Chrome 和 Firefox 開始分食 IE 的市場，直到這時微軟才驚覺網站平台的時代來臨了。這時微軟也針對 HTML 5 推出 IE 8.0 ，但支援程度依舊非常落後，不過因為 Windows 7 的關係，使得它還是佔有一席之地。</p>
<p>當然也不是只有 IE 在進步，其他瀏覽器也持續著他們自己的腳步；在 Web 標準上， Opera 一直是先驅，幾乎很多 HTML 5 的特性都是它先實作出來。 Firefox 則是將重心放在外掛套件上面，對 HTML 5 的支援採取保守的態度。而 Safari 也因為使用與 Chrome 相同核心的 Webkit ，所以正在迎頭趕上。</p>
<p>倒是 Chrome 瀏覽器因為其開發版本一直更新的關係，所以正式版本的版號推進速度非常快，讓其他瀏覽器廠商嚇了一大跳。</p>
<h2 id="2011">2011</h2>
<p>針對 HTML5 時代的到來，微軟終於趕上了， IE 9.0 可以說是完全重新出發的作品。</p>
<blockquote>
<p>瀏覽器大戰又再次開打了，不過戰場不再是在瀏覽器上，而是應用平台。像是 Chrome OS 、 Firefox OS 、 Windows 8 等等，各家廠商都想透過公開或私有的 HTML5 API 去存取裝置原生的功能，表面上看起來是共同推進 HTML5 規範，實際上卻是你來我往，暗潮洶湧。 (本段文字感謝 <a href="http://www.plurk.com/kurotanshi">Kuro</a> 補充)</p>
</blockquote>
<p>而 CSS 2.1 也終於定案了，但這只是 W3C 對它主導地位的一點掙扎。這時 CSS 3 早就已經被各大瀏覽器所支援。從這裡可以看出， W3C 的角色其實更像歷史的紀錄者，而非技術的制訂者。</p>
<h2 id="2012">2012</h2>
<p>前陣子 WHATWG 的頭頭又出來說話，他們覺得 W3C 是要穩定的規格，而他們則是隨著時代而演進。因此 WHATWG 的 HTML 5 改名為 HTML Living Standard 。</p>
<p>所以接下來幾年可以說是 HTML 5 、 CSS 3 與 JavaScript 的天下了。</p>
<p>不過<a href="https://speakerdeck.com/stopsatgreen/the-css-of-tomorrow-revised">新的 CSS 構想</a>已經悄悄地出現了， Web 開發技術也持續在演進中。</p>
<p>To be continued…</p>
<h2 id="參考">參考</h2>
<ul>
<li><a href="http://www.w3.org/MarkUp/draft-ietf-iiir-html-01.txt">Hypertext Markup Language</a></li>
<li><a href="http://en.wikipedia.org/wiki/HTML">HTML (維基百科)</a></li>
<li><a href="http://en.wikipedia.org/wiki/CSS">CSS (維基百科)</a></li>
<li><a href="http://en.wikipedia.org/wiki/ECMAScript">ECMAScript (維基百科)</a></li>
<li><a href="http://en.wikipedia.org/wiki/JavaScript">JavaScript (維基百科)</a></li>
<li><a href="http://zh.wikipedia.org/wiki/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%A4%A7%E6%88%98">瀏覽器大戰 (維基百科)</a></li>
<li><a href="http://www.evolutionoftheweb.com/?hl=zh-tw">網路演進</a></li>
<li><a href="http://www.w3.org/">W3C</a></li>
<li><a href="http://www.webstandards.org/">The Web Standards Project</a></li>
<li><a href="http://www.whatwg.org/">WHATWG</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>用 Imagick 建立 CSS Sprites</title>
			<link>https://jaceju.net/create-css-sprites-with-imagick/</link>
			<pubDate>Mon, 11 Jun 2012 12:01:00 +0800</pubDate>
			
			<guid>https://jaceju.net/create-css-sprites-with-imagick/</guid>
			<description>這是工作筆記，介紹如何用 Imagick 製作出 CSS Sprites 。 工具 這個功能需要在系統上安裝以下工具： ImageMagick ImageMagick 是一個圖形處理套件，請參考作系統版本安裝；以 Ubuntu 為例，安裝方法</description>
			<content type="html"><![CDATA[<p>這是工作筆記，介紹如何用 Imagick 製作出 CSS Sprites 。</p>
<!-- raw HTML omitted -->
<h2 id="工具">工具</h2>
<p>這個功能需要在系統上安裝以下工具：</p>
<h3 id="imagemagick">ImageMagick</h3>
<p>ImageMagick 是一個圖形處理套件，請參考作系統版本安裝；以 Ubuntu 為例，安裝方法為：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">sudo apt-install imagemagick libmagickwand-dev libmagickcore-dev
</code></pre></div><h3 id="imagick">imagick</h3>
<p>imagick 讓 PHP 可以透過 ImageMagick 來處理圖片。</p>
<p>如果 Ubuntut 上的 PHP 版本為 5.3 的話，可以用以下指令安裝：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">sudo apt-get install php5-imagick
</code></pre></div><p>不過如果安裝的是 PHP 5.4 的話，那麼就沒辦法直接用 <code>apt-get</code> 指令來安裝，我們要從 PECL 的官方網站下載最新的 imagick 來安裝：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">wget http://pecl.php.net/get/imagick-3.1.0RC2.tgz
tar xzvf imagick-3.1.0RC2.tgz
<span class="nb">cd</span> imagick-3.1.0RC2/
phpize
./configure
make
sudo make install
</code></pre></div><p>然後在 <code>/etc/php5/mods-available</code> 中新增一個 <code>imagick.ini</code> 檔，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-ini" data-lang="ini"><span class="na">extension</span><span class="o">=</span><span class="s">imagick.so</span>
</code></pre></div><p>再用以下指令讓 PHP 把把上面的設定檔連結進來：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nb">cd</span> /etc/php5/conf.d
sudo ln -s ../mods-available/imagick.ini 30-imagick.ini
</code></pre></div><p>註：各家作業系統設定 php.ini 的方式不一定相同。</p>
<p>最後記得重新啟動 Apache 來重新載入 PHP 設定。</p>
<p>現在我們可以在 PHP 程式中使用 imagick 這個套件了，來看看我們怎麼製作 CSS sprites 。</p>
<h2 id="開始動工">開始動工</h2>
<p>CSS Sprites 的基本原理如下：</p>
<ol>
<li>將許多小圖 (通常是同寬或同長) 排列在一起，合併為一大張。</li>
<li>然後透過 CSS 的 <code>background-image</code> 屬性將它當成是元素的背景。</li>
<li>最後再用 <code>background-position</code> 、 <code>width</code> 與 <code>height</code> 屬性調整小圖要顯示的位置及大小。</li>
</ol>
<p>這裡要介紹的是同尺寸的圖片合併成 CSS Sprites 的技巧，首先我們準備以下的圖片：</p>
<ul>
<li><img src="/resources/css_sprites_imagick/clock.png" alt="Clock">: <code>img/clock.png</code></li>
<li><img src="/resources/css_sprites_imagick/disc.png" alt="Disc">: <code>img/disc.png</code></li>
<li><img src="/resources/css_sprites_imagick/mail.png" alt="Mail">: <code>img/mail.png</code></li>
<li><img src="/resources/css_sprites_imagick/gear.png" alt="Gear">: <code>img/gear.png</code></li>
<li><img src="/resources/css_sprites_imagick/terminal.png" alt="Terminal">: <code>img/terminal.png</code></li>
</ul>
<p>它們的寬高都是 32px 。</p>
<p>這裡我用 PNG 檔示範，當然也可以用 JPEG 檔或 GIF 檔；不過如果是動畫 GIF 檔的話，程式上要特別處理，這點稍後再提。</p>
<h3 id="產生-css-sprites">產生 CSS Sprites</h3>
<p>要用 Imagick 來產生 CSS Sprites 的話，首先利用 <code>glob</code> 函式搜集全部的小圖資訊：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$imagePaths = glob(__DIR__ . &#39;/img/*&#39;);
</code></pre></div><p><code>glob</code> 函式會幫我們取得 <code>img</code> 資料夾下所有檔案的完整路徑，並以陣列回傳。</p>
<p>然後我們就可以把它丟給 Imagick 處理：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$image = new Imagick();

// 一張一張的把圖疊到空白圖上
foreach ($imagePaths as $imagePath) {
    $sprite = new Imagick($imagePath);
    $image-&gt;addImage($sprite);
}

// 用附加在圖片之後的方式，製作出垂直的 sprites 圖片
$image-&gt;resetIterator();
$combined = $image-&gt;appendImages(true);

// 輸出格式為 PNG
$combined-&gt;setImageFormat(&#39;png&#39;);

// 將最後的圖片內容寫入檔案
$combined-&gt;writeImage(__DIR__ . &#39;/img/sprites.png&#39;);
</code></pre></div><p>這樣一來，就會得到 <code>img/sprites.png</code> 這個圖檔，內容如下：</p>
<p><img src="/resources/css_sprites_imagick/sprites.png" alt="sprites"></p>
<h3 id="產生-css-檔案">產生 CSS 檔案</h3>
<p>現在我們要讓產生 CSS 檔案，第一步把小圖的圖檔路徑，轉換成 CSS 的 class 名稱。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// 用圖檔名稱做為 CSS 的 class 名稱
$classNames = array();
foreach ($imagePaths as $imagePath) {
    $fileInfo = pathinfo($imagePath);
    $classNames[] = &#39;.icon-&#39; . $fileInfo[&#39;filename&#39;];
}
</code></pre></div><p>這裡我加上了 <code>icon-</code> 這個前置字串，目的是為了不讓產生出來的 class 跟現存的其他 class 衝突。例如 <code>mail.png</code> 產生出來的 class 名稱為 <code>icon-mail</code> 。</p>
<p>有了 class 名稱後，就可以設定它們的背景圖了。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// 把全部的 class 設為相同的不重覆背景圖。
$css = implode(&#39;, &#39;, $classNames);
$css .= &#34; {\nbackground: url(&#39;../img/sprites.png&#39;) no-repeat; \n}\n&#34;;
</code></pre></div><p>然後針對每個 class 名稱，加入對應的背景位置：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// 寫入每個 sprite 的背景位置定義
$y = 0;
foreach ($classNames as $className) {
    $css .= $className;
    $css .= &#34; {\nbackground-position: 0 -{$y}px\n}\n&#34;;
    $y += 32;
}
</code></pre></div><p>最後把得到的 CSS 內容，寫入 <code>css/sprites.css</code> 裡就可以了。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// 將最後的 CSS 內容寫入檔案
$cssPath = __DIR__ . &#39;/css/sprites.css&#39;;
file_put_contents($cssPath, $css);
</code></pre></div><p>產生出來的 CSS 檔案內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">.</span><span class="nc">icon-clock</span><span class="o">,</span><span class="p">.</span><span class="nc">icon-disc</span><span class="o">,</span><span class="p">.</span><span class="nc">icon-gear</span><span class="o">,</span><span class="p">.</span><span class="nc">icon-mail</span><span class="o">,</span><span class="p">.</span><span class="nc">icon-terminal</span> <span class="p">{</span>
<span class="k">background</span><span class="p">:</span> <span class="nb">url</span><span class="p">(</span><span class="s1">&#39;../img/sprites.png&#39;</span><span class="p">)</span> <span class="kc">no-repeat</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">.</span><span class="nc">icon-clock</span> <span class="p">{</span>
<span class="k">background-position</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">-0</span><span class="kt">px</span>
<span class="p">}</span>
<span class="p">.</span><span class="nc">icon-disc</span> <span class="p">{</span>
<span class="k">background-position</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">-32</span><span class="kt">px</span>
<span class="p">}</span>
<span class="p">.</span><span class="nc">icon-gear</span> <span class="p">{</span>
<span class="k">background-position</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">-64</span><span class="kt">px</span>
<span class="p">}</span>
<span class="p">.</span><span class="nc">icon-mail</span> <span class="p">{</span>
<span class="k">background-position</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">-96</span><span class="kt">px</span>
<span class="p">}</span>
<span class="p">.</span><span class="nc">icon-terminal</span> <span class="p">{</span>
<span class="k">background-position</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">-128</span><span class="kt">px</span>
<span class="p">}</span>
</code></pre></div><p>註：換行字元其實是可以不需要的，這裡只是為了方便檢查 CSS 的正確性而已。</p>
<h3 id="用法範例">用法範例</h3>
<p>簡單的用法如下，首先是 HTML ：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="cp">&lt;!DOCTYPE&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;zh-TW&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">&#34;UTF-8&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>CSS sprites<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;css/screen.css&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;css/sprites.css&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
  <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;icon-clock&#34;</span><span class="p">&gt;</span>Clock<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;icon-disc&#34;</span><span class="p">&gt;</span>Disc<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;icon-mail&#34;</span><span class="p">&gt;</span>Mail<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;icon-gear&#34;</span><span class="p">&gt;</span>Gear<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;icon-terminal&#34;</span><span class="p">&gt;</span>Terminal<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
  <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div><p>其中 <code>screen.css</code> 內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="nt">ul</span> <span class="o">&gt;</span> <span class="nt">li</span> <span class="p">{</span>
<span class="k">list-style</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
<span class="k">float</span><span class="p">:</span> <span class="kc">left</span><span class="p">;</span>
<span class="k">margin</span><span class="p">:</span> <span class="mi">3</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">ul</span> <span class="o">&gt;</span> <span class="nt">li</span> <span class="o">&gt;</span> <span class="nt">a</span> <span class="p">{</span>
<span class="k">display</span><span class="p">:</span> <span class="kc">block</span><span class="p">;</span>
<span class="k">width</span><span class="p">:</span> <span class="mi">32</span><span class="kt">px</span><span class="p">;</span>
<span class="k">height</span><span class="p">:</span> <span class="mi">32</span><span class="kt">px</span><span class="p">;</span>
<span class="c">/* hide text */</span>
<span class="k">text-indent</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
<span class="k">white-space</span><span class="p">:</span> <span class="kc">nowrap</span><span class="p">;</span>
<span class="k">overflow</span><span class="p">:</span> <span class="kc">hidden</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>這裡我們希望 <code>ul</code> 中的連結可以變成橫向排例的圖示，所以先讓 <code>li</code> 元素調整成向左浮動，再把連結設定為可以設定寬高的區塊元素。這樣一來，我們只要再調整背景圖就可以了。</p>
<p>註：這裡有用到以背景圖取代文字的技巧，請參考： <a href="http://www.zeldman.com/2012/03/01/replacing-the-9999px-hack-new-image-replacement/">Replacing the -9999px hack</a> 一文。</p>
<p>因為 <code>sprites.css</code> 是用程式產生的，所以我們把它獨立出來。</p>
<p>最後的結果如下：</p>
<p><img src="/resources/css_sprites_imagick/result.png" alt="Result"></p>
<h2 id="一些經驗">一些經驗</h2>
<p>實際應用這個技術時，我有幾個經驗分享給大家：</p>
<h3 id="清除舊圖">清除舊圖</h3>
<p>前面的程式碼裡，我把最後產生出來的 sprites 圖檔跟原來的小圖放在一起；但如果這時要重新產生 sprites 的話，就會把上次產生的 sprites 圖檔也一併合併進來。</p>
<p>為了解決這個錯誤，我們必須先把舊的 sprites 圖檔刪除：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">@unlink(__DIR__ . &#39;/img/sprites.png&#39;);
$imagePaths = glob(__DIR__ . &#39;/img/*&#39;);
</code></pre></div><p>當然如果不是在同一個目錄下的話，這個動作就可以不用了。</p>
<h3 id="利用後台管理-sprites">利用後台管理 sprites</h3>
<p>前面的範例裡，我是用 <code>glob</code> 這個函式來取得小圖資訊，但有時候我們希望某些暫時用不到的圖片可以不要出現在 sprites 時該怎麼辦呢？這時候我會利用後台來管理它們。</p>
<p>一般來說，我會在後台上傳新圖後，將相關資訊寫入資料庫中；然後就可以透過程式篩選掉暫時下架的小圖，最後再透過上面的程式來產生 sprites 。</p>
<h3 id="gif-動畫">GIF 動畫</h3>
<p>Imagick 也支援 GIF 動畫的處理，不過如果把 GIF 動畫合併到 sprites 時，它會把每張動畫影格 (frame) 都當成是一張小圖。</p>
<p>還好 GIF 動畫被 Imagick 載入時，會被當成是一個圖片集合物件，所以假如我們只需要第一個影格來當做 sprite 的話，可以這麼做：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">foreach ($imagePaths as $imagePath) {
    $im = new Imagick($imagePath);
    foreach ($im as $frame) {
        $tm = $frame-&gt;getImage();
        $image-&gt;addImage($tm);
        break;
    }
}
</code></pre></div><p>註：會這麼寫是因為 Imagick 沒有實作 ArraryAccess 介面，所以沒辦法像陣列一樣使用。</p>
<p>希望這些經驗可以幫助到大家。</p>
<h2 id="心得">心得</h2>
<p>Imagick 真的是一個很棒的 PHP 套件，它將 ImageMagick 的功能包裝得非常好用。這次剛好藉著製作 CSS Sprites 的功能，體會到這個套件的強大。</p>
<p>當然這裡我用到的只是 ImageMagick 與 Imagick 的小部份功能而已，往後如果有機會再實作到其他功能時，我也會再分享給大家。</p>
]]></content>
		</item>
		
		<item>
			<title>領悟 - 程序員版</title>
			<link>https://jaceju.net/realize/</link>
			<pubDate>Tue, 07 Feb 2012 21:16:00 +0800</pubDate>
			
			<guid>https://jaceju.net/realize/</guid>
			<description>時程一直是專案很難掌握的東西，尤其是上頭訂下一個尷尬的死線時。然後自己明知道不可為，但為了面子就只能勉強答應下來。 所以新網站在還沒完全測試後</description>
			<content type="html"><![CDATA[<p>時程一直是專案很難掌握的東西，尤其是上頭訂下一個尷尬的死線時。然後自己明知道不可為，但為了面子就只能勉強答應下來。</p>
<p>所以新網站在還沒完全測試後就上線了，只好邊看規格邊修 Bug 。</p>
<p>這時腦海裡突然浮現出一首歌的旋律，心有七七煙的我，就改出了以下的歌詞：</p>
<!-- raw HTML omitted -->
<h2 id="領悟---程序員版">領悟 - 程序員版</h2>
<p>我以為有寫測試，但是我沒有；</p>
<p>我只是征征望著那些錯誤，然後像孩子一樣無助。</p>
<p>這何嘗不是一種領悟，讓我把規格看清楚；</p>
<p>雖然那加班的痛苦，將日日夜夜在我靈魂最深處。</p>
<!-- raw HTML omitted -->
<p>我以為我會度姑，但是我沒有；</p>
<p>當我看到我親手寫的程式，給我那大紅色的錯誤。</p>
<p>這何嘗不是一種領悟，讓你把程式看清楚；</p>
<p>測試是奢侈的幸福，可惜 PM 從來不在乎。</p>
<!-- raw HTML omitted -->
<p>啊！一段程式就此刪除&hellip;</p>
<p>啊！一小時眼看要荒蕪。</p>
<p>我的程式若是錯誤，願 user 沒有白白受苦；</p>
<p>曾經無日無夜忙碌，就只有「辛苦」。</p>
<!-- raw HTML omitted -->
<p>啊！多麼痛的領悟，這規格不是全部；</p>
<p>只是我回首開會時的每一步，都走的好孤獨。</p>
<!-- raw HTML omitted -->
<p>啊！多麼痛的領悟，這時程已經延誤；</p>
<p>只願我掙開票的枷鎖， PM 束縛，全力追逐，別再為 bug 受苦。</p>
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted -->Muzicons.com<!-- raw HTML omitted --></p>
]]></content>
		</item>
		
		<item>
			<title>讀書會報告 - 深入淺出 MVC</title>
			<link>https://jaceju.net/head-first-mvc/</link>
			<pubDate>Mon, 08 Aug 2011 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/head-first-mvc/</guid>
			<description>MVC 一直以來是初學者很難跨過的一個觀念障礙，因此前陣子，我便在 PHP 讀書會中分享了這個主題：深入淺出 MVC 。 這次的內容主要介紹了 MVC 與 MVP 的觀念，以及簡易</description>
			<content type="html"><![CDATA[<p>MVC 一直以來是初學者很難跨過的一個觀念障礙，因此前陣子，我便在 PHP 讀書會中分享了這個主題：深入淺出 MVC 。</p>
<p>這次的內容主要介紹了 MVC 與 MVP 的觀念，以及簡易的 PHP 及 JavaScript 實作，希望能讓大家對 MVC 有進一步的認識。</p>
<!-- raw HTML omitted -->
<p>投影片如下：</p>
<!-- raw HTML omitted -->
<p>錄影內容如下：</p>
<!-- raw HTML omitted -->
<p>在 JavaScript MVP 的簡易實作版本中，我也做了兩個範例：  <a href="http://jsfiddle.net/RbFSc/2/">Supervising Controller 版本</a> / <a href="http://jsfiddle.net/FMruf/1/">Passive View 版本</a>。</p>
<p>其他範例程式可以到 <a href="https://github.com/jaceju/head_first_mvc_sample">GitHub</a> 下載。</p>
<p>最後，如果大家有發現我的觀念有誤的話，還請不吝指正。</p>
<p>謝謝大家。</p>
]]></content>
		</item>
		
		<item>
			<title>學習設計模式的心得</title>
			<link>https://jaceju.net/learning-patterns/</link>
			<pubDate>Mon, 25 Jul 2011 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/learning-patterns/</guid>
			<description>從幾年前開始接觸設計模式之後，一直覺得設計模式真的是太酷了！而這其間看了一些設計模式的書籍與文章，也實作了一些模式，深深覺得大師們的教導是對</description>
			<content type="html"><![CDATA[<p>從幾年前開始接觸設計模式之後，一直覺得設計模式真的是太酷了！而這其間看了一些設計模式的書籍與文章，也實作了一些模式，深深覺得大師們的教導是對的。</p>
<p>但要到達大師所體悟的境界，以我目前的程度來說還差得很遠；所以就我個人所學的過程，我簡單整理出一句話，那就是：</p>
<p><!-- raw HTML omitted -->找出模式，熟悉模式，模仿模式，忘掉模式，而後領悟模式。<!-- raw HTML omitted --></p>
<!-- raw HTML omitted -->
<h2 id="找出模式">找出模式</h2>
<p>當看完書中的內容後，我曾迫不及待地想去試試看模式怎麼用。不過後來我發現，我根本不知道從何用起。</p>
<p>因此，我換了個方式。我試著在一些知名的 Open Source 專案中，看看高手們是如何解決問題的；然後再從這些解決方案中，去分析出模式的蹤影。</p>
<p>當然這些解法不見得百分之百符合書上的模式範例，因此我也曾經誤判過一些模式。但我會再回頭看看書中對模式所描述的核心概念，去找出這些解法背後所隱藏的真正模式。</p>
<h2 id="熟悉模式">熟悉模式</h2>
<p>接下來我不斷地去用我熟悉的工具 (也就是 PHP ) 去練習模式的基本架構，試圖去找出該工具在模式上的最佳實作方法。</p>
<p>當然這些練習的成果並不能真正派上用場，而且也不見得是最佳方法；不過這樣的練習方式為我日後在實戰時，提供了不小的幫助。</p>
<h2 id="模仿模式">模仿模式</h2>
<p>後來在實戰時，我開始模仿別人在模式上的經驗；也剛好有些專案較為複雜，我便試著用模仿到的作法來一一解決這些難題。</p>
<p>這種方式所得到的解法，讓我覺得異常的漂亮；但也就從這時開始，我漸漸陷入模式迷思，開始認為絕大部份的問題都可以用模式解決。</p>
<h2 id="忘掉模式">忘掉模式</h2>
<p>以前看書時，一直無法理解大師為什麼要說不要將模式硬套在設計上；直到有次維護某個使用模式解決的專案功能時，發現它讓我難以理解，我才知道自己對模式有走火入魔的跡象。</p>
<p>我重新再把書上的內容翻過一次，大師的當頭棒喝及時地敲醒了我，有時簡單的解法就可以幫助你完成目標。模式只有在你真正需要時，才會浮現在你心中，並透過重構去找到它們；用不到時，就暫時忘了它吧。</p>
<h2 id="領悟模式">領悟模式</h2>
<p>嚴格來說，模式的領悟，一直是我還未到達的境界。</p>
<p>要能看出程式用了哪些模式，要能輕鬆地用模式與伙伴溝通，要能確定自己需不需要模式，要能隨心所欲地使用模式；我認為要做到這些要求，才算是我個人對模式有了真正的領悟。</p>
<p>希望，會有這麼一天。</p>
]]></content>
		</item>
		
		<item>
			<title>實戰 PHP 重構與模式</title>
			<link>https://jaceju.net/php-refactor-to-pattern-in-action/</link>
			<pubDate>Thu, 05 May 2011 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-refactor-to-pattern-in-action/</guid>
			<description>重構對一般開發者來說，實在是一件吃力不討好的事情，更別說想要把程式以模式來重構了。 在這次的讀書會報告裡，我首次挑戰這樣的題目，為大家介紹如何</description>
			<content type="html"><![CDATA[<p>重構對一般開發者來說，實在是一件吃力不討好的事情，更別說想要把程式以模式來重構了。</p>
<p>在這次的讀書會報告裡，我首次挑戰這樣的題目，為大家介紹如何將模式真正地應用到重構裡，也以實際的例子來展現重構與模式的威力。</p>
<p>當然以我個人的能力，還是有很多不盡理想之處；但還是希望透過這樣的介紹，讓大家對重構與模式有不一樣的看法。</p>
<p>因為在讀書會是用 Live Demo 來呈現重構的流程，所以可能在看投影片和範例時，會無法感受到重構與模式的魅力，這點要請大家多多包涵。</p>
<p>以下就是本次報告的投影片及範例：</p>
<!-- raw HTML omitted -->
<h2 id="投影片與範例">投影片與範例</h2>
<!-- raw HTML omitted -->
<p><a href="https://github.com/jaceju/PHP-Refactoring-And-Patterns">範例程式</a></p>
<p>謝謝大家。</p>
]]></content>
		</item>
		
		<item>
			<title>[PHP] 簡易的物件傳遞方法</title>
			<link>https://jaceju.net/passing-php-object/</link>
			<pubDate>Thu, 03 Feb 2011 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/passing-php-object/</guid>
			<description>在 PHP 中，傳遞物件是很容易的事；我們只需要將物件的狀態封裝起來後，以字串的方式傳遞給另一端的程式還原執行即可。物件的傳遞用途很多，例如我們在 Gearman</description>
			<content type="html"><![CDATA[<p>在 PHP 中，傳遞物件是很容易的事；我們只需要將物件的狀態封裝起來後，以字串的方式傳遞給另一端的程式還原執行即可。物件的傳遞用途很多，例如我們在 Gearman 中，就可以在 client 把物件當做是 job data 傳遞給 Server 。</p>
<p>註： Gearman 的介紹可以參考拙作：<a href="http://www.jaceju.net/blog/archives/1211">Gearman 心得</a>。</p>
<p>以下我們來看看範例。</p>
<!-- raw HTML omitted -->
<p>假設我們有個 <code>Event</code> 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">class</span> <span class="nc">Event</span>
<span class="p">{</span>
    <span class="k">protected</span> <span class="nv">$_name</span> <span class="o">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span><span class="nv">$name</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_name</span> <span class="o">=</span> <span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="nv">$name</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">getName</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_name</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>然後在 <code>client.php</code> 中我們建立了一個 <code>Event</code> 物件 <code>$event</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require_once</span> <span class="s1">&#39;Event.php&#39;</span><span class="p">;</span>
<span class="nv">$event</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Event</span><span class="p">(</span><span class="s1">&#39;test&#39;</span><span class="p">);</span>
<span class="nx">file_put_contents</span><span class="p">(</span><span class="s1">&#39;event.txt&#39;</span><span class="p">,</span> <span class="nx">serialize</span><span class="p">(</span><span class="nv">$event</span><span class="p">));</span>
</code></pre></div><p>在這邊我們把 <code>$event</code> 實體用 <code>serialize</code> 這個方法序列化，這樣就能把 <code>$event</code> 實體的狀態封裝起來了。</p>
<p>最後我們在 <code>server.php</code> 中還原它：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require_once</span> <span class="s1">&#39;Event.php&#39;</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">file_exists</span><span class="p">(</span><span class="s1">&#39;event.txt&#39;</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">exit</span><span class="p">;</span>
<span class="p">}</span>

<span class="nv">$event</span> <span class="o">=</span> <span class="nx">unserialize</span><span class="p">(</span><span class="nx">file_get_contents</span><span class="p">(</span><span class="s1">&#39;event.txt&#39;</span><span class="p">));</span>
<span class="k">echo</span> <span class="nv">$event</span><span class="o">-&gt;</span><span class="na">getName</span><span class="p">();</span>
</code></pre></div><p>要注意的是，我們必須把 <code>Event</code> 類別的宣告也包含進來，這樣 <code>unserialize</code> 才能正確還原物件。而還原後的 <code>$event</code> 實體，就跟我們在 <code>client.php</code> 建立的 <code>$event</code> 實體的狀態是一模一樣的了。</p>
<p>當然如果物件裡有些狀態是我們所不想傳遞出去時，這時候可以在類別裡定義 <code>__sleep</code> 這個魔術方法來回傳我們想要保留的屬性，而 <code>__wakeup</code> 方法則可以協助我們在 <code>unserialize</code> 後，執行一些初始化的方法；詳細的說明可以參考官方手冊裡 <a href="http://www.php.net/manual/en/language.oop5.magic.php">Magic Method</a> 一節。</p>
]]></content>
		</item>
		
		<item>
			<title>重構實例介紹 – 分析篇</title>
			<link>https://jaceju.net/refactoring-1/</link>
			<pubDate>Thu, 06 Jan 2011 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/refactoring-1/</guid>
			<description>重構是什麼？這問題其實真的很難回答。 我個人覺得重構是一種讓程式保持活力的一種方法，讓它能隨著時間而不斷地進化。如果我們一直放任自己不對程式碼</description>
			<content type="html"><![CDATA[<p>重構是什麼？這問題其實真的很難回答。</p>
<p>我個人覺得重構是一種讓程式保持活力的一種方法，讓它能隨著時間而不斷地進化。如果我們一直放任自己不對程式碼做適當的整理，而是靠著不求甚解的修修補補來維繫它的生命，很快地程式碼就會變得殘破不堪、臃腫肥大而難以維護。</p>
<p>而且有的時候，我們也想讓程式碼能隨著我們觀念的增長，以適應未來的變化；今天我們會覺得這樣的寫法很讚，但明天可能又會學到更好的寫法，重構就能給我們改變程式碼的機會。</p>
<p>但是這些都是很籠統的解釋，有沒有什麼方法可以讓我們更瞭解重構呢？我想，只有用實際的例子來說明是最直接的吧。但是太精簡的範例可能表達不了重構的意圖，而過於複雜的範例又會讓重構的焦點模糊，要找到一個適合的例子可能比重構本身還困難。</p>
<p>後來某次的改版機會下，我分析了伙伴之前所製作的功能並做了一次重構，發現這個功能的規模不算太大，而且也很容易展現出重構後的優點；因此，我便將這個功能稍微做了簡化以方便說明，希望能讓大家瞭解重構究竟是在做些什麼。</p>
<p>不過這個範例雖然不大，但也還是需要一番功夫來解說；因此我將會把它分成兩個部份來說明，第一篇是分析，第二篇則是實戰。</p>
<p>接下來就一起來看看這個例子吧。</p>
<!-- raw HTML omitted -->
<h2 id="需求說明">需求說明</h2>
<p>這是一個裝修問卷的需求，基本上是在收集客戶想要幫房子裝修的資訊，要注意的重點如下：</p>
<ul>
<li>裝修類型是指常見的房屋裝修重點，例如廚房、浴室或地板等。</li>
<li>裝修類型包含了客戶基本資訊以及裝修內容。</li>
<li>一個裝修問卷只會對應一種裝修類型，同時也只記錄一位客戶。</li>
<li>裝修內容則包含裝修類型裡會需要裝修的項目，像是廚房會需要換裝瓦斯爐、上下櫃等。</li>
<li>隨著裝修類型的不同，裝修內容也會不同。</li>
<li>未來會陸續新增裝修類型，因此裝修的內容也會有所變動。</li>
</ul>
<p>以下是這個需求所提供的樣板，表單頁：</p>
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
<p>完成頁：</p>
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
<p>在伙伴的努力下，現在這個功能已經達到客戶的需求並上線一段時間了，維護也都是交由伙伴自己來進行；但在某次系統改版時，為了能對系統做出整體改版計劃，所以我得瞭解這個功能細部的狀況。</p>
<p>以下就讓我們一起來從這個完成後的版本開始看起吧。</p>
<p>註：你可以在我的 GitHub 上找到<a href="https://github.com/jaceju/refactoring_sample">完整的程式碼</a>。</p>
<h2 id="程式解說">程式解說</h2>
<p>在重構之前，一定要先瞭解我們即將要重構的程式碼。如果完全不曉得它是做什麼的，那麼重構就會存在著一定的風險。</p>
<p>由於我們是採用 Zend Framework 來建置這個專案，因此程式將分成 Controller 、 View 及 Model 三個部份。以下我們先對它們的作用一一說明，然後再分析它們為何需要重構。</p>
<h3 id="controller">Controller</h3>
<p>Controller 的部份主要只有 IndexController 這個類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/controllers/IndexController.php]
class IndexController extends Zend_Controller_Action
{
    // ...
}

</code></pre></div><p>它包含了 index 、 step2 及 step3 三個 action ，也是本功能主要的流程。</p>
<p>indexAction() 方法只是單純地顯示連結用的 template 而已，沒有實作任何程式碼。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function indexAction()
    {
    }
</code></pre></div><p>step2Action() 方法負責表單的顯示與處理表單回傳的資料。這裡我們會先接收 decoration 參數，以決定要後面的程式要處理的裝修類型。</p>
<p>接著再透過 Request 物件的 isPost() 方法，來判斷是否是 PostBack 。如果是 PostBack 的話就先檢查表單資料，正確就寫入資料表，反之則顯示錯誤訊息。</p>
<p>註：這裡為了減少篇幅，故省略掉了找不到裝修類型的錯誤判斷。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function step2Action()
    {
        $decorationName = strtolower($this-&gt;getRequest()-&gt;getParam(&#39;decoration&#39;));
        $error = false;
        $messenger = $this-&gt;_helper-&gt;flashMessenger;
        /* @var $messenger Zend_Controller_Action_Helper_FlashMessenger */
        if ($this-&gt;getRequest()-&gt;isPost()) {
            $filter = new Zend_Filter_StripTags();
            $callback = array($filter, &#39;filter&#39;);
            $formData = array_map($callback, $this-&gt;getRequest()-&gt;getPost());
            $formData = array_map(&#39;trim&#39;, $formData);
            // 檢查表單必填值
            $checkFunctionName = &#39;_check&#39; . ucwords(strtolower($decorationName)) . &#39;FormData&#39;;
            $this-&gt;$checkFunctionName($formData, $error, $messenger);
            if (!$error) {
                $decorationTable = $this-&gt;_createDecorationTable($decorationName);
                $decorationRow = $decorationTable-&gt;createRow($formData);
                $decorationRow-&gt;save();
                $this-&gt;_helper-&gt;redirector-&gt;gotoSimple(&#39;step3&#39;, null, null, array(
                    &#39;decoration&#39; =&gt; $decorationName,
                    &#39;id&#39; =&gt; $decorationRow-&gt;id,
                ));
            } else {
                $params = array(
                    &#39;decoration&#39; =&gt; $decorationName,
                );
                $this-&gt;_helper-&gt;redirector-&gt;gotoSimple(&#39;step2&#39;, null, null, $params);
            }
        }
        $this-&gt;view-&gt;decorationName = $decorationName;
        $this-&gt;view-&gt;decorationDisplayName =
                $this-&gt;_decorationDisplayNameList[strtolower($decorationName)];
        $this-&gt;view-&gt;messages = $messenger-&gt;getMessages();
    }
</code></pre></div><p>而且因為我們已經知道裝修類型會隨著時間而增多，因此伙伴在撰寫程式時，已經考慮兩個可能會變化的部份。</p>
<p>第一個是在檢查表單內容的部份；在 step2Action() 方法接收了表單資料後，我們透過參數取得的裝修類型名稱來決定要呼叫哪個檢查方法。但因為目前我們只有 kitchen 這個裝修類型，所以在 IndexController 類別中只有 _checkKitchenFormData() 這個方法。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function step2Action()
    {
       // ...
            $checkFunctionName = &#39;_check&#39; . ucwords(strtolower($decorationName)) . &#39;FormData&#39;;
            $this-&gt;$checkFunctionName($formData, $error, $messenger);
       // ...
    }
    protected function _checkKitchenFormData($formData, <span class="ni">&amp;amp;</span>$error, Zend_Controller_Action_Helper_FlashMessenger <span class="ni">&amp;amp;</span>$messenger)
    {
        if (0 === strlen($formData[&#39;name&#39;])) {
            $error = true;
            $messenger-&gt;addMessage(&#39;請輸入姓名&#39;);
        }
        if (0 === strlen($formData[&#39;phone&#39;])) {
            $error = true;
            $messenger-&gt;addMessage(&#39;請輸入電話&#39;);
        }
        if (0 === strlen($formData[&#39;address&#39;])) {
            $error = true;
            $messenger-&gt;addMessage(&#39;請輸入地址&#39;);
        }
        if (!array_key_exists(&#39;kitchenQuestion01&#39;, $formData)
                <span class="ni">&amp;amp;&amp;amp;</span> !array_key_exists(&#39;kitchenQuestion02&#39;, $formData)) {
            $error = true;
            $messenger-&gt;addMessage(&#39;請選擇裝修內容&#39;);
        }
        if (!array_key_exists(&#39;kitchenQuestion03&#39;, $formData)
                <span class="ni">&amp;amp;&amp;amp;</span> !array_key_exists(&#39;kitchenQuestion04&#39;, $formData)) {
            $error = true;
            $messenger-&gt;addMessage(&#39;請選擇設備是否保留&#39;);
        }
        if (!array_key_exists(&#39;kitchenQuestion05&#39;, $formData)) {
            $error = true;
            $messenger-&gt;addMessage(&#39;請選擇現有廚具&#39;);
        }
    }

</code></pre></div><p>另一個是建立資料表的部份，這裡是透過 _createDecorationTable() 這個方法來動態決定要使用哪個資料表，也就是一般常見的工廠方法。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    protected function _createDecorationTable($decorationName)
    {
        $tableName = &#39;Application_Model_DbTable_Decoration&#39; . ucfirst($decorationName) . &#39;s&#39;;
        return new $tableName();
    }
    public function step2Action()
    {
        // ...
        $decorationTable = $this-&gt;_createDecorationTable($decorationName);
        // ...
    }

</code></pre></div><p>step3Action() 方法則是用來顯示完成後的頁面。這裡首先會取得 step2Action() 方法新增的自動編號，然後才從資料表裡取得對應的問卷資料來顯示。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    public function step3Action()
    {
        $decorationName = strtolower($this-&gt;getRequest()-&gt;getParam(&#39;decoration&#39;));
        $decorationId = (int) $this-&gt;getRequest()-&gt;getParam(&#39;id&#39;);
        $decorationTable = $this-&gt;_createDecorationTable($decorationName);
        $decorationRow = $decorationTable-&gt;find($decorationId)-&gt;current();
        if (!$decorationRow) {
            $this-&gt;_redirect(&#39;/&#39;);
        }
        $this-&gt;view-&gt;decorationName = $decorationName;
        $this-&gt;view-&gt;decorationDisplayName =
                $this-&gt;_decorationDisplayNameList[strtolower($decorationName)];
        $this-&gt;view-&gt;decorationRow = $decorationRow;
        $this-&gt;view-&gt;decorationMap = $this-&gt;_exportMapData($decorationName);
        $this-&gt;view-&gt;actionController = $this;
    }

</code></pre></div><p>與 step2Action() 方法相同，在 step3Action() 方法也呼叫了 _createDecorationTable() 方法來動態取得對應的資料表，接著再透過 id 參數取得在 step2Action() 方法新增的資料列。</p>
<p>因為存在資料表裡的資料並不包含中文訊息，因此我們需要將它做轉換。這個動作原本是可以在樣版裡做的，但為了不在樣版裡有太多的判斷式，因此採用了名稱對照表的方法。</p>
<p>_exportMapData() 方法會從 application/controllers/DecorationMap 資料夾下載入對應的名稱對照表 (即 kitchen.php) ，然後將它 assign 到 view 的 decorationMap 變數。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">    protected function _exportMapData($decorationName)
    {
        $includeFile = __DIR__ . &#39;/DecorationMap/&#39; . $decorationName . &#39;.php&#39;;
        if (!file_exists($includeFile)) {
            $this-&gt;_redirect(&#39;/&#39;);
        } else {
            $decorationMap = include($includeFile);
            return $decorationMap;
        }
    }

</code></pre></div><p>名稱對照表內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/controllers/DecorationMap/kitchen.php]
return array(
    &#39;kitchenQuestion01&#39; =&gt; array(
        &#39;y&#39; =&gt; &#39;吊櫃&#39;,
        &#39;n&#39; =&gt; &#39;&#39;,
    ),
    &#39;kitchenQuestion02&#39; =&gt; array(
        &#39;y&#39; =&gt; &#39;下櫃&#39;,
        &#39;n&#39; =&gt; &#39;&#39;,
    ),
    &#39;kitchenQuestion03&#39; =&gt; array(
        &#39;y&#39; =&gt; &#39;瓦斯爐&#39;,
        &#39;n&#39; =&gt; &#39;&#39;,
    ),
    &#39;kitchenQuestion04&#39; =&gt; array(
        &#39;y&#39; =&gt; &#39;排油煙機&#39;,
        &#39;n&#39; =&gt; &#39;&#39;,
    ),
    &#39;kitchenQuestion05&#39; =&gt; array(
        &#39;1&#39; =&gt; &#39;無&#39;,
        &#39;2&#39; =&gt; &#39;泥作&#39;,
        &#39;3&#39; =&gt; &#39;組合式&#39;,
        &#39;4&#39; =&gt; &#39;歐化&#39;,
        &#39;5&#39; =&gt; &#39;不鏽鋼&#39;,
    ),
);

</code></pre></div><p>另外 IndexController 類別裡也定義了一個 $_decorationDisplayNameList 屬性，它的目的是存放 decoration 參數所對應的中文名稱。因此在 step2Action() 方法及 step3Action() 方法中都會先找出裝修類型所對應的中文名稱，再 assign 到 view 的 decorationDisplayName 變數中。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/controllers/IndexController.php]
    protected $_decorationDisplayNameList = array(
        &#39;kitchen&#39; =&gt; &#39;廚房&#39;,
    );
    // ...
    public function step2Action()   {
        // ...
        $this-&gt;view-&gt;decorationDisplayName =
                $this-&gt;_decorationDisplayNameList[strtolower($decorationName)];
        // ...
    }

</code></pre></div><p>接下來就是 View 的部份。</p>
<h3 id="view">View</h3>
<p>View 的部份除了三個對應 action 的 template scripts 外，還有裝修類型所對應的裝修內容表單及完成頁。</p>
<p>index.phtml 為對應 indexAction() 方法的樣版，顯示表單的連結。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/index.phtml]
<span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>房屋裝修<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;</span><span class="cp">&lt;?php</span>
<span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">url</span><span class="p">(</span><span class="k">array</span><span class="p">(</span>
    <span class="s1">&#39;controller&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;index&#39;</span><span class="p">,</span>
    <span class="s1">&#39;action&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;step2&#39;</span><span class="p">,</span>
    <span class="s1">&#39;decoration&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;kitchen&#39;</span><span class="p">,</span>
<span class="p">));</span> <span class="cp">?&gt;</span><span class="s">&#34;</span><span class="p">&gt;</span>廚房<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>

</code></pre></div><p>step2.phtml 為對應 step2Action() 方法的樣版，用來顯示表單；因為我們已經預知每個裝修內容的選項是不一樣的，因此將會變化的子表單部份獨立成子樣版。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/step2.phtml]
<span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span><span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">escape</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationDisplayName</span><span class="p">);</span> <span class="cp">?&gt;</span><span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">form</span> <span class="na">action</span><span class="o">=</span><span class="s">&#34;&#34;</span> <span class="na">method</span><span class="o">=</span><span class="s">&#34;post&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">fieldset</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">legend</span><span class="p">&gt;</span>連絡資訊<span class="p">&lt;/</span><span class="nt">legend</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;name&#34;</span><span class="p">&gt;</span>姓名<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;name&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;name&#34;</span> <span class="p">/&gt;&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;phone&#34;</span><span class="p">&gt;</span>電話<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;phone&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;phone&#34;</span> <span class="p">/&gt;&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">label</span> <span class="na">for</span><span class="o">=</span><span class="s">&#34;address&#34;</span><span class="p">&gt;</span>住址<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;address&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;address&#34;</span> <span class="p">/&gt;&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">fieldset</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">fieldset</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">legend</span><span class="p">&gt;</span>裝修選項<span class="p">&lt;/</span><span class="nt">legend</span><span class="p">&gt;</span>
        <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">partial</span><span class="p">(</span><span class="s1">&#39;index/&#39;</span> <span class="o">.</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationName</span> <span class="o">.</span> <span class="s1">&#39;/form.phtml&#39;</span><span class="p">);</span> <span class="cp">?&gt;</span>
    <span class="p">&lt;/</span><span class="nt">fieldset</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span><span class="p">&gt;</span>完成送出<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>

</code></pre></div><p>每個會變化的子表單都放在同名的裝修類型資料表下，例如廚房的部份就是：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/kitchen/form.phtml]
<span class="p">&lt;</span><span class="nt">dl</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>裝修內容<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;checkbox&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion01&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;y&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>吊櫃<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;checkbox&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion02&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;y&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>下櫃<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>設備是否保留<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;checkbox&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;y&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>瓦斯爐<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;checkbox&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion04&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;y&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>排油煙機<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>現有廚具<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion05&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>無（需新製）<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion05&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;2&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>泥作<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion05&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;3&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>組合式（分件廚具）<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion05&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;4&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>歐化（木質桶身）<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;kitchenQuestion05&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;5&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>不鏽鋼桶身<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">dl</span><span class="p">&gt;</span>

</code></pre></div><p>另外 step2.phtml 也會在表單資料驗證錯誤時，將對應的錯誤訊息顯示出來。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/kitchen/form.phtml]
    <span class="cp">&lt;?php</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">messages</span><span class="p">))</span> <span class="o">:</span> <span class="cp">?&gt;</span>
    <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
    <span class="cp">&lt;?php</span> <span class="k">foreach</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">messages</span> <span class="k">as</span> <span class="nv">$message</span><span class="p">)</span> <span class="o">:</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span><span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">escape</span><span class="p">(</span><span class="nv">$message</span><span class="p">);</span> <span class="cp">?&gt;</span><span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="cp">&lt;?php</span> <span class="k">endforeach</span><span class="p">;</span> <span class="cp">?&gt;</span>
    <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
    <span class="cp">&lt;?php</span> <span class="k">endif</span><span class="p">;</span> <span class="cp">?&gt;</span>
<span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span>

</code></pre></div><p>step3.phtml 為對應 step3Action() 方法的樣版，用來顯示完成頁；這裡也和 step2Action() 方法一樣用子樣版來顯示不同的裝修類型結果。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/step3.phtml]
<span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span><span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">escape</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationDisplayName</span><span class="p">);</span> <span class="cp">?&gt;</span><span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span>連絡資訊<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>姓名： <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">escape</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="o">-&gt;</span><span class="na">name</span><span class="p">);</span> <span class="cp">?&gt;</span><span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>電話： <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">escape</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="o">-&gt;</span><span class="na">phone</span><span class="p">);</span> <span class="cp">?&gt;</span><span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>住址： <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">escape</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="o">-&gt;</span><span class="na">address</span><span class="p">);</span> <span class="cp">?&gt;</span><span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">h2</span><span class="p">&gt;</span>裝修選項<span class="p">&lt;/</span><span class="nt">h2</span><span class="p">&gt;</span>
<span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">partial</span><span class="p">(</span><span class="s1">&#39;index/&#39;</span> <span class="o">.</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationName</span> <span class="o">.</span> <span class="s1">&#39;/ok.phtml&#39;</span><span class="p">,</span> <span class="k">null</span><span class="p">,</span> <span class="nv">$this</span><span class="p">);</span> <span class="cp">?&gt;</span>

</code></pre></div><p>子樣版如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/kitchen/ok.phtml]
<span class="p">&lt;</span><span class="nt">dl</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>裝修內容：<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion01&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion01&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion02&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion02&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>設備是否保留：<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion03&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion03&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion04&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion04&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>現有廚具：<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion05&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;kitchenQuestion05&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">dl</span><span class="p">&gt;</span>

</code></pre></div><p>比較特別的是在 ok.html 這個子樣版裡，我們用到了定義在 IndexController 類別裡的 buildOptionString() 方法，以取得資料列欄位值對應到 decorationMap 的中文名稱。</p>
<h3 id="model">Model</h3>
<p>Model 的部份比較簡單，目前只有一個 Zend_Db_Table 類別。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/models/DbTable/DecorationKitchens.php]
class Application_Model_DbTable_DecorationKitchens extends Zend_Db_Table_Abstract
{
    protected $_name = &#39;decoration_kitchens&#39;;
}

</code></pre></div><p>而對應的 database schema 如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-sql" data-lang="sql"><span class="k">SET</span> <span class="n">SQL_MODE</span><span class="o">=</span><span class="s2">&#34;NO_AUTO_VALUE_ON_ZERO&#34;</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">DATABASE</span> <span class="o">`</span><span class="n">refactoring</span><span class="o">`</span> <span class="k">DEFAULT</span> <span class="nb">CHARACTER</span> <span class="k">SET</span> <span class="n">utf8</span> <span class="k">COLLATE</span> <span class="n">utf8_general_ci</span><span class="p">;</span>
<span class="n">USE</span> <span class="o">`</span><span class="n">refactoring</span><span class="o">`</span><span class="p">;</span>
<span class="k">DROP</span> <span class="k">TABLE</span> <span class="k">IF</span> <span class="k">EXISTS</span> <span class="o">`</span><span class="n">decoration_kitchens</span><span class="o">`</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="o">`</span><span class="n">decoration_kitchens</span><span class="o">`</span> <span class="p">(</span>
  <span class="o">`</span><span class="n">id</span><span class="o">`</span> <span class="nb">int</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="n">unsigned</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="n">auto_increment</span><span class="p">,</span>
  <span class="o">`</span><span class="n">name</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">phone</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">cellphone</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">address</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">kitchenQuestion01</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="o">`</span><span class="n">kitchenQuestion02</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="o">`</span><span class="n">kitchenQuestion03</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="o">`</span><span class="n">kitchenQuestion04</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="o">`</span><span class="n">kitchenQuestion05</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;1&#39;</span><span class="p">,</span><span class="s1">&#39;2&#39;</span><span class="p">,</span><span class="s1">&#39;3&#39;</span><span class="p">,</span><span class="s1">&#39;4&#39;</span><span class="p">,</span><span class="s1">&#39;5&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="k">PRIMARY</span> <span class="k">KEY</span>  <span class="p">(</span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="p">)</span>
<span class="p">)</span> <span class="n">ENGINE</span><span class="o">=</span><span class="n">MyISAM</span>  <span class="k">DEFAULT</span> <span class="n">CHARSET</span><span class="o">=</span><span class="n">utf8</span> <span class="n">ROW_FORMAT</span><span class="o">=</span><span class="k">DYNAMIC</span> <span class="k">COMMENT</span><span class="o">=</span><span class="s1">&#39;廚房問卷&#39;</span><span class="p">;</span>

</code></pre></div><p>以上就是第一版程式所需要的所有檔案和它們的運作原理。</p>
<h3 id="加入新裝修類型">加入新裝修類型</h3>
<p>瞭解了程式碼之後，接著我們來看如何加入一個新的裝修類型；假設新的裝修類型是「浴室」 (Bathroom) ，也已經有了對應的 database schema 及樣版。</p>
<p>我們需要更動以下幾個部份：</p>
<p>註：以下將會以一般 patch 檔的方式來表示更動後的差異，即在程式碼的行頭加上加號 (+) 或減號 (-) 來分別表示新增程式碼或刪減程式碼；而行頭沒有特別標示的話，就表示沒有更動；至於移除檔案、資料夾、類別屬性或整個類別方法的部份，就不會再特別用程式碼標明，以節省篇幅。</p>
<p>第一步：新增 Bathroom 的 DbTable 類別並匯入 schema 到資料庫中。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">[/refactoring_sample/application/models/DbTable/DecorationBathrooms.php]
<span class="gi">+class Application_Model_DbTable_DecorationBathrooms extends Zend_Db_Table_Abstract
</span><span class="gi">+{
</span><span class="gi">+    protected $_name = &#39;decoration_bathrooms&#39;;
</span><span class="gi">+}
</span><span class="gi"></span>
</code></pre></div><p>資料表的 schema 如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-sql" data-lang="sql"><span class="k">DROP</span> <span class="k">TABLE</span> <span class="k">IF</span> <span class="k">EXISTS</span> <span class="o">`</span><span class="n">decoration_bathrooms</span><span class="o">`</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="o">`</span><span class="n">decoration_bathrooms</span><span class="o">`</span> <span class="p">(</span>
  <span class="o">`</span><span class="n">id</span><span class="o">`</span> <span class="nb">int</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="n">unsigned</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="n">auto_increment</span><span class="p">,</span>
  <span class="o">`</span><span class="n">name</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">phone</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">address</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">bathroomQuestion01</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;1&#39;</span><span class="p">,</span><span class="s1">&#39;2&#39;</span><span class="p">,</span><span class="s1">&#39;3&#39;</span><span class="p">,</span><span class="s1">&#39;4&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">bathroomQuestion02</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;1&#39;</span><span class="p">,</span><span class="s1">&#39;2&#39;</span><span class="p">,</span><span class="s1">&#39;3&#39;</span><span class="p">,</span><span class="s1">&#39;4&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">bathroomQuestion03</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;1&#39;</span><span class="p">,</span><span class="s1">&#39;2&#39;</span><span class="p">,</span><span class="s1">&#39;3&#39;</span><span class="p">,</span><span class="s1">&#39;4&#39;</span><span class="p">,</span><span class="s1">&#39;5&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">bathroomQuestion04</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="o">`</span><span class="n">bathroomQuestion05</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="k">PRIMARY</span> <span class="k">KEY</span>  <span class="p">(</span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="p">)</span>
<span class="p">)</span> <span class="n">ENGINE</span><span class="o">=</span><span class="n">MyISAM</span>  <span class="k">DEFAULT</span> <span class="n">CHARSET</span><span class="o">=</span><span class="n">utf8</span> <span class="n">ROW_FORMAT</span><span class="o">=</span><span class="k">DYNAMIC</span> <span class="k">COMMENT</span><span class="o">=</span><span class="s1">&#39;衛浴問卷&#39;</span><span class="p">;</span>

</code></pre></div><p>第二步：在 IndexController 類別的 $_decorationDisplayNameList 變數中加入新的中文名稱對應。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     protected $_decorationDisplayNameList = array(
         &#39;kitchen&#39; =&gt; &#39;廚房&#39;,
<span class="gi">+        &#39;bathroom&#39; =&gt; &#39;浴室&#39;,
</span><span class="gi"></span>     );
</code></pre></div><p>第三步：在 IndexController 類別中新增 _checkBathroomFormData() 方法。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gi">+    protected function _checkBathroomFormData($formData, &amp;amp;$error, Zend_Controller_Action_Helper_FlashMessenger &amp;amp;$messenger)
</span><span class="gi">+    {
</span><span class="gi">+        if (0 === strlen($formData[&#39;name&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入姓名&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;phone&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入電話&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;address&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入地址&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;bathroomQuestion01&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇坪數&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;bathroomQuestion02&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇馬桶&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;bathroomQuestion03&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇面盆&#39;);
</span><span class="gi">+        }
</span><span class="gi">+    }
</span><span class="gi"></span>
</code></pre></div><p>第四步：在 application/controllers/DecorationMap 目錄下加入名稱對照檔。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/controllers/DecorationMap/bathroom.php]
<span class="cp">&lt;?php</span>
<span class="k">return</span> <span class="k">array</span><span class="p">(</span>
    <span class="s1">&#39;bathroomQuestion01&#39;</span> <span class="o">=&gt;</span> <span class="k">array</span><span class="p">(</span>
        <span class="s1">&#39;1&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;1 坪以下&#39;</span><span class="p">,</span>
        <span class="s1">&#39;2&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;1 ~ 1.3 坪&#39;</span><span class="p">,</span>
        <span class="s1">&#39;3&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;1.3 ~ 1.5 坪&#39;</span><span class="p">,</span>
        <span class="s1">&#39;4&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;1.5 坪以上&#39;</span><span class="p">,</span>
    <span class="p">),</span>
    <span class="s1">&#39;bathroomQuestion02&#39;</span> <span class="o">=&gt;</span> <span class="k">array</span><span class="p">(</span>
        <span class="s1">&#39;1&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;新製 - 一般馬桶&#39;</span><span class="p">,</span>
        <span class="s1">&#39;2&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;新製 - 免治馬桶&#39;</span><span class="p">,</span>
        <span class="s1">&#39;3&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;不需更換&#39;</span><span class="p">,</span>
        <span class="s1">&#39;4&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;未決定&#39;</span><span class="p">,</span>
    <span class="p">),</span>
    <span class="s1">&#39;bathroomQuestion03&#39;</span> <span class="o">=&gt;</span> <span class="k">array</span><span class="p">(</span>
        <span class="s1">&#39;1&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;新製 - 長柱&#39;</span><span class="p">,</span>
        <span class="s1">&#39;2&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;新製  -短柱&#39;</span><span class="p">,</span>
        <span class="s1">&#39;3&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;新製 - 單孔&#39;</span><span class="p">,</span>
        <span class="s1">&#39;4&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;不需更換&#39;</span><span class="p">,</span>
        <span class="s1">&#39;5&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;未決定&#39;</span><span class="p">,</span>
    <span class="p">),</span>
<span class="p">);</span>

</code></pre></div><p>第五步：加入 Bathroom 的子表單樣版及完成頁樣版。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/bathroom/form.phtml]
<span class="p">&lt;</span><span class="nt">dl</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>坪數<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion01&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>1 坪以下<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion01&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;2&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>1 ~ 1.3 坪<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion01&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;3&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>1.3 ~ 1.5 坪<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion01&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;4&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>1.5 坪以上<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>馬桶<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion02&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>新製 - 一般馬桶<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion02&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;2&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>新製 - 免治馬桶<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion02&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;3&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>不需更換<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion02&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;4&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>未決定<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>面盆<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>新製 - 長柱<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;2&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>新製  -短柱<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;3&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>新製 - 單孔<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;4&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>不需更換<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;bathroomQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;5&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>未決定<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">dl</span><span class="p">&gt;</span>
</code></pre></div><div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/bathroom/ok.phtml]
<span class="p">&lt;</span><span class="nt">dl</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>坪數<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;bathroomQuestion01&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;bathroomQuestion01&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>馬桶<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;bathroomQuestion02&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;bathroomQuestion02&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>面盆<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">actionController</span><span class="o">-&gt;</span><span class="na">buildOptionString</span><span class="p">(</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationMap</span><span class="p">[</span><span class="s1">&#39;bathroomQuestion03&#39;</span><span class="p">],</span>
                    <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="p">[</span><span class="s1">&#39;bathroomQuestion03&#39;</span><span class="p">]);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">dl</span><span class="p">&gt;</span>

</code></pre></div><p>第六步：最後在 index.phtml 加入 Bathroom 的新連結。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/views/scripts/index/index.phtml]
 <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>房屋裝修<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
 <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
     <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;</span><span class="cp">&lt;?php</span>
 <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">url</span><span class="p">(</span><span class="k">array</span><span class="p">(</span>
     <span class="s1">&#39;controller&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;index&#39;</span><span class="p">,</span>
     <span class="s1">&#39;action&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;step2&#39;</span><span class="p">,</span>
     <span class="s1">&#39;decoration&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;kitchen&#39;</span><span class="p">,</span>
 <span class="p">));</span> <span class="cp">?&gt;</span><span class="s">&#34;</span><span class="p">&gt;</span>廚房<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
+    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;</span><span class="cp">&lt;?php</span>
<span class="o">+</span><span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">url</span><span class="p">(</span><span class="k">array</span><span class="p">(</span>
<span class="o">+</span>    <span class="s1">&#39;controller&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;index&#39;</span><span class="p">,</span>
<span class="o">+</span>    <span class="s1">&#39;action&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;step2&#39;</span><span class="p">,</span>
<span class="o">+</span>    <span class="s1">&#39;decoration&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;bathroom&#39;</span><span class="p">,</span>
<span class="o">+</span><span class="p">));</span> <span class="cp">?&gt;</span><span class="s">&#34;</span><span class="p">&gt;</span>浴室<span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
 <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>

</code></pre></div><p>接著我們來分析這樣的程式寫法有什麼缺點。</p>
<h2 id="程式分析">程式分析</h2>
<p>從上面新增一個裝修類型的過程來看，除了新增必要的 DbTable 類別和樣版外，大部份的修改都集中在 IndexController 類別上；可以想見未來在不斷增加裝修類型後， IndexContrller 類別會越來越龐大。</p>
<p>物件導向開發中，有個很重要的原則就是 SRP (Single Responsibility Principle) ，但顯然 IndexController 類別已經違反了這個原則。原因就是我們把所有的邏輯都集中在 IndexController 類別的身上，使得它所背負的工作超過它原先擔任的角色。</p>
<p>所以這裡可以改進的地方在於，讓 IndexController 類別除了流程的調整外，儘可能不要因為新增裝修類型而有所更動。</p>
<p>另外 View 應該是直接輸出 Model 的資料，或是透過 View Helper 或樣版語法來處理資料的呈現；但是在 step3Action() 方法中，卻因為 View 需要轉換資料列欄位值的名稱，而呼叫了 IndexController 類別的 buildOptionString() 方法；這使得 View 層不適當地依賴了 Controller 層，造成程式碼維護上的困難。</p>
<p>而且名稱對應表也必須透過 IndexController 類別來載入，這使得我們在其他 Controller 中必須重複貼上載入檔案的程式碼，這也是一個可以改進的地方。</p>
<p>下一篇，我們就來針對這些問題一一重構吧！</p>
<p>繼續閱讀：<a href="http://jaceju.net/2011/01/06/1501/">重構實例介紹 - 實戰篇</a></p>
]]></content>
		</item>
		
		<item>
			<title>重構實例介紹 – 實戰篇</title>
			<link>https://jaceju.net/refactoring-2/</link>
			<pubDate>Thu, 06 Jan 2011 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/refactoring-2/</guid>
			<description>接續前篇：重構實例介紹 - 分析篇 在上一篇中，我們分析了該功能的每段程式碼，也瞭解有哪些地方需要改進，接著我們就來進入實戰的階段。 在實戰的階段，</description>
			<content type="html"><![CDATA[<p>接續前篇：<a href="http://jaceju.net/2011/01/06/1495/">重構實例介紹 - 分析篇</a></p>
<p>在上一篇中，我們分析了該功能的每段程式碼，也瞭解有哪些地方需要改進，接著我們就來進入實戰的階段。</p>
<p>在實戰的階段，我們要做兩件事：建立自動化測試，重構程式碼。</p>
<!-- raw HTML omitted -->
<h2 id="選擇自動化測試工具">選擇自動化測試工具</h2>
<p>因為重構是「在不改變程式碼外在行為的前提下，對程式碼做出修正，以改進程式的內部結構」 (重構 - 侯捷／熊節) ，所以在重構之前我們必須先有一個信心依據，確保我們的修改不會影響原來的功能，而這個方法就是自動化測試。</p>
<p>一般舊程式的架構很少能用單元測試的概念做到自動化測試，所以每次測試時都要人工打開瀏覽器用眼睛一個項目一個項目去驗證，這種測試的方式非常仰賴畫面輸出的結果。而在我們這個例子裡，因為 IndexController 類別負擔了太多工作，因此也難以直接使用一般的單元測試，只能依靠畫面輸出來驗證程式的正確性。不過有兩種測試技術的出現，使得透過畫面輸出來驗證的方式也能夠自動化，那就是 Zend_Test 與 Selenium 。</p>
<p>它們的原理都是透過解析輸出的 HTML 來做測試，不同的是 Zend_Test 會直接在伺服端截取 Response 物件的內容，而 Selenium 則是透過瀏覽器來解析。 Zend_Test 的優勢在於它可以直接驗證應用程式的內部流程及伺服端物件狀態 (例如 Session 內容) ，適合系統較為複雜的開發環境；而 Selenium 的優勢則在於它可以測試頁面上的互動功能，例如透過 JavaScript 產生的 UI 等。</p>
<p>另外 Zend_Test 需要手動去撰寫測試案例，而且也只能用在以 Zend Framework 為基礎的應用程式上；而 Selenium 則提供視覺化的 Selenium IDE 來建立測試案例，也能透過 Selenium RC 來建立自動化測試環境，而且可適用在各種不同的 Web 開發環境上。</p>
<p>綜合上面的各種優缺點，因此在這次重構的過程中，我們就選擇  Selenium  做為自動測試的工具。</p>
<h2 id="建立自動化測試">建立自動化測試</h2>
<p>因為我們已經有了已經上線的程式，執行的功能也是經過客戶確認的，所以便可直接使用 Selenium 來錄製正常的流程。這邊就不再詳細描述錄製的過程，請大家自行參考以下的圖示。</p>
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
<p>這裡的測試案例主要分成兩個部份：成功寫入及驗證失敗。當然如果為了確保測試的正確性，也是可以再添增更複雜的測試組合。但因為這是個範例，所以就以簡單易懂為優先考量。</p>
<p>完成錄製後，就可以將它存成以下 HTML 檔案：</p>
<ul>
<li>/refactoring_sample/tests/selenium/UISuite.html</li>
<li>/refactoring_sample/tests/selenium/KitchenTest.html</li>
<li>/refactoring_sample/tests/selenium/BathroomTest.html</li>
</ul>
<p>註：為省略篇幅，這裡請參考置於 GitHub 的<a href="http://goo.gl/nIgWG">原始程式碼</a>。</p>
<p>如果搭配 Selenium RC 的話，還可以錄製成 PHPUnit 的 TestCase 檔；不過由於我們並不需要做到全自動化的測試，因此使用 Selenium IDE 載入前面錄製的 HTML 檔做測試即可。</p>
<p>接下來在重構的過程中，請開啟 Selenium IDE 並載入剛剛的 HTML 測試案例檔。稍後重構的步驟中，若有提到測試的話，就代表要按下 Selenium IDE 的執行按鍵 (圖示為綠色三角形) 。</p>
<p>有了測試的依據後，我們就可以進行接下來的重構了。</p>
<h2 id="重構實戰">重構實戰</h2>
<p>事實上重構並沒有一定要從哪個地方開始做起，一般來說要看分析的結果。而在前面提到 IndexController 類別負擔了太多責任，所以我們第一步可以先讓它從這些責任中解脫。</p>
<p>註：同上一篇，以下將會以一般 patch 檔的方式來表示更動後的差異，以節省篇幅。</p>
<h3 id="1-移動名稱對照表">1. 移動名稱對照表</h3>
<p>首先我們要把原來透過 IndexController::_exportMapData() 方法所載入的名稱對照表移動位置，因為我們希望要能夠減少新增裝修類型時，所要建立的檔案數量。因此我把原來的名稱對照表暫時先放到對應的 DbTable 類別中，作法如下：</p>
<p>a. 在 Application_Model_DbTable_DecorationKitchens 類別中加入 $_displayMap 屬性及 getDisplayMap() 方法。 $_displayMap 屬性值即為 application/controllers/DecorationMap/kitchen.php 的陣列值； getDisplayMap() 方法則回傳 $_displayMap 屬性。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationKitchens extends Zend_Db_Table_Abstract
 {
     protected $_name = &#39;decoration_kitchens&#39;;

<span class="gi">+    protected $_displayMap = array(
</span><span class="gi">+        &#39;kitchenQuestion01&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;吊櫃&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion02&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;下櫃&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion03&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;瓦斯爐&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion04&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;排油煙機&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion05&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;無&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;泥作&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;組合式&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;歐化&#39;,
</span><span class="gi">+            &#39;5&#39; =&gt; &#39;不鏽鋼&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+    );
</span><span class="gi">+
</span><span class="gi">+    public function getDisplayMap()
</span><span class="gi">+    {
</span><span class="gi">+        return $this-&gt;_displayMap;
</span><span class="gi">+    }
</span><span class="gi"></span> }
</code></pre></div><p>b. 再對 Application_Model_DbTable_DecorationBathrooms 這個類別做一次類似的修改。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationBathrooms extends Zend_Db_Table_Abstract
 {
     protected $_name = &#39;decoration_bathrooms&#39;;
<span class="gi">+    protected $_displayMap = array(
</span><span class="gi">+        &#39;bathroomQuestion01&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;1 坪以下&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;1 ~ 1.3 坪&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;1.3 ~ 1.5 坪&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;1.5 坪以上&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;bathroomQuestion02&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;新製 - 一般馬桶&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;新製 - 免治馬桶&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;不需更換&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;未決定&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;bathroomQuestion03&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;新製 - 長柱&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;新製  -短柱&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;新製 - 單孔&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;不需更換&#39;,
</span><span class="gi">+            &#39;5&#39; =&gt; &#39;未決定&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+    );
</span><span class="gi">+
</span><span class="gi">+    public function getDisplayMap()
</span><span class="gi">+    {
</span><span class="gi">+        return $this-&gt;_displayMap;
</span><span class="gi">+    }
</span><span class="gi"></span> }
</code></pre></div><p>c. 修改 IndexController 類別的 step3Action() 方法，將原本透過 IndexController 類別的 _exportMapData() 方法取值的部份，改用 DbTable 類別的 getDisplayMap() 方法。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-        $this-&gt;view-&gt;decorationMap = $this-&gt;_exportMapData($decorationName);
</span><span class="gd"></span><span class="gi">+        $this-&gt;view-&gt;decorationMap = $decorationTable-&gt;getDisplayMap();
</span></code></pre></div><p>註：這裡的「 DbTable 類別」就是指 Application_Model_DbTable_DecorationKitchens 及 Application_Model_DbTable_DecorationBathrooms 這兩個類別，後續之說明亦同。</p>
<p>這裡有個很重要的重構，就是方法名稱的修正。原本的 _exportMapData 並無法正確表達程式的意圖，所以在重構時我們最好能將它修改成符合程式意圖的名稱。</p>
<p>d. 切換到 Selenium IDE 的畫面，測試。沒意外的話，應該就能看到 Selenium IDE 出現綠色光棒了。</p>
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
<p>e. 確定沒問題後，我們就可以移除 IndexContrller 的 _exportMapData() 方法和 application/controllers/DecorationMap 整個目錄，再執行一次測試。</p>
<p>第一步就是這麼簡單，因為重構本身就是維持小步前進；我們不需要一次做太多，避免出錯時難以找到問題所在。</p>
<h3 id="2-改用-row-類別來解決名稱對應表的問題">2. 改用 Row 類別來解決名稱對應表的問題</h3>
<p>前面我們把名稱對應表放在 DbTable 類別裡，其實還是不太好；因為我們還是得透過 IndexController 類別的 buildOptionString() 方法，讓從 DbTable 裡取得的名稱對應表能轉換 Row 物件的欄位值。</p>
<p>所以在這個步驟中，我們就要讓 Row 物件自己能輸出欄位值所對應的中文名稱，作法如下：</p>
<p>a. 在 application/models/DbTable 下加入 Application_Model_DbTable_DecorationKitchen 及 Application_Model_DbTable_DecorationBathroom 的 Row 類別，它們分別繼承自 Zend_Db_Table_Row_Abstract 類別。</p>
<p>註：之後的說明裡，提到「 Row 類別」的話，就代表 Application_Model_DbTable_DecorationKitchen 及 Application_Model_DbTable_DecorationBathroom 兩個類別。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gi">+class Application_Model_DbTable_DecorationKitchen extends Zend_Db_Table_Row_Abstract
</span><span class="gi">+{
</span><span class="gi">+}
</span></code></pre></div><div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gi">+class Application_Model_DbTable_DecorationBathroom extends Zend_Db_Table_Row_Abstract
</span><span class="gi">+{
</span><span class="gi">+}
</span></code></pre></div><p>b. 在原來 DbTable 類別中，加入 $_rowClass 屬性，其值為對應的 Row 類別名稱。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationKitchens extends Zend_Db_Table_Abstract
 {
     protected $_name = &#39;decoration_kitchens&#39;;
<span class="gi">+    protected $_rowClass = &#39;Application_Model_DbTable_DecorationKitchen&#39;;
</span></code></pre></div><div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationBathrooms extends Zend_Db_Table_Abstract
 {
     protected $_name = &#39;decoration_kitchens&#39;;
<span class="gi">+    protected $_rowClass = &#39;Application_Model_DbTable_DecorationBathroom&#39;;
</span></code></pre></div><p>c. 將原來在 DbTable 類別上的 $_displayMap 屬性和 getDisplayMap() 方法複製到對應的 Row 類別上。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationKitchen extends Zend_Db_Table_Row_Abstract
 {
<span class="gi">+    protected $_displayMap = array(
</span><span class="gi">+        &#39;kitchenQuestion01&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;吊櫃&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion02&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;下櫃&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion03&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;瓦斯爐&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion04&#39; =&gt; array(
</span><span class="gi">+            &#39;y&#39; =&gt; &#39;排油煙機&#39;,
</span><span class="gi">+            &#39;n&#39; =&gt; &#39;&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;kitchenQuestion05&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;無&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;泥作&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;組合式&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;歐化&#39;,
</span><span class="gi">+            &#39;5&#39; =&gt; &#39;不鏽鋼&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+    );
</span><span class="gi">+
</span><span class="gi">+    public function getDisplayMap()
</span><span class="gi">+    {
</span><span class="gi">+        return $this-&gt;_displayMap;
</span><span class="gi">+    }
</span><span class="gi"></span> }
</code></pre></div><div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationBathroom extends Zend_Db_Table_Row_Abstract
 {
<span class="gi">+    protected $_displayMap = array(
</span><span class="gi">+        &#39;bathroomQuestion01&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;1 坪以下&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;1 ~ 1.3 坪&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;1.3 ~ 1.5 坪&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;1.5 坪以上&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;bathroomQuestion02&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;新製 - 一般馬桶&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;新製 - 免治馬桶&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;不需更換&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;未決定&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+        &#39;bathroomQuestion03&#39; =&gt; array(
</span><span class="gi">+            &#39;1&#39; =&gt; &#39;新製 - 長柱&#39;,
</span><span class="gi">+            &#39;2&#39; =&gt; &#39;新製  -短柱&#39;,
</span><span class="gi">+            &#39;3&#39; =&gt; &#39;新製 - 單孔&#39;,
</span><span class="gi">+            &#39;4&#39; =&gt; &#39;不需更換&#39;,
</span><span class="gi">+            &#39;5&#39; =&gt; &#39;未決定&#39;,
</span><span class="gi">+        ),
</span><span class="gi">+    );
</span><span class="gi">+
</span><span class="gi">+    public function getDisplayMap()
</span><span class="gi">+    {
</span><span class="gi">+        return $this-&gt;_displayMap;
</span><span class="gi">+    }
</span><span class="gi"></span> }
</code></pre></div><p>e. 將原來呼叫 DbTable 類別的 getDisplayMap() 方法的部份，改為呼叫 Row 類別的 getDisplayMap() 方法，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-        $this-&gt;view-&gt;decorationMap = $decorationTable-&gt;getDisplayMap();
</span><span class="gd"></span><span class="gi">+        $this-&gt;view-&gt;decorationMap = $decorationRow-&gt;getDisplayMap();
</span></code></pre></div><p>f. 測試通過後，將原來在 DbTable 類別上的 $_displayMap 屬性和 getDisplayMap() 方法刪除。</p>
<p>到這裡，相信大家會覺得很疑惑，為什麼不一開始就把 $_displayMap 屬性和 getDisplayMap() 方法移到 Row 類別上呢？</p>
<p>因為在一開始尋找需要處理的問題的方向，會影響我們後續重構的過程；而且我並不確定一開始就把 $_displayMap 屬性和 getDisplayMap() 方法放在 Row 類別上是不是一個好作法，所以我選擇了影響較少的方式。保持小步前進是重構最重要的關鍵之一，這樣一來即使發現方向錯誤，也可以讓復原的代價減到最低。</p>
<p>g. 將 IndexController 類別的 buildOptionString() 複製到 Application_Model_DbTable_DecorationKitchen 類別裡，並改名為 display() 方法。接著修改 display() 方法的程式碼，讓它直接使用內部的 $_displayMap 屬性。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationKitchen extends Zend_Db_Table_Row_Abstract
 {
<span class="gi">+    public function display($name)
</span><span class="gi">+    {
</span><span class="gi">+        return isset($this-&gt;_displayMap[$name]($this-&gt;$name))
</span><span class="gi">+             ? $this-&gt;_displayMap[$name]($this-&gt;$name)
</span><span class="gi">+             : null;
</span><span class="gi">+    }
</span></code></pre></div><p>h. 接著將樣版中呼叫 IndexController::buildOptionString() 方法的部份改為呼叫 Row 物件的 display() 方法；可以先改一個，測試無誤後再全部改掉。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-            &lt;?php echo $this-&gt;actionController-&gt;buildOptionString(
</span><span class="gd">-                    $this-&gt;decorationMap[&#39;kitchenQuestion01&#39;],
</span><span class="gd">-                    $this-&gt;decorationRow[&#39;kitchenQuestion01&#39;]); ?&gt;
</span><span class="gd"></span><span class="gi">+            &lt;?php echo $this-&gt;decorationRow-&gt;display(&#39;kitchenQuestion01&#39;); ?&gt;
</span></code></pre></div><p>i. Application_Model_DbTable_DecorationBathroom 類別做同樣的處理，測試。</p>
<p>j. 最後將 IndexController::step3Action() 方法中，移除 view 對 decorationMap 和 actionController 兩個變數的引用。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function step3Action()
     {
         // ...
<span class="gd">-        $this-&gt;view-&gt;decorationMap = $decorationRow-&gt;getDisplayMap();
</span><span class="gd">-        $this-&gt;view-&gt;actionController = $this;
</span><span class="gd"></span>     }
</code></pre></div><p>k. 最後刪掉整個 IndexController::buildOptionString() 方法。</p>
<p>當然，我們也可以讓 application/views/scripts/index/kitchen/form.phtml 改用新的 display() 方法來顯示選項，這就看維護上的需求值不值得我們這麼做了。</p>
<h3 id="3-處理裝修類型名稱">3. 處理裝修類型名稱</h3>
<p>現在我們已經讓 IndexController 類別減輕不少責任，但是還有一個地方會影響到每次我們新增裝修類型，那就是 IndexController 類別的 $_decorationDisplayNameList 屬性。我們希望讓它是定義在 DbTable 類別或是 Row 類別裡，這樣就不用每次都要修改 IndexController 類別。</p>
<p>那麼新的屬性位置應該放在 DbTable 類別裡好，還是放在 Row 類別裡好呢？其實兩個都是可行的選擇，但原則上我們希望儘可能讓相同的邏輯放在同一個地方，因此這邊我選擇了 Row 類別。完整的作法如下：</p>
<p>a. 先將 step2Action 呼叫 _createDecorationTable() 的程式碼移到 action 的開頭處，並在下一行加上 $decorationTable-&gt;createRow() ；而原本呼叫 DbTable 物件的 createRow() 方法的部份則改為呼叫 Row 物件的 setFromArray() 方法，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class IndexController 類別extends Zend_Controller_Action
 {
     public function step2Action()
     {
         $decorationName = strtolower($this-&gt;getRequest()-&gt;getParam(&#39;decoration&#39;));
<span class="gi">+        $decorationTable = $this-&gt;_createDecorationTable($decorationName);
</span><span class="gi">+        $decorationRow = $decorationTable-&gt;createRow();
</span><span class="gi"></span><span class="gd">-                $decorationTable = $this-&gt;_createDecorationTable($decorationName);
</span><span class="gd">-                $decorationRow = $decorationTable-&gt;createRow($formData);
</span><span class="gd"></span><span class="gi">+                $decorationRow-&gt;setFromArray($formData);
</span></code></pre></div><p>b. 在 Row 類別中加入 $_displayName 屬性及 getDisplayName() 方法，其中 $_displayName 屬性的值即為裝修類型的中文名稱。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationKitchen extends Zend_Db_Table_Row_Abstract
 {
<span class="gi">+    protected $_displayName = &#39;廚房&#39;;
</span><span class="gi">+
</span><span class="gi">+    public function getDisplayName()
</span><span class="gi">+    {
</span><span class="gi">+        return $this-&gt;_displayName;
</span><span class="gi">+    }
</span></code></pre></div><div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationBathroom extends Zend_Db_Table_Row_Abstract
 {
<span class="gi">+    protected $_displayName = &#39;浴室&#39;;
</span><span class="gi">+
</span><span class="gi">+    public function getDisplayName()
</span><span class="gi">+    {
</span><span class="gi">+        return $this-&gt;_displayName;
</span><span class="gi">+    }
</span></code></pre></div><p>c. 在 step2Action() 方法的 view 中加入對 $decorationRow 的引用，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class IndexController 類別extends Zend_Controller_Action
 {
     public function step2Action()
     {
         $this-&gt;view-&gt;decorationName = $decorationName;
         $this-&gt;view-&gt;decorationDisplayName =
                 $this-&gt;_decorationDisplayNameList[strtolower($decorationName)];
<span class="gi">+        $this-&gt;view-&gt;decorationRow = $decorationRow;
</span></code></pre></div><p>d. 在 step2.phtml 及 step3.phtml 兩個樣版中，將顯示樣版變數 decorationDisplayName 的部份，改為顯示 Row 物件的 getDisplayName() 方法，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-&lt;h1&gt;&lt;?php echo $this-&gt;escape($this-&gt;decorationDisplayName); ?&gt;&lt;/h1&gt;
</span><span class="gd"></span><span class="gi">+&lt;h1&gt;&lt;?php echo $this-&gt;escape($this-&gt;decorationRow-&gt;getDisplayName()); ?&gt;&lt;/h1&gt;
</span></code></pre></div><p>e. 拿掉 IndexController 類別的 step2Action() 與 step3Action() 方法中，指定給 view 的 $_decorationDisplayNameList 變數所在相關程式碼，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function step2Action()
     {
<span class="gd">-        $this-&gt;view-&gt;decorationDisplayName =
</span><span class="gd">-                $this-&gt;_decorationDisplayNameList[strtolower($decorationName)];
</span><span class="gd"></span>     public function step3Action()
     {
<span class="gd">-        $this-&gt;view-&gt;decorationDisplayName =
</span><span class="gd">-                $this-&gt;_decorationDisplayNameList[strtolower($decorationName)];
</span></code></pre></div><p>f. 最後移除 IndexController 類別裡的 $_decorationDisplayNameList 屬性。</p>
<p>這樣一來，在 IndexController 類別就不需要額外處理中文名稱顯示的問題了。</p>
<h3 id="4-處理重複的程式碼">4. 處理重複的程式碼</h3>
<p>重複的程式碼是最糟糕的一種壞味道，但我們卻在上面的重構過程中，讓兩個 Row 類別都重複了同樣的程式碼；所以現在我們要透過繼承來消除掉這些重複的程式碼，作法如下：</p>
<p>a. 建立抽象的 Application_Model_DbTable_DecorationRow 類別，將在另外兩個 Row 類別重複的 getDisplayName() 、 getDisplayMap() 及 display() 等三個方法複製過來，並加入必要的 protected 屬性： $_displayName 及 $_displayMap 。</p>
<p>註：後面的說明中，會以「抽象 Row 類別」來代替 Application_Model_DbTable_DecorationRow 的完整名稱。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[/refactoring_sample/application/models/DbTable/DecorationRow.php]
abstract class Application_Model_DbTable_DecorationRow extends Zend_Db_Table_Row_Abstract
{
    protected $_displayName = null;
    public function getDisplayName()
    {
        return $this-&gt;_displayName;
    }
    protected $_displayMap = array();
    public function getDisplayMap()
    {
        return $this-&gt;_displayMap;
    }
    public function display($name)
    {
        return isset($this-&gt;_displayMap[$name]($this-&gt;$name))
             ? $this-&gt;_displayMap[$name]($this-&gt;$name)
             : null;
    }
}

</code></pre></div><p>b. 舊的 Row 類別改為繼承抽象 Row 類別，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-class Application_Model_DbTable_DecorationKitchen extends Zend_Db_Table_Row_Abstract
</span><span class="gd"></span><span class="gi">+class Application_Model_DbTable_DecorationKitchen extends Application_Model_DbTable_DecorationRow
</span></code></pre></div><div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-class Application_Model_DbTable_DecorationKitchen extends Zend_Db_Table_Row_Abstract
</span><span class="gd"></span><span class="gi">+class Application_Model_DbTable_DecorationKitchen extends Application_Model_DbTable_DecorationRow
</span></code></pre></div><p>c. 最後移除 Row 類別的 getDisplayName() 、 getDisplayMap() 及 display() 三個方法，測試。</p>
<p>現在我們的 Row 類別就只需要定義好裝修類型中文名稱以及欄位值名稱對應表就可以了。</p>
<h3 id="5-處理表單驗證函式">5. 處理表單驗證函式</h3>
<p>最後我們來處理會讓 IndexController 類別變成得非常冗長的表單驗證函式。當然時間允許的話，這邊可以用 Zend_Form 改寫；但這樣會偏離本文的主軸，因此我們還是以重構為優先。</p>
<p>這個步驟主要的目標是將各個裝修類型表單驗證函式放在對應的 Row 類別裡，這樣我們就可以直接讓 Row 類別驗證表單資料，而不需要在 IndexController 類別裡定義驗證函式。作法如下：</p>
<p>a. 將 IndexController 類別的 _checkKitchenFormData() 方法複製到 Application_Model_DbTable_DecorationKitchen 類別，並改成名為 checkData() 的公開方法；然後對 IndexController 類別的 _checkBathroomFormData() 也做同樣的處理。現在兩個 Row 類別分別都有 checkData() 函式。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationKitchen extends Application_Model_DbTable_DecorationRow
 {
<span class="gi">+    public function checkData($formData, $error, $messenger)
</span><span class="gi">+    {
</span><span class="gi">+        $error = false;
</span><span class="gi">+        if (0 === strlen($formData[&#39;name&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入姓名&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;phone&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入電話&#39;;
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;address&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入地址&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;kitchenQuestion01&#39;, $formData)
</span><span class="gi">+                &amp;amp;&amp;amp; !array_key_exists(&#39;kitchenQuestion02&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇裝修內容&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;kitchenQuestion03&#39;, $formData)
</span><span class="gi">+                &amp;amp;&amp;amp; !array_key_exists(&#39;kitchenQuestion04&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇設備是否保留&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;kitchenQuestion05&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇現有廚具&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        return $error;
</span><span class="gi">+    }
</span></code></pre></div><div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class Application_Model_DbTable_DecorationBathroom extends Application_Model_DbTable_DecorationRow
 {
<span class="gi">+    public function checkData($formData, $error, $messenger)
</span><span class="gi">+    {
</span><span class="gi">+        $error = false;
</span><span class="gi">+        if (0 === strlen($formData[&#39;name&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入姓名&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;phone&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入電話&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;address&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請輸入地址&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;bathroomQuestion01&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇坪數&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;bathroomQuestion02&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇馬桶&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        if (!array_key_exists(&#39;bathroomQuestion03&#39;, $formData)) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $messenger-&gt;addMessage(&#39;請選擇面盆&#39;);
</span><span class="gi">+        }
</span><span class="gi">+        return $error;
</span><span class="gi">+    }
</span></code></pre></div><p>b. 將 IndexController 類別的 step2Action() 方法中，以 $checkFunctionName() 呼叫驗證函式的方式，改為呼叫 Row 物件的 checkData() 方法，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> class IndexController 類別extends Zend_Controller_Action
 {
     public function step2Action()
     {
<span class="gd">-            $this-&gt;$checkFunctionName($formData, $error, $messenger);
</span><span class="gd"></span><span class="gi">+            $decorationRow-&gt;checkData($formData, $error, $messenger);
</span></code></pre></div><p>c. 移除 step2Action() 方法裡的 $checkFunctionName 變數，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function step2Action()
     {
<span class="gd">-            $checkFunctionName = &#39;_check&#39; . ucwords(strtolower($decorationName)) . &#39;FormData&#39;;
</span></code></pre></div><p>d. 最後移除 IndexController 類別裡的 _checkKitchenFormData() 及 _checkBathroomFormData() 兩個方法，</p>
<p>現在我們終於讓 IndexController 類別脫離那些不讓屬於它的繁雜責任了。</p>
<p>註：事實上還有 _createDecorationTable() 方法也不應該放在 IndexController 類別裡，這邊就留給大家思考看看需不需要再做進一步的重構。</p>
<h3 id="6-解決-row-類別與-indexcontroller-類別類別的耦合">6. 解決 Row 類別與 IndexController 類別類別的耦合</h3>
<p>重構了 IndexController 類別的部份後，最後我們來處理 Row 類別。在 Row 類別的 checkData() 方法中，我們可以看到它除了必須的 $formData 參數外，還得取得 IndexController 類別的 $error 及 $messenger 兩個參數的引用；尤其 $messenger 變數的型態又是 Zend_Controller_Action_Helper_FlashMessenger 類別，使得 Row 類別必須依賴 Controller 層，大大降低了 Row 類別的可重用性。</p>
<p>因此我們要讓 Row 類別自行提處理掉驗證錯誤的部份，反過來讓 IndexController 類別依賴 Row 類別；換句話說，就是要拿掉 checkData() 的 $error 及 $messenger 兩個參數，作法如下：</p>
<p>a. 分別把兩個 Row 類別的 checkData() 方法裡的 $error 變成區域變數，並將其回傳給呼叫者。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-    public function checkData($formData, $error, $messenger)
</span><span class="gd"></span><span class="gi">+    public function checkData($formData, $messenger)
</span><span class="gi"></span>     {
<span class="gi">+        $error = false;
</span><span class="gi"></span>         // ...
<span class="gi">+        return $error;
</span><span class="gi"></span>     }
</code></pre></div><p>b. 然後在 IndexController 類別的 step2Action() 方法中接收 Row 類別的 checkData() 方法所回傳的 $error ，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function step2Action()
     {
<span class="gd">-            $decorationRow-&gt;checkData($formData, $error, $messenger);
</span><span class="gd"></span><span class="gi">+            $error = $decorationRow-&gt;checkData($formData, $messenger);
</span></code></pre></div><p>c. 接著在抽象 Row 類別中建立一個 $_messages 陣列屬性及 getMessages() 方法。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> abstract class Application_Model_DbTable_DecorationRow extends Zend_Db_Table_Row_Abstract
 {
<span class="gi">+    protected $_messages = array();
</span><span class="gi">+
</span><span class="gi">+    public function getMessages()
</span><span class="gi">+    {
</span><span class="gi">+        return $this-&gt;_messages;
</span><span class="gi">+    }
</span></code></pre></div><p>d. 然後把 Row 類別的 checkData() 方法中原本用 $messenger-&gt;addMessage() 的部份，改用新的 $_messages 屬性來指定。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function checkData($formData, $messenger)
     {
         if (0 === strlen($formData[&#39;name&#39;])) {
             $error = true;
<span class="gd">-            $messenger-&gt;addMessage(&#39;請輸入姓名&#39;);
</span><span class="gd"></span><span class="gi">+            $this-&gt;_messages[] = &#39;請輸入姓名&#39;;
</span><span class="gi"></span>         }
         // 以下依此類推
</code></pre></div><p>e. 在 IndexController 類別的 step2Action() 方法中處理驗證失敗的區段中，在導回 step2Action() 前，透過抽象 Row 類別的 getMessages() 方法，以取得錯誤訊息並一一加入 $messenger 中，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function step2Action()
     {
         // ...
             if (!$error) {
                 // ...
             } else {
                 // ...
<span class="gi">+                $messages = $decorationRow-&gt;getMessages();
</span><span class="gi">+                foreach ($messages as $message) {
</span><span class="gi">+                    $messenger-&gt;addMessage($message);
</span><span class="gi">+                }
</span><span class="gi"></span>                 $this-&gt;_helper-&gt;redirector-&gt;gotoSimple(&#39;step2&#39;, null, null, $params);
             }
</code></pre></div><p>f. 最後移除 checkData() 的 $messenger 參數及 step2Action() 傳入 $messenger 變數的部份，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"><span class="gd">-    public function checkData($formData, $messenger)
</span><span class="gd"></span><span class="gi">+    public function checkData($formData)
</span><span class="gi"></span>
</code></pre></div><div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function step2Action()
     {
         // ...
<span class="gd">-            $error = $decorationRow-&gt;checkData($formData, $messenger);
</span><span class="gd"></span><span class="gi">+            $error = $decorationRow-&gt;checkData($formData);
</span></code></pre></div><p>現在 Row 類別就跟 IndexController 類別解耦了。</p>
<p>註：這邊請大家思考一下，為什麼我在處理 $error 的部份時，一開始就拿掉 checkData() 方法的 $error 參數；但是處理 $messenger 時，卻沒有一開始就拿掉 $messenger 參數呢？</p>
<h3 id="7-處理-checkdata-方法重複的部份">7. 處理 checkData() 方法重複的部份</h3>
<p>再回頭看看 Row 類別的 checkData() 方法，我們可以發現裡面還是有重複的程式碼，也就是檢查姓名、電話與地址的部份。這三個欄位的檢查在未來新增裝修類型時也不會變動，因此我們可以將它們抽出來變成共用的程式碼，作法如下：</p>
<p>a. 在抽象 Row 類別中建立一個 _checkCommonData() 方法，並將 Row 類別的 checkData() 方法裡檢查姓名、電話、地址的部份複製出來，放到新的 _checkCommonData() 方法中，一樣要加入 $error 區域變數並回傳。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff"> abstract class Application_Model_DbTable_DecorationRow extends Zend_Db_Table_Row_Abstract
 {
     // ...
<span class="gi">+
</span><span class="gi">+    protected function _checkCommonData($formData)
</span><span class="gi">+    {
</span><span class="gi">+        $error = false;
</span><span class="gi">+        if (0 === strlen($formData[&#39;name&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $this-&gt;_messages[] = &#39;請輸入姓名&#39;;
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;phone&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $this-&gt;_messages[] = &#39;請輸入電話&#39;;
</span><span class="gi">+        }
</span><span class="gi">+        if (0 === strlen($formData[&#39;address&#39;])) {
</span><span class="gi">+            $error = true;
</span><span class="gi">+            $this-&gt;_messages[] = &#39;請輸入地址&#39;;
</span><span class="gi">+        }
</span><span class="gi">+        return $error;
</span><span class="gi">+    }
</span></code></pre></div><p>b. 改寫各 Row 類別的 checkData() 方法，移除原來的共用驗證程式碼，並改為呼叫 _checkCommonData() 方法，測試。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     public function checkData($formData)
     {
         $error = false;
<span class="gd">-        if (0 === strlen($formData[&#39;name&#39;])) {
</span><span class="gd">-            $error = true;
</span><span class="gd">-            $this-&gt;_messages[] = &#39;請輸入姓名&#39;;
</span><span class="gd">-        }
</span><span class="gd">-        if (0 === strlen($formData[&#39;phone&#39;])) {
</span><span class="gd">-            $error = true;
</span><span class="gd">-            $this-&gt;_messages[] = &#39;請輸入電話&#39;;
</span><span class="gd">-        }
</span><span class="gd">-        if (0 === strlen($formData[&#39;address&#39;])) {
</span><span class="gd">-            $error = true;
</span><span class="gd">-            $this-&gt;_messages[] = &#39;請輸入地址&#39;;
</span><span class="gd">-        }
</span><span class="gd"></span><span class="gi">+        $error = $error || $this-&gt;_checkCommonData($formData);
</span><span class="gi"></span>
</code></pre></div><p>測試無誤後，我們就完成了整個重構了，謝天謝地。</p>
<h2 id="再次加入新的裝修類型">再次加入新的裝修類型</h2>
<p>花了這些功夫做重構後，最重要的是它好不好維護，容不容易因應未來的變化；因此接下來我們就要加入一個新的裝修類型，來看看重構後的程式碼是不是符合我們的期待。</p>
<p>假設新的裝修類型是「地板」 (Flooring) ，客戶也提供了對應的 database schema 及樣版。</p>
<p>第一步：加入 Flooring 的 DbTable 類別並匯入 schema 到資料庫中。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="err">&lt;</span>/path_to_project/application/models/DbTable/DecorationFloorings.php&gt;
class Application_Model_DbTable_DecorationFloorings extends Zend_Db_Table_Abstract
{
    protected $_name = &#39;decoration_floorings&#39;;
    protected $_rowClass = &#39;Application_Model_DbTable_DecorationFlooring&#39;;
}

</code></pre></div><p>資料表的 schema 如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-sql" data-lang="sql"><span class="k">DROP</span> <span class="k">TABLE</span> <span class="k">IF</span> <span class="k">EXISTS</span> <span class="o">`</span><span class="n">decoration_floorings</span><span class="o">`</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="o">`</span><span class="n">decoration_floorings</span><span class="o">`</span> <span class="p">(</span>
  <span class="o">`</span><span class="n">id</span><span class="o">`</span> <span class="nb">int</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="n">unsigned</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="n">auto_increment</span><span class="p">,</span>
  <span class="o">`</span><span class="n">name</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">phone</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">address</span><span class="o">`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">200</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">flooringQuestion01</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="o">`</span><span class="n">flooringQuestion02</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;y&#39;</span><span class="p">,</span><span class="s1">&#39;n&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="s1">&#39;n&#39;</span><span class="p">,</span>
  <span class="o">`</span><span class="n">flooringQuestion03</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;1&#39;</span><span class="p">,</span><span class="s1">&#39;2&#39;</span><span class="p">,</span><span class="s1">&#39;3&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="o">`</span><span class="n">flooringQuestion04</span><span class="o">`</span> <span class="n">enum</span><span class="p">(</span><span class="s1">&#39;1&#39;</span><span class="p">,</span><span class="s1">&#39;2&#39;</span><span class="p">,</span><span class="s1">&#39;3&#39;</span><span class="p">,</span><span class="s1">&#39;4&#39;</span><span class="p">,</span><span class="s1">&#39;5&#39;</span><span class="p">)</span> <span class="k">default</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="k">PRIMARY</span> <span class="k">KEY</span>  <span class="p">(</span><span class="o">`</span><span class="n">id</span><span class="o">`</span><span class="p">)</span>
<span class="p">)</span> <span class="n">ENGINE</span><span class="o">=</span><span class="n">MyISAM</span> <span class="k">DEFAULT</span> <span class="n">CHARSET</span><span class="o">=</span><span class="n">utf8</span> <span class="n">ROW_FORMAT</span><span class="o">=</span><span class="k">DYNAMIC</span> <span class="k">COMMENT</span><span class="o">=</span><span class="s1">&#39;地板問卷&#39;</span><span class="p">;</span>

</code></pre></div><p>第二步：加入 Flooring 的 Row 類別。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="err">&lt;</span>/path_to_project/application/models/DbTable/DecorationFlooring.php&gt;
class Application_Model_DbTable_DecorationFlooring extends Application_Model_DbTable_DecorationRow
{
    protected $_displayName = &#39;地板&#39;;
    protected $_displayMap = array(a
        &#39;flooringQuestion01&#39; =&gt; array(
            &#39;y&#39; =&gt; &#39;地板施作&#39;,
            &#39;n&#39; =&gt; &#39;&#39;,
        ),
        &#39;flooringQuestion02&#39; =&gt; array(
            &#39;y&#39; =&gt; &#39;樓梯&#39;,
            &#39;n&#39; =&gt; &#39;&#39;,
        ),
        &#39;flooringQuestion03&#39; =&gt; array(
            &#39;1&#39; =&gt; &#39;已有鋪設木地板&#39;,
            &#39;2&#39; =&gt; &#39;磁磚&#39;,
            &#39;3&#39; =&gt; &#39;塑膠地磚&#39;,
        ),
        &#39;flooringQuestion04&#39; =&gt; array(
            &#39;1&#39; =&gt; &#39;透天住宅&#39;,
            &#39;2&#39; =&gt; &#39;一般公寓&#39;,
            &#39;3&#39; =&gt; &#39;電梯大樓&#39;,
            &#39;4&#39; =&gt; &#39;公司行號&#39;,
            &#39;5&#39; =&gt; &#39;其他&#39;,
        ),
    );
    public function checkData($formData)
    {
        $error = false;
        $error = $error || $this-&gt;_checkCommonData($formData);
        if (!array_key_exists(&#39;flooringQuestion01&#39;, $formData)
                <span class="ni">&amp;amp;&amp;amp;</span> !array_key_exists(&#39;flooringQuestion02&#39;, $formData)) {
            $error = true;
            $this-&gt;_messages[] = &#39;請選擇裝修內容&#39;;
        }
        if (!array_key_exists(&#39;flooringQuestion03&#39;, $formData)) {
            $error = true;
            $this-&gt;_messages[] = &#39;請選擇施工環境情況&#39;;
        }
        if (!array_key_exists(&#39;flooringQuestion04&#39;, $formData)) {
            $error = true;
            $this-&gt;_messages[] = &#39;請選擇住家環境&#39;;
        }
        return $error;
    }
}

</code></pre></div><p>第三步：加入 Flooring 的子表單樣版及完成頁樣版。</p>
<p>表單樣版：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="err">&lt;</span>/path_to_project/application/views/scripts/index/bathroom/form.phtml&gt;
<span class="p">&lt;</span><span class="nt">dl</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>裝修內容<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;checkbox&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion01&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;y&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>地板施作<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;checkbox&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion02&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;y&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>樓梯<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>施工環境情況<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>已有鋪設木地板<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;2&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>磁磚<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion03&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;3&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>塑膠地磚<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>住家環境<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion04&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>透天住宅<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion04&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;2&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>一般公寓<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion04&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;3&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>電梯大樓<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion04&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;4&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>公司行號<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;flooringQuestion04&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;5&#34;</span> <span class="p">/&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>其他<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">dl</span><span class="p">&gt;</span>

</code></pre></div><p>完成頁樣版：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="err">&lt;</span>/path_to_project/application/views/scripts/index/bathroom/ok.phtml&gt;
<span class="p">&lt;</span><span class="nt">dl</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>裝修內容<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s1">&#39;flooringQuestion01&#39;</span><span class="p">);</span> <span class="cp">?&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s1">&#39;flooringQuestion02&#39;</span><span class="p">);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>施工環境情況<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s1">&#39;flooringQuestion03&#39;</span><span class="p">);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dt</span><span class="p">&gt;</span>住家環境<span class="p">&lt;/</span><span class="nt">dt</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">dd</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
            <span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">decorationRow</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s1">&#39;flooringQuestion04&#39;</span><span class="p">);</span> <span class="cp">?&gt;</span>
        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">dd</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">dl</span><span class="p">&gt;</span>

</code></pre></div><p>第四步：最後在 index.phtml 加入 Flooring 的新連結。</p>
<div class="highlight"><pre class="chroma"><code class="language-diff" data-lang="diff">     &lt;li&gt;&lt;a href=&#34;&lt;?php
 echo $this-&gt;url(array(
     &#39;controller&#39; =&gt; &#39;index&#39;,
     &#39;action&#39; =&gt; &#39;step2&#39;,
     &#39;decoration&#39; =&gt; &#39;bathroom&#39;,
 )); ?&gt;&#34;&gt;浴室&lt;/a&gt;&lt;/li&gt;
<span class="gi">+    &lt;li&gt;&lt;a href=&#34;&lt;?php
</span><span class="gi">+echo $this-&gt;url(array(
</span><span class="gi">+        &#39;controller&#39; =&gt; &#39;index&#39;,
</span><span class="gi">+        &#39;action&#39; =&gt; &#39;step2&#39;,
</span><span class="gi">+        &#39;decoration&#39; =&gt; &#39;flooring&#39;,
</span><span class="gi">+)); ?&gt;&#34;&gt;地板&lt;/a&gt;&lt;/li&gt;
</span><span class="gi"></span> &lt;/ul&gt;

</code></pre></div><p>除了因為要加入連結而必須修改 index.html 外，我們幾乎都是新增檔案，而沒有修改到原來的程式碼。對於後續維護的人員來說，就可以不心擔心修改到舊程式碼而出現不可預期的錯誤了，是不是比原來的方法好多了呢？</p>
<h2 id="結語">結語</h2>
<p>當然這樣的重構並不是最完美的，也可能跟大家心中所想的作法有所差異，不過這已經達成我們重構的目的；因為透過這樣的重構，我們瞭解了整個系統，也將原來散亂各地的資料存取邏輯統整在一起，使得程式碼更容易新增功能與維護。</p>
<p>然而重構最大的問題並不是不知道怎麼重構，而是不知道該什麼時候重構，還有什麼時候該停止重構。老實說該什麼時候重構，我沒辦法給大家什麼好答案；個人認為比較好的方式就是寫好一個功能時，留點時間給重構；或是在新增及維護功能時，進行小規模的重構。然後見好就收，覺得已經達到重構目的後，就把時間還給新的專案或需求。</p>
<p>每次的重構應該只針對一個目標去完成，避免過程無目的地發散；如果在重構的過程中，發現任何其他部份也需要重構的話，可以先記下來列入下次的重構裡。而在重構一小步並測試無誤之後，就可以將它放到版本控制系統裡，這樣就不用擔心走得太遠，結果出問題後無法復原。</p>
<p>重構說難不難，說容易也不是那麼簡單；希望從這個範例中，大家可以對重構有進一步的瞭解。</p>
<p>謝謝收看，下次再會。</p>
]]></content>
		</item>
		
		<item>
			<title>購物車程式架構簡介</title>
			<link>https://jaceju.net/the-design-of-shopping-cart/</link>
			<pubDate>Thu, 23 Dec 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/the-design-of-shopping-cart/</guid>
			<description>自從學習開發 Web 程式以來，我的工作就離不開購物車了。從模仿其他網站的購物機制開始，到接觸了物件導向後的所寫的購物車架構，每一次的經驗都讓我成長</description>
			<content type="html"><![CDATA[<p>自從學習開發 Web 程式以來，我的工作就離不開購物車了。從模仿其他網站的購物機制開始，到接觸了物件導向後的所寫的購物車架構，每一次的經驗都讓我成長不少。</p>
<p>不過每次所寫出來的購物車系統，不論是在新增功能或是修改上都讓我覺得非常麻煩；只要客戶有些稍微複雜一點的需求，常讓我改程式改到想翻桌。</p>
<p>後來接觸設計模式、 MVC 及 UnitTest 之後，一個新的購物車架構漸漸在我腦海裡成形。一來我不想再讓商品加入購物車、更改商品數量、促銷活動或是結帳等機制散落在各個 PHP 程式中，但我也不想讓它們完全集中在一個類別裡，那麼適當的架構分離就顯得非常重要。</p>
<p>於是乎，集合了多年的經驗，我在某個專案裡試做了一個新的購物車架構；而經過一段時間的線上測試後，事實證明它非常容易增加功能及修改功能，也更容易讓我們釐清整個購物流程。而且如果在良好的設計安排下，它也能做到模組化的功能抽換。我心中不由得吶喊：「就是它了！」</p>
<p>當然，這個機制並不是最好的，也可能無法因應所有網站的需求；但是這至少是我自己在電子商務技術這個領域的經驗，以及經過多次挫敗後所得到的成果。因此在以下的投影片裡，我將單純地就這個購物車機制來做一些探討，希望能為大家在架構的設計上，帶來一些不同的想法。</p>
<p>另外要提醒大家，這裡所提到的購物車架構，並沒有涉及所謂的後台商品上稿或是前台商品陳列等機制；換句話說，它不是一般我們所定義的購物車模組，請大家要先瞭解這一點。</p>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
]]></content>
		</item>
		
		<item>
			<title>台灣的軟體工程師</title>
			<link>https://jaceju.net/software-engineer-in-taiwan/</link>
			<pubDate>Tue, 22 Jun 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/software-engineer-in-taiwan/</guid>
			<description>我常常問自己，台灣的業界生態真的適合開發軟體嗎？如果不適合，那麼軟體工程師們到底是為了什麼而選擇這個工作呢？ 從網路上或是前輩口中所得到的大部</description>
			<content type="html"><![CDATA[<p>我常常問自己，台灣的業界生態真的適合開發軟體嗎？如果不適合，那麼軟體工程師們到底是為了什麼而選擇這個工作呢？</p>
<p>從網路上或是前輩口中所得到的大部份資訊裡，不難看出大多數的軟體工程師對於自己的職業生涯並沒有過於高深的期許；因為寫程式只不過是賺錢的手段之一，可以的話還是買買股票看能不能賺得比較飽。</p>
<p>以下，就我所看到的例子，來嘴炮一下大部份台灣軟體工程師的心聲吧。如有雷同，純屬巧合。</p>
<!-- raw HTML omitted -->
<h2 id="二十歲之前">二十歲之前</h2>
<p>「程式是什麼？」</p>
<p>大部份的軟體工程師其實都不是本科系出身，所以大多都不清楚軟體開發是個什麼樣的工作。而為了要找一份夠水準的工作，許多隱沒自己原本才能的朋友就到著名的補習班去，想辦法讓自己成為一位履歷份量看起來夠巨大的工匠。不過一旦踏入軟體開發這個領域之後，他們才開始明白，在台灣，這真的是一條漫長而艱辛的不歸路。</p>
<h2 id="二十歲的時候">二十歲的時候</h2>
<p>「我要學習高深的技術，所以我可以吃苦耐勞&hellip;」</p>
<p>多數有自覺的朋友在剛進入職場時，就發現自己本身的不足；也因此他們開始出沒於各大論壇或 BBS ，請教前輩們一些技術上的問題。有些人下班後，還特地跑到書局買一堆參考書籍堆滿自己的床頭，期待自己有天變成一條技術之龍。薪水太少？這從來不是他們所在乎的。</p>
<h2 id="二十五歲的時候">二十五歲的時候</h2>
<p>「工作有點多，好，我加班拼了~」</p>
<p>當自我的能力漸漸加強之後，上頭交辦的事項也越來越多了。不過對這些已經學到有用技巧的軟體工程師來說，不過就是小菜一碟，加個班就什麼都解決啦！沒什麼了不起。不過不曉得為什麼，他們每天加班完回家後，特別愛買些豬肝湯來當宵夜吃。</p>
<h2 id="三十歲的時候">三十歲的時候</h2>
<p>「你爸的，那個主管真機車&hellip;明天我要跟老婆去玩了，竟然還來個鬼需求&hellip;」</p>
<p>大部份的軟體工程師在變成職場老鳥之後，對於任何客戶丟過來的需求都能一眼看穿它們是不是真的需要執行；只不過通常決定要不要執行的人不會是軟體工程師，可能是空降下來，然後不小心就當上主管，那個董事長的兒子。</p>
<h2 id="三十五歲的時候">三十五歲的時候</h2>
<p>「老天爺&hellip;什麼工作都好&hellip;給我錢吧&hellip;」</p>
<p>人生終於爆發了危機，大部份沒有考慮自己未來職場生涯的軟體工程師，在可取代性過高的情況下，被每個月薪資只需要兩萬二的新人給 Override 了&hellip;.為了爭取更多的工作機會，他們又回到了補習班，然後整天在那裡「喔喔~ 喔喔~ 」的拼命學習&hellip;</p>
<h2 id="四十歲的時候">四十歲的時候</h2>
<p>「&hellip;」</p>
<p>台灣的軟體工程師哪來四十歲可活呀！？</p>
<h2 id="心得">心得</h2>
<p>健康誠可貴，生命價更高；若為金錢故，兩者皆可拋。</p>
]]></content>
		</item>
		
		<item>
			<title>將測試融入開發，讓程式擁有品質</title>
			<link>https://jaceju.net/my-first-tdd/</link>
			<pubDate>Sat, 12 Jun 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/my-first-tdd/</guid>
			<description>很多書上和網路的文章都提到了很 TDD 的做法，不過老實說我並沒有實際參與這些大師的 TDD 過程，其實很難把他們的方法百分之分地應用到自己的專案裡。 因此，</description>
			<content type="html"><![CDATA[<p>很多書上和網路的文章都提到了很 TDD 的做法，不過老實說我並沒有實際參與這些大師的 TDD 過程，其實很難把他們的方法百分之分地應用到自己的專案裡。</p>
<p>因此，凡事還是要自己動手做過才會明白，所以我就趁著在開發新的 Library 時，真正地去落實測試與重構。</p>
<p>以下就是我大致的心得，供大家參考。</p>
<!-- raw HTML omitted -->
<h2 id="我的測試驅動開發流程">我的測試驅動開發流程</h2>
<h3 id="第一步準備好工具">第一步：準備好工具</h3>
<p><a href="http://www.jaceju.net/blog/archives/1152">之前提過的 NetBeans 所提供的 PHPUnit 整合</a>，就是個很不錯的自動化測試工具。</p>
<p>在寫類別時，按下 Ctrl + F6 就會自動執行這個類別的測試 (當然前提是要已經先建立好測試類別) ；而如果是在撰寫測試類別時，可以按下 Shift + F6 ，這樣 NetBeans 就會自動執行測試類別了。這兩個快速鍵如果記住的話，會有助於我們加快做測試的動作。</p>
<h3 id="第二步草圖階段">第二步：草圖階段</h3>
<p>和設計階段的 UML 圖不一樣，這個階段算是撰寫程式碼了，所以要先想好你的程式要怎麼動，然後用畫個簡單的流程圖將它記下來。然後在圖塊上標明你會用到什麼資料，越詳細越好。</p>
<p>而流程圖不一定要是圖，也可以是步驟的表列，總之能表現出程式的意圖即可。至於畫在哪裡都可以，像我就習慣使用最單純的紙和筆。而草圖也不需要畫得多好看，除非你想向正妹展示你的繪圖天份。</p>
<h3 id="第三步撰寫測試">第三步：撰寫測試</h3>
<p>在草圖畫好後，就可以用測試去呈現它。測試的寫法先不用寫得太好，把概念呈現出來就可以了。這個動作主要的目的是讓我們對要測試的類別有個大致的輪廓，讓寫出來的程式碼不致於發散到我們無法掌控的地步。</p>
<h3 id="第四步撰寫程式">第四步：撰寫程式</h3>
<p>寫好第一個測試後，就可以開始寫程式去符合測試了。在這個過程中，最好能找個人一起討論，也就是所謂的 Pair Programming 。找正妹一起 Pair Programming 是會增加興奮度，但不會讓你的程式寫得更好。</p>
<p>另外，程式碼以快速產出為主，除了遵守 Coding Style 外，邏輯上並不用寫得多漂亮；因為如果有正妹在旁邊一起寫程式時，你的腦袋一定不會百分之百運轉，所以等等再重構就好。</p>
<h3 id="第五步停下思考">第五步：停下思考</h3>
<p>當程式碼在可以符合測試後，就停下來想想這樣的設計是不是符合原先的想法。</p>
<p>不符合的話，就再重來，因為我們只前進了一小步，所以不必擔心花太多時間，畢竟找出不良的設計也是一種進展。符合的話，就再往下走；也就是撰寫下一個測試，然後再撰寫程式符合它。</p>
<h3 id="第六步重構">第六步：重構</h3>
<p>完成一小階段後，就馬上回頭重構我們的程式碼，這時有測試當靠山，就不用怕出問題。</p>
<p>而當程式碼重構完後，看就再回頭看看測試碼，如果有需要的話就重構它；因為測試本身也是程式，一定也會有壞味道。例如我們可以用 PHPUnit 的 @dataProvider 機制，去重構要驗證不同資料集但流程卻重複的測試；或是用 Template Method 去重構大部份流程相同，只有少部份流程有差異的測試。</p>
<p>而重構測試碼的時候，我們的程式碼就可以做為測試的測試，用來驗證重構後的測試。</p>
<p>完成這個步驟後，就可以繼續進行下一個小階段了。</p>
<h2 id="心得">心得</h2>
<p>真正將 TDD 落實到平常的開發後，我發現這樣的方式會幫助我的思考清晰，考試都一百分 (因為我沒正妹在旁邊一起 Pair Programming &hellip;) 。而且另一個好處就是不論是一開始的撰寫或是事後的重構，我都可以精簡我的程式碼。</p>
<p>不過我並無法保證 TDD 一定適合你，因為每個人的開發習慣不一定相同，不過還是推薦大家試試吧。</p>
]]></content>
		</item>
		
		<item>
			<title>Gearman 心得</title>
			<link>https://jaceju.net/gearman/</link>
			<pubDate>Mon, 07 Jun 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/gearman/</guid>
			<description>前幾天， Glenn 與 Mark 分享了 Gearman 的觀念與實作，以下就是我簡單的筆記與心得。 問題 以往我在開發會員註冊功能時，通知信總是即時寄出；雖然寄信這個動作不會花太</description>
			<content type="html"><![CDATA[<p>前幾天， Glenn 與 Mark 分享了 Gearman 的觀念與實作，以下就是我簡單的筆記與心得。</p>
<!-- raw HTML omitted -->
<h2 id="問題">問題</h2>
<p>以往我在開發會員註冊功能時，通知信總是即時寄出；雖然寄信這個動作不會花太多時間，但遇到網路塞車或是郵件伺服器反應較慢時，那麼會員就有可能要等好一陣子才能進到下一個網頁。</p>
<p>那麼我們要怎麼解決這種問題呢？</p>
<p>如果大家有用過 SlideShare 或是 YouTube 的話，這些網路服務在我們上傳檔案後，就會開始做轉換的動作，但卻又不必讓我們在那裡傻傻的等；那麼同樣的道理，如果我們能把寄信這個訊息丟到一個負負發信的機器去，然後就繼續處理我們的工作，而不必等候它的通知，這樣不就解決我們的問題了嗎？</p>
<p>這個技巧就稱為「 Message Queue 」。</p>
<h2 id="message-queue-原理">Message Queue 原理</h2>
<p>想像一下我們人現在正在銀行，現在櫃台窗口的辦事人員都在忙碌，而門口的服務人員會親切地給我們票卡，讓我們在一旁稍等；這時我們可以先看看報紙，或是打電話先到公司交代一些事情，等待有空的窗口呼叫我們就可以了。</p>
<p>用程式的角度來說，當我們發出請求之後並不一定馬上就要處理，而是先進入佇列等候，這時我們就可以先行往下執行其他步驟；當可以處理該需求的資源有空閒時，就會幫我們做處理。這種模式就是 Message Queue 的基本概念。</p>
<p>所以在 Message Queue 中就有以下這三個角色：</p>
<ul>
<li>
<p>Client ：就是需要服務的客戶；也就是發送需求的程式。</p>
</li>
<li>
<p>Job Server ：就是整間銀行，更嚴格的來說，它指的是「瞭解客戶需要何種服務，並查看哪個窗口可以處理這項需求」的機制。一般來說，在系統裡它通常會是個 Daemon 。</p>
</li>
<li>
<p>Worker ：負責處理客戶需求的櫃台人員；也就是實際處理需求的程式。</p>
</li>
</ul>
<p>我們簡單用下圖來說明：</p>
<p><a href="/resources/gearman/message_queue.png"><img src="/resources/gearman/message_queue.png" alt="Message Queue 概念圖"></a></p>
<p>首先，執行 Message Queue 服務的 Job Server 可以是多台伺服器組成，也就是分散式架構。然後我們會在 Job Server 上註冊並執行 Worker 程式，這些 Worker 程式會一直循環地等候，直到 Job Server 呼叫它執行工作。</p>
<p>在 Client 發送出需求之後，會將要需要的資料及動作記錄在 Job Server 上，這時 Job Server 會查看是否有空閒並且符合需求的 Worker ；例如 Client 程式告訴 Job Server 要寄信，那麼 Job Server 就會查看負責寄信的 Worker 目前是否有空。當 Worker 有空時，那麼 Job Server 就會從佇列中把 Client 的需求轉移給 Worker 開始執行。</p>
<p>在 Worker 結束工作後，也會發送通知給 Job Server ，這時 Job Server 就會視狀況把結果回傳給 Client 。也就是這樣的機制，讓 Client 不必再等候需求的執行結果，而可以直接再往下執行其他動作。</p>
<p>值得注意的是，一般 Client 和 Job Server 的主機會是分開的；這樣的架構，才不會造成執行 Client 程式主機的負擔。不過稍後的示範裡，我們會在同一台主機上實作 Client 和 Worker 。</p>
<h2 id="gearman-簡介">Gearman 簡介</h2>
<p>實作 Message Queue 套件有很多， Gearman 也是其中之一。它的詳細歷史與介紹請參考<a href="http://gearman.org/index.php?id=manual">官方說明</a>，以下我們簡單介紹它的應用方式：</p>
<p>下圖取自官方手冊，主要是說明 Gearman 的運作機制：</p>
<p><a href="http://gearman.org/img/stack.png"><img src="http://gearman.org/img/stack.png" alt="Gearman 流程圖"></a></p>
<p>藍色部份是由我們開發的程式碼，而黃色部份是由 Gearman 或第三方 API 提供的。也因為只要符合 API 規範就可以跟 Gearman 溝通，所以 Client 和 Worker 並不需要用同樣的語言來實作 API ；例如我們可以在 Client 端使用 PHP 開發程式，在 Worker 端使用 C 或 Perl 來開發，因為它們有提供 Gearman 的 API 來供我們呼叫。</p>
<p>註：在官方網站的<a href="http://gearman.org/download/">下載頁</a>中，可以看到分別以各種語言實作的 Gearman API Library 。</p>
<p>另外 Gearman 也提供了 Persistent Queues 的功能，也就是當 Worker 在無法提供服務時， Job Server 會將 Queue 保留 Persistent Storage 中，以便在 Worker 恢復運作時能再次運行。</p>
<h2 id="安裝-gearman-job-server">安裝 Gearman Job Server</h2>
<p>Gearman 在官方網站上已經提供了各種套件版本的<a href="http://gearman.org/getting-started/">安裝說明</a>，不過目前不論是 Server 端或 Client API 端，都不提供 Windows 版本，因此以下的安裝與範例我都將以 Ubuntu 10.04 為主。</p>
<blockquote>
<p>註：這裡我也假設大家已經安裝好了 PHP 。</p>
</blockquote>
<p>先利用以下的指令來安裝 gearman 及相關套件：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">sudo apt-get install gearman-job-server
</code></pre></div><p>完成後， Gearman Job Server 就會在我們的系統中啟動了。</p>
<h2 id="設定-persistent-queue">設定 Persistent Queue</h2>
<p>接著我們要在 MySQL 中先建立一個 gearman 資料庫，這樣稍後啟動 Gearman Job Server 時，才能建立所需要的資料表：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nb">echo</span> <span class="s1">&#39;CREATE DATABASE gearman&#39;</span> &gt; /tmp/temp.sql <span class="p">;</span> mysql -u root -p &lt; /tmp/temp.sql <span class="p">;</span> rm -f /tmp/temp.sql
</code></pre></div><p>而為了讓 Gearman Job Server 能夠串接 MySQL ，我們要在 Service Script 中設定相關參數。編輯 <code>/etc/init/gearman-job-server.conf</code> 這個檔案：</p>
<p>將：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nb">exec</span> start-stop-daemon --start --chuid gearman --exec /usr/sbin/gearmand -- --log-file<span class="o">=</span>/var/log/gearman-job-server/gearman.log
</code></pre></div><p>置換為：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">script
    . /etc/default/gearman-job-server
    <span class="nb">exec</span> start-stop-daemon --start --chuid gearman --exec /usr/sbin/gearmand -- <span class="nv">$PARAMS</span> --log-file<span class="o">=</span>/var/log/gearman-job-server/gearman.log
end script
</code></pre></div><blockquote>
<p>註：這邊是個 bug ，可以參考 <a href="http://jeremykendall.net/2014/09/04/ubuntu-14-dot-04-gearman-config-bug/">Ubuntu 14.04 Gearman Config Bug</a> 一文。</p>
</blockquote>
<p>然後再編輯 <code>/etc/default/gearman-job-server</code> ，將：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nv">PARAMS</span><span class="o">=</span><span class="s2">&#34;--listen=localhost&#34;</span>
</code></pre></div><p>置換成：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="nv">PARAMS</span><span class="o">=</span><span class="s2">&#34;-q mysql --mysql-host 127.0.0.1 \
</span><span class="s2">                 --mysql-user root \
</span><span class="s2">                 --mysql-password secret \
</span><span class="s2">                 --mysql-db gearman \
</span><span class="s2">                 --mysql-table gearman_queue&#34;</span>
</code></pre></div><p>其中 <code>--mysql-host</code> 可換成各位慣用的 MySQL 伺服器 IP ，而 <code>--mysql-user</code> 、 <code>--mysql-password</code> 則是要有 <code>CREATE TABLE</code> 的權限。</p>
<p>最後重新啟動 Gearman Job Server ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">sudo service gearman-job-server restart
</code></pre></div><p>我們可以用 ps 指令來查看啟動是否成功：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">ps aux <span class="p">|</span> grep gearman
</code></pre></div><p>出現以下結果的話，就表示我們成功安裝並設定好 Gearman Job Server 了。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">gearman   <span class="m">7158</span>  0.0  0.3 <span class="m">483732</span>  <span class="m">7384</span> ?        Ssl  16:01   0:00 /usr/sbin/gearmand -q mysql --mysql-host 127.0.0.1 --mysql-user root --mysql-password secret --mysql-db gearman --mysql-table gearman_queue --log-file<span class="o">=</span>/var/log/gearman-job-server/gearman.log
</code></pre></div><h2 id="安裝-php-gearman-api-extension">安裝 PHP Gearman API Extension</h2>
<p>因為後面的範例是使用 PHP 做示範，所以我們安裝 Gearman Extension ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">sudo apt-get install php5-gearman
</code></pre></div><h2 id="簡易實作">簡易實作</h2>
<p>接下來，我們可以試著用 PHP API 來連接 Job Server 。前面安裝好 PECL 的 Gearman Extension 後，我們就可以在 PHP 程式裡建立操作 Gearman API 的物件了。</p>
<p>以下我用簡單的方式來模擬 Client 和 Worker 的運作，所以這裡 Client 和 Worker 會在同一部主機上，但實際運作時是不需要的，請大家注意。</p>
<h3 id="client-端程式">Client 端程式</h3>
<p>先看看 client.php ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">GearmanClient</span><span class="p">();</span>
<span class="nv">$client</span><span class="o">-&gt;</span><span class="na">addServer</span><span class="p">();</span> <span class="c1">// 預設為 localhost
</span><span class="c1"></span><span class="nv">$emailData</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span>
    <span class="s1">&#39;name&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;web&#39;</span><span class="p">,</span>
    <span class="s1">&#39;email&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;member@example.com&#39;</span><span class="p">,</span>
<span class="p">);</span>
<span class="nv">$imageData</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span>
    <span class="s1">&#39;image&#39;</span> <span class="o">=&gt;</span> <span class="s1">&#39;/var/www/pub/image/test.png&#39;</span><span class="p">,</span>
<span class="p">);</span>
<span class="nv">$client</span><span class="o">-&gt;</span><span class="na">doBackground</span><span class="p">(</span><span class="s1">&#39;sendEmail&#39;</span><span class="p">,</span> <span class="nx">serialize</span><span class="p">(</span><span class="nv">$emailData</span><span class="p">));</span>
<span class="k">echo</span> <span class="s2">&#34;Email sending is done.</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="nv">$client</span><span class="o">-&gt;</span><span class="na">doBackground</span><span class="p">(</span><span class="s1">&#39;resizeImage&#39;</span><span class="p">,</span> <span class="nx">serialize</span><span class="p">(</span><span class="nv">$imageData</span><span class="p">));</span>
<span class="k">echo</span> <span class="s2">&#34;Image resizing is done.</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
</code></pre></div><p>首先， PHP Gearman Extension 提供了一個名為 <a href="http://www.php.net/manual/en/class.gearmanclient.php">GearmanClient</a> 的類別，它可以讓程式安排工作給 Job Server 。</p>
<p>而 <a href="http://www.php.net/manual/en/gearmanclient.addserver.php">addServer</a> 方法表示要通知的是哪些 Job Server ，也就是說如果有多台 Job Server 的話，就可以透過 addServer 新增。</p>
<p>然後我們將要呼叫哪個 Worker 以及該 Worker 所需要的資料，利用 GearmanClient 的 <a href="http://www.php.net/manual/en/gearmanclient.dobackground.php">doBackground</a> 方法傳送過去。 doBackground 方法顧名思義就是在背景執行， Client 在丟出需求後就可以繼續處理其他的程式，也就是我們常說的「射後不理」。</p>
<p>doBackground 方法的第一個參數是告訴 Job Server 要執行哪個功能，而這個功能則是由 Worker 提供的；要注意是，這個參數只是識別用的，並不是真正的函式名稱。而第二個參數是要傳給 Worker 的資料，它必須是個字串；因此如果要傳送的是陣列的話，我們就要用 PHP 的 serialize 函式來對這些資料做序列化。</p>
<h3 id="worker-端程式">Worker 端程式</h3>
<p>接下來我們要製作 Worker ，以下就是 worker.php ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$id</span> <span class="o">=</span> <span class="nx">microtime</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
<span class="nv">$worker</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">GearmanWorker</span><span class="p">();</span>
<span class="nv">$worker</span><span class="o">-&gt;</span><span class="na">addServer</span><span class="p">();</span> <span class="c1">// 預設為 localhost
</span><span class="c1"></span><span class="nv">$worker</span><span class="o">-&gt;</span><span class="na">addFunction</span><span class="p">(</span><span class="s1">&#39;sendEmail&#39;</span><span class="p">,</span> <span class="s1">&#39;doSendEmail&#39;</span><span class="p">);</span>
<span class="nv">$worker</span><span class="o">-&gt;</span><span class="na">addFunction</span><span class="p">(</span><span class="s1">&#39;resizeImage&#39;</span><span class="p">,</span> <span class="s1">&#39;doResizeImage&#39;</span><span class="p">);</span>
<span class="k">while</span> <span class="p">(</span><span class="nv">$worker</span><span class="o">-&gt;</span><span class="na">work</span><span class="p">())</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$worker</span><span class="o">-&gt;</span><span class="na">returnCode</span><span class="p">()</span> <span class="o">!=</span> <span class="nx">GEARMAN_SUCCESS</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nx">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span> <span class="c1">// 無限迴圈，並讓 CPU 休息一下
</span><span class="c1"></span><span class="p">}</span>
<span class="k">function</span> <span class="nf">doSendEmail</span><span class="p">(</span><span class="nv">$job</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">global</span> <span class="nv">$id</span><span class="p">;</span>
    <span class="nv">$data</span> <span class="o">=</span> <span class="nx">unserialize</span><span class="p">(</span><span class="nv">$job</span><span class="o">-&gt;</span><span class="na">workload</span><span class="p">());</span>
    <span class="nx">print_r</span><span class="p">(</span><span class="nv">$data</span><span class="p">);</span>
    <span class="nx">sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span> <span class="c1">// 模擬處理時間
</span><span class="c1"></span>    <span class="k">echo</span> <span class="s2">&#34;</span><span class="si">$id</span><span class="s2">: Email sending is done really.</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">function</span> <span class="nf">doResizeImage</span><span class="p">(</span><span class="nv">$job</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">global</span> <span class="nv">$id</span><span class="p">;</span>
    <span class="nv">$data</span> <span class="o">=</span> <span class="nx">unserialize</span><span class="p">(</span><span class="nv">$job</span><span class="o">-&gt;</span><span class="na">workload</span><span class="p">());</span>
    <span class="nx">print_r</span><span class="p">(</span><span class="nv">$data</span><span class="p">);</span>
    <span class="nx">sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span> <span class="c1">// 模擬處理時間
</span><span class="c1"></span>    <span class="k">echo</span> <span class="s2">&#34;</span><span class="si">$id</span><span class="s2">: Image resizing is really done.</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>PHP 的 Gearman Extension 也提供了一個 GearmanWorker 類別，讓我們可以實作 Worker 。而 GearmanWorker 類別也提供了 <a href="http://www.php.net/manual/en/gearmanworker.addserver.php">addServer</a> 方法，讓所生成的 Worker 物件可以註冊到 Job Server 中。</p>
<p>另外 GearmanWorker 類別也提供了 <a href="http://www.php.net/manual/en/gearmanworker.addfunction.php">addFuncton</a> 方法，告訴 Job Server 自己可以處理哪些工作。 addFunction 的第一個參數就是對應到 GearmanClient::doBackground 方法的第一個參數，也就是功能名稱；這使得 Client 和 Worker 能透過這個名稱來互相溝通。而第二個參數則是一個 <a href="http://www.php.net/manual/en/language.pseudo-types.php#language.types.callback">callback</a> 函式，它會指向真正應該要處理該工作的函式或類別方法等。</p>
<p>最後因為 Worker 因為要隨時準備服務，是不能被中斷的，因此我們透過一個無限迴圈來讓它常駐在 Job Server 中。</p>
<h3 id="測試">測試</h3>
<p>準備好 Client 和 Worker 的程式後，就可以測試看看了。首先我們必須得先執行 worker.php ，讓它開始服務。</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">php worker.php
</code></pre></div><p>這時我們會看到 worker.php 停駐在螢幕上等待服務。</p>
<p>接著我們開啟另一個 console 視窗來執行 client.php ：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">php client.php
</code></pre></div><p>會立刻出現以下結果：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">Email sending is <span class="k">done</span>.
Image Resizing is <span class="k">done</span>.
</code></pre></div><p>而切換到執行 worker.php 的 console 時，就會看到以下執行結果：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">Array
<span class="o">(</span>
    <span class="o">[</span>who_send<span class="o">]</span> <span class="o">=</span>&gt; web
    <span class="o">[</span>get_email<span class="o">]</span> <span class="o">=</span>&gt; member@example.com
<span class="o">)</span>
Email sending is really <span class="k">done</span>.
Array
<span class="o">(</span>
    <span class="o">[</span>image<span class="o">]</span> <span class="o">=</span>&gt; /var/www/pub/image/test.png
<span class="o">)</span>
Image resizing is really <span class="k">done</span>.
</code></pre></div><p>這表示 Worker 正常地處理 Client 的需求了。</p>
<p>現在試著把 worker.php 停掉 (Ctrl+C) ，然後再執行 client.php ，大家應該會發現 client.php 還是正常地完成它的工作；這是因為 Job Server 幫我們把需求先放在 Queue 裡，等待 Worker 啟動後再處理。</p>
<p>這時可以查看 MySQL 的 gearman 資料庫，在 gearman_queue 資料表中應該就會看到以下結果：</p>
<p><a href="/resources/gearman/queue_table.png"><img src="/resources/gearman/queue_table.png" alt="Queue 資料表"></a></p>
<p>這表示 Job Server 成功地將 Queue 保留在 MySQL 資料表中。</p>
<p>接著再執行 worker.php ，這時 Job Server 會得知 Worker 復活，趕緊將 Queue 裡面屬於該 Worker 應該執行的工作再發送出去以完成作業；而 Worker 完成作業後， Job Server 就會把 Queue 清空了。</p>
<p>是不是很有趣呢？</p>
<h2 id="心得">心得</h2>
<p>Message Queue 這個架構的應用可以說相當廣泛，尤其在大流量的網站上，我們能透過它來來有效運用分散式的系統架構，以處理更多使用者的需求。</p>
<p>而目前 Gearman 可說是在 PHP 上一個很棒的 Message Queue 支援套件，而且 API 也相當完善；因此如果能善用 Gearman 的話，那麼我們在 PHP 網站的架構上就可以有更大的延展性，也能有更多的可能性。</p>
]]></content>
		</item>
		
		<item>
			<title>關於學習技術這件事</title>
			<link>https://jaceju.net/about-learning/</link>
			<pubDate>Mon, 24 May 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/about-learning/</guid>
			<description>專業技術從來就不是人天生就會的，所以我們才需要去學習它。可是有許多人在學習新技術時，常常只是把書瀏覽過一遍，然後真正要下手時卻不知所措。其實</description>
			<content type="html"><![CDATA[<p>專業技術從來就不是人天生就會的，所以我們才需要去學習它。可是有許多人在學習新技術時，常常只是把書瀏覽過一遍，然後真正要下手時卻不知所措。其實在學習技術的方法有很多，只不過並沒有哪個方法特別好，就看這個方法適不適合我們而已。</p>
<p>然而適合每個人學習的方式不一定相同，以下就是我個人以往的學習經驗，供大家參考看看。</p>
<!-- raw HTML omitted -->
<h2 id="瞭解">瞭解</h2>
<p>我在學一個技術之前，都會先去瞭解它的背景觀念，並知道為什麼要需要學它。</p>
<p>很多技術都有它的來源背景，當我花心思去瞭解發明這項技術的人的想法，並且也明白這個技術要解決什麼樣的問題之後，就會比較容易接受這個技術，進而快速進入學習它的大門。</p>
<p>可是千萬不要在只懂了這技術一小部份的來由之後就開始自滿了起來，並用自己狹隘的觀點來評斷這個技術。因為任何技術的誕生都有它的時空背景，即便它不適合我們現在的環境；我們該做的是思考這項技術的優缺點，讓自己能更善於應用它，或是在未來不再犯下同樣的錯誤。</p>
<h2 id="記憶">記憶</h2>
<p>把想學的技術，用心智圖畫出來；學過的就用圖示標起來，讓自己知道學過了哪些。</p>
<p>通常我的記憶是不可靠的，常常學過的東西也很快就忘了，這時簡單的筆記以及部落格就是幫助我記憶的最大利器了。不過這樣記下的東西常常是雜亂的，所以最好再用較為易於整理的系統 (例如 Wiki ) ，來將這些技術心得歸納與吸收。</p>
<h2 id="應用">應用</h2>
<p>技術不用懂到八、九成，重點是做中學、學中做。</p>
<p>其實我很少去將一個技術研究到透徹，一來我沒有那個時間，二來沒有實作的輔助，我很難去更深入這個技術的核心。因此我常是學到一個階段，並且謹責地評估之後，就會實際地將它應用在某些專案上。而從這些專案實際上遇到的問題中，我就可以瞭解這項技術的優缺點，也能驅動我研究有關該技術更深一層的知識。</p>
<h2 id="期望">期望</h2>
<p>規劃短期的學習目標，想像自己在目標完成後會變成什麼樣的一個人。</p>
<p>在我剛開始學習物件導向的時候，我其實很盲目。但是經過自己設定目標並且不斷去練習後，我慢慢發現雖然我並無法像高手那般對物件導向運用自如，但卻也已經能看懂他們寫出來的程式。我很高興我有這樣的成果，這促使我朝著物件導向開發的方向前進。</p>
<p>一直到現在，雖然我在物件導向的觀念上還談不上是非常成熟，但有些概念已經漸漸融會貫通；至少，我已不再是物件導向苦手！</p>
<h2 id="分享">分享</h2>
<p>問問題要先做功課，得到解答後也要懂得反饋。</p>
<p>很多朋友學習技術時，一遇到問題就問，但是他們卻常常碰了好大的釘子。通常很大的原因是他們並沒有自己先去思考過這個問題，也沒有在這個領域先去打好必要的基礎。人必自助而後人助之，當我們沒花心思去解決問題時，怎麼能期待別人花時間幫我們解決呢？</p>
<p>而以前常常在技術論壇裡打混的我，大部份的時候是試著去解決別人的問題，而不是去問問題。在試著解決這些問題的過程中，我獲得了很多以前自己忽略的觀念；而且當我分享我的方法後，也會有高手願意跟我討論我的方法的優缺點，那種成長是言語難以形容的！</p>
<h2 id="恆心">恆心</h2>
<p>下決心去做很難，但持之以恆更難。</p>
<p>學習技術其實是一條很漫長的路，很多人都是為了工作而不得不去學；而有些人一開始是非常有興趣，但到後面就發現自己其實根本只是三分鐘熱度而已。而我個人對於有些技術，其實也常抱有同樣的觀念；所以有時間的話，我常會回頭去重看一些觀念和基礎的部份。畢竟以往我的根紮得不夠深，如果只是一味地往上成長，到時候只是讓自己倒得更快而已。</p>
<p>記住，讓自己永遠保持在學習的心態上，因為我們不是天才。</p>
]]></content>
		</item>
		
		<item>
			<title>關於測試這件事</title>
			<link>https://jaceju.net/about-testing/</link>
			<pubDate>Fri, 14 May 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/about-testing/</guid>
			<description>為什麼要測試？ 因為我們沒辦法保證程式碼是萬無一失的，所以通常我們都會需要驗證我們的程式，而測試就是其中一種方法。很多工程師都在寫完程式之後，</description>
			<content type="html"><![CDATA[<h2 id="為什麼要測試">為什麼要測試？</h2>
<p>因為我們沒辦法保證程式碼是萬無一失的，所以通常我們都會需要驗證我們的程式，而測試就是其中一種方法。很多工程師都在寫完程式之後，自己手動去做測試；如果執行結果是按照自己的想法出現後，那麼他們通常就認為程式沒錯了。</p>
<p>可是，其實這樣的程式碼通常就會躲進很多很多的 Bug 。所以我們要<!-- raw HTML omitted -->讓程式碼自己告訴我們它沒問題，而不是我們自己去想像它沒問題<!-- raw HTML omitted -->。</p>
<!-- raw HTML omitted -->
<p>當然測試並不是萬靈丹，它只是去驗證我們所考慮到的狀況；但如果抱持著「既然我都有考慮到，而且也寫在程式碼裡，那這樣就不用測試了呀！」的態度，那其實也是災難的根源。<!-- raw HTML omitted -->因為如果我們沒有寫測試來驗證程式碼的話，那我們的防範措施根本就不值得信賴！<!-- raw HTML omitted --></p>
<p>所以不是手動測試過沒問題，下次就不會再發生了；而是要拿出保證來才值得相信，而測試就是我們的保證。</p>
<h2 id="如何確保系統照著規格實作">如何確保系統照著規格實作？</h2>
<p>符合客戶所要求的規格，就是我們實作系統的最終目標。有些規格是流程的控制，有些規格是資料的處理；但這些如果沒有一套好的檢驗機制來協助我們確認這些規格的話，我們就沒辦法有百分之百的信心告訴客戶說，我們的程式完成了你們的需求。</p>
<p>自動化測試就是一個方便的檢驗機制，而測試案例就是客戶的規格。當我們的程式通過測試後，我們也就可以放心把這些程式碼用在實際的開發中；如果這時還發現執行的結果有問題的話，通常就就可以判斷是實際開發的程式碼中出現了錯誤。</p>
<p>當然有些工程師會認為：為什麼我要把同樣的程式碼在測試案例和實作運作的系統中各寫一遍？其實這是錯誤的觀念。</p>
<p>測試案例分成兩種：一種是在程式正常運作下的案例，它們能明確告訴我們程式的使用方式，也就是客戶要求的規格；一種是設想到可能會讓程式出現問題的案例，它們考驗著身為工程師應該去設想到的錯誤處理機制。</p>
<p>當客戶新增或修改了規格時，那麼我們的測試案例就要跟著調整。<!-- raw HTML omitted -->而沒有調整的部份，我們就能透過現有的測試案例來確保舊有程式按照舊的規格執行，確保我們的程式沒有因為新增或修改的功能而出現預料之外的錯誤。<!-- raw HTML omitted --></p>
<h2 id="怎麼寫出好測試">怎麼寫出好測試？</h2>
<p>好的測試案例依賴著工程師的經驗，經驗越豐富的工程師，就能預想到各種可能發生的狀況，並寫出因應的測試案例。不過有經驗的工程師畢竟是少數，所以大多數時候我們還是要按步就班，從我們能做得到的部份做起。</p>
<p>一開始我們可以先對正常的流程編寫測試；而在程式通過正常測試之後，我們再針對有可能造成我們程式誤動作的狀況去加入異常的測試。有時候 Bug 可能是因為操作流程錯誤而產生，那麼就要在流程中的每個關卡做好程序的把關。</p>
<p>例如某些關卡要有防呆機制，某些關卡不能重覆執行，我們就必須去防止等等。測試之所以要強調自動化，就是讓我們不用把時間花在手動去模擬使用者操作的過程；而且我們可以透過對調測試案例裡的每個觸發條件的執行順序來產生不同的測試方式，以找出可能潛藏的流程問題。</p>
<p>有時候我們的系統可能會有多個使用者同時在操作，如何讓獨立性的資料不互相干擾，或是避免資料更新的先後順序問題 (例如商品庫存) ，這都是好的測試應該注意的重點。</p>
<p>重要的是，每個測試案例是要有意義的，我們在撰寫測試之前就要先分析它們的前因後果，才能擬定出正確的測試方向。如果覺得自己寫的流程測試可能有盲點時，那麼也可以請伙伴幫我們一起思考可能的狀況。</p>
<h2 id="測試太花時間">測試太花時間？</h2>
<p>在很多 Web 系統開發的過程中，其實很少有人會去考慮測試這件事情；主要是因為 Web 系統中大部份只是簡單的 CRUD 而已，加上時程的壓力，就會讓很多開發人員放棄撰寫自動化測試。</p>
<p>不過在一個較具規模的系統的規劃與設計過程中，就應該將自動化測試考慮進來；而且<!-- raw HTML omitted -->即便有時程壓力，也應該對核心功能撰寫測試案例。<!-- raw HTML omitted -->尤其當核心功能牽扯到有關金流等不容有錯的部份時，更是應該用測試來嚴格把關。</p>
<p>更好的方式是將撰寫測試的時間在一開始就放到時程規劃裡，其他的就讓專案經理去傷腦筋，即便是在維護階段時也是如此。<!-- raw HTML omitted -->因為如果多花半個小時寫自動化測試的話，那麼未來找 Bug 的時間就至少可以減掉半天。<!-- raw HTML omitted --></p>
<h2 id="既有的程式該如何測試">既有的程式該如何測試？</h2>
<p>如果大部份已經在運作的程式沒有對應的自動化測試，而在它們發生問題就要花很多時間來除錯時，就應該考慮安排時間進行重構與撰寫測試。</p>
<p>首先在第一次重構時，可以先考慮把核心邏輯獨立出來；如果這個動作在執行上相當困難的話，那麼就先將資料或網路存取的部份獨立出來。這些外在因素我們可以透過 Dependency Injection 的方式來重構，然後在自動化測試時用 Stub 物件來代替它們。</p>
<p>當程式重構至無關外在變因，且手動測試通過後，就可以開始考慮撰寫自動化測試；這時還是要按照「先測正常流程，再測異常流程」的步驟來進行。而其中測試用的假資料可以從現有的資料中產生，這些資料因為已經通過客戶的驗證，更能確保測試的可靠性。</p>
<p>另外有些既有的錯誤是必須在長時間才能發現，這時候我們可能就要用推測的方式來撰寫測試案例。這樣一來有兩個好處，其一是我們就可以讓測試自動化去代替手動測試的部份，其二是如果未來還是發生同樣問題時，我們也能透過這些測試來排除掉先前推測出來的因素。</p>
<h2 id="結論">結論</h2>
<p>其實我們也很難百分之百地保證通過測試的程式碼一定完全沒問題，但很重要的一點就是我們至少能<!-- raw HTML omitted -->保證<!-- raw HTML omitted -->通過測試的部份是會按照我們的想法來執行。</p>
<p>一個好的系統在交到客戶手上時，就應該要有一定的品質；而品質的保證來自我們的信心。至於我們的信心並不是建構在想像之上，而是應該用實際的測試數據來做為它的基礎。</p>
]]></content>
		</item>
		
		<item>
			<title>如何在 PHP 中平順地處理 Error 及 Exception ？</title>
			<link>https://jaceju.net/handle-php-error-and-exception/</link>
			<pubDate>Fri, 23 Apr 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/handle-php-error-and-exception/</guid>
			<description>在開發 PHP 的時候，最麻煩的事情之一就是處理錯誤。一個好的程式除了要將錯誤訊息呈現給使用者知道之外，也要讓該結束的部份正常結束才行。 而在 PHP5 之後，</description>
			<content type="html"><![CDATA[<p>在開發 PHP 的時候，最麻煩的事情之一就是處理錯誤。一個好的程式除了要將錯誤訊息呈現給使用者知道之外，也要讓該結束的部份正常結束才行。</p>
<p>而在 PHP5 之後，除了以往的 Error Handling 之外，還多了 Exception Handling ，使得程式變得更難去處理錯誤；所以大多數的開發者只能雙手一攤，讓這些錯誤訊息大剌剌地出現在使用者面前。</p>
<p>有沒有什麼好方法可以讓我們好好控制 Error 和 Exception 呢？</p>
<!-- raw HTML omitted -->
<h2 id="傳統的做法">傳統的做法</h2>
<p>在很多書籍和網路範例裡，當程式出錯時就是讓程序直接死掉，最常見的就是資料庫連線：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$link = mysql_connect(&#39;localhost&#39;, &#39;mysql_user&#39;, &#39;mysql_password&#39;);

if (!$link) {
    die(&#39;Could not connect: &#39; . mysql_error());
}

echo &#39;Connected successfully&#39;;
mysql_close($link);
</code></pre></div><p>或是在送出導向 header 後，就直接 exit ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">header(&#34;Location: http://www.example.com/&#34;); /* Redirect browser */
/* Make sure that code below does not get executed when we redirect. */
exit;
</code></pre></div><p>這些都不是好做法，因為有些流量較大的網站裡可能有多個資料庫連線，或是檔案的 handler 仍在開啟中；如果直接讓程序死亡或離開的話，就沒辦法將這些已經開啟的資源給正常關閉掉，進而造成系統的不穩定。</p>
<h2 id="exception-的處理">Exception 的處理</h2>
<p>PHP5 中，有個 <a href="http://www.php.net/manual/en/function.set-exception-handler.php"><code>set_exception_handler</code></a> 這個函式，它可以幫我們處理 Exception ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function exception_handler($exception)
{
    echo &#34;Uncaught exception: &#34; , $exception-&gt;getMessage(), &#34;\n&#34;;
}

set_exception_handler(&#39;exception_handler&#39;);
throw new Exception(&#39;Uncaught Exception&#39;);
echo &#34;Not Executed\n&#34;;
</code></pre></div><p>不過我個人認為用 <a href="http://www.php.net/manual/en/language.exceptions.php"><code>try...catch</code></a> 會讓我們在程式流程上的彈性更大：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function inverse($x)
{
    if (!$x) {
        throw new Exception(&#39;Division by zero.&#39;);
    }
    else return 1/$x;
}

try {
    echo inverse(5) . &#34;\n&#34;;
    echo inverse(0) . &#34;\n&#34;;
} catch (Exception $e) {
    echo &#39;Caught exception: &#39;,  $e-&gt;getMessage(), &#34;\n&#34;;
}

// Continue execution
echo &#39;Hello World&#39;;
</code></pre></div><p>而在我研究過 Zend Framework 的做法後，發現它在處理 Exception 上更加聰明。 Zend Framework 在 Controller 中引入一個 Response 物件，所有對瀏覽器的輸出都要經過它 (例如 Header 、 Content Body 等等) ；而這個 Reponse 物件也同時控管著 Exception 是否要被輸出到瀏覽器端，讓程式開發者能有更大的空間處理 Exception 。</p>
<p>以下我簡單把 Zend Framework 在 Response 物件中處理 Exception 的概念整理成一個自製的 <code>Response</code> 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Response
{
    private $_exceptions = array();

    private $_renderExceptions = false;

    public function setException(Exception $e)
    {
        $this-&gt;_exceptions[] = $e;
    }

    public function getExceptions()
    {
        return $this-&gt;_exceptions;
    }

    public function isException()
    {
        return !empty($this-&gt;_exceptions);
    }

    public function renderExceptions($flag = null)
    {
        if (null !== $flag) {
            $this-&gt;_renderExceptions = $flag ? true : false;
        }
        return $this-&gt;_renderExceptions;
    }

    public function sendResponse()
    {
        echo &#34;Header sending...\n&#34;;
        $exception = &#39;&#39;;
        if ($this-&gt;isException() <span class="ni">&amp;amp;&amp;amp;</span> $this-&gt;renderExceptions()) {
            foreach ($this-&gt;getExceptions() as $e) {
                $exception .= $e-&gt;getMessage() . &#34;\n&#34;;
            }
            echo $exception;
        }
        echo &#34;Body sending...\n&#34;;
    }
}
</code></pre></div><p>主要的概念很簡單，就是 Response 物件先把 Exception 先收集起來，然後再視狀況如何處理，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$response = new Response();
$response-&gt;renderExceptions(true); // 讓 Exception 呈現出來

try {
    // 這裡處理我們真正要執行的動作
    throw new Exception(&#39;TEST&#39;); // 丟出一個測試用的例外
} catch (Exception $e) {
    $response-&gt;setException($e); // 收集例外
}

if ($response-&gt;isException()) {
    // 可以在這裡記錄 Exception
}

$response-&gt;sendResponse(); // 顯示所有結果 (包含 Header, Exception, Body 等)
</code></pre></div><p>透過了 Response 物件來管理 Exception ，就可以不必因為 Exception 而中斷我們的程式碼。</p>
<h2 id="php-error-的處理">PHP Error 的處理</h2>
<p>雖然 Exception 可以用 <code>try...catch</code> 控制程式流程，但 PHP Error 卻不行。</p>
<p>因為一般處理 PHP Error 的方法是透過 <a href="http://www.php.net/manual/en/function.set-error-handler.php">set_error_handler</a>，而當執行完自訂的 Error Handler 後，我們卻只能選擇繼續執行下一行程式碼或將程式中斷離開，不然就是要利用全域變數來設定錯誤旗標以達到控制的目的。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$error = false;

function exceptionErrorHandler($errno, $errstr, $errfile, $errline)
{
    global $error;
    $error = true;
    echo $errstr, &#34;\n&#34;;
    return true;
}

set_error_handler(&#34;exceptionErrorHandler&#34;);
strpos();

if (!$error) {
    echo &#34;Do normal process here.\n&#34;;
}
echo &#34;end.\n&#34;;
</code></pre></div><p>不過 PHP5 也幫我們想好了，我們可以在 Error Handler 裡丟出 <a href="http://www.php.net/manual/en/class.errorexception.php">ErrorException</a> ，就可以配合前面提到的 Response 物件做到更平順的 Exception 處理：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function exceptionErrorHandler($errno, $errstr, $errfile, $errline )
{
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}

set_error_handler(&#34;exceptionErrorHandler&#34;);
$response = new Response();
$response-&gt;renderExceptions(true); // 讓 Exception 呈現出來

try {
    // 這裡處理我們真正要執行的動作
    trigger_error(&#39;TEST&#39;, E_USER_ERROR); // 改用 trigger_error 來丟出測試用錯誤
} catch (Exception $e) {
    $response-&gt;setException($e); // 收集例外
}

if ($response-&gt;isException()) {
    // 可以在這裡記錄 Exception
}
$response-&gt;sendResponse(); // 顯示所有結果 (包含 Header, Exception, Body 等)
</code></pre></div><p>這邊最棒的是 Error Handler 丟出 <code>ErrorException</code> 後， <code>try...catch</code> 就會發生作用，而不再像 <code>set_error_handler</code> 這樣又返回中斷的地方繼續執行，一切就像行雲流水般那麼自然。</p>
<h2 id="結論">結論</h2>
<p>一個運作良好的系統必須要對錯誤的發生有最大的掌控權，而不是放任它讓系統墜毀在五里霧之中。</p>
<p>雖然前面提到的處理方式也許不是最佳的，但希望透過這樣的介紹，讓大家能夠思考自己的程式應該如何去處理錯誤這件事。</p>
<p>就寫到這裡吧，收工~</p>
]]></content>
		</item>
		
		<item>
			<title>為什麼台灣的工程師 / 設計師常常加班？</title>
			<link>https://jaceju.net/why-engineer-or-designer-are-always-work-overtime-in-taiwan/</link>
			<pubDate>Thu, 01 Apr 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/why-engineer-or-designer-are-always-work-overtime-in-taiwan/</guid>
			<description>加班、加班，在台灣的工程師或設計師的生活裡，它已經是個習以為常的事情了。但是為什麼要加班呢？ 在本人不算長的職業生涯中，我找出了幾個常加班的原</description>
			<content type="html"><![CDATA[<p>加班、加班，在台灣的工程師或設計師的生活裡，它已經是個習以為常的事情了。但是為什麼要加班呢？</p>
<p>在本人不算長的職業生涯中，我找出了幾個常加班的原因&hellip;如有雷同，純屬故意&hellip;</p>
<!-- raw HTML omitted -->
<h2 id="前人種樹後人砍樹">前人種樹，後人&hellip;砍樹</h2>
<p>剛接下一個由前人留下的案子，你發現程式裡沒有註解，文件也只有企劃那邊寥寥幾張草圖；你只能再找企劃開一次會，瞭解這個案子到底是啥鬼。</p>
<p>開完會回到座位上後，你只能把前人寫的程式丟到了垃圾筒裡，自己另起爐灶。</p>
<h2 id="不可能的任務永遠是客戶心中的最愛">「不可能的任務」永遠是客戶心中的最愛</h2>
<p>客戶花了半天開會來畫他的餅，而當你心中盤算著這些功能大概要花兩天才能完成時，他會告訴你這些功能<!-- raw HTML omitted -->明天<!-- raw HTML omitted -->就要。</p>
<p>很不巧地，你的主管也常會幹這種事。</p>
<h2 id="成功男人的背後總是有個美麗的女人">成功男人的背後總是有個美麗的女人</h2>
<p>老闆總是在你忙得不可開支時，帶著他那五歲的小朋友來公司。</p>
<p>為了這個月的獎金，你不得不當起免費的保姆。當你問起小朋友為什麼今天放學沒有直接回家？他告訴你：</p>
<p>「媽媽這星期要去上舞蹈課。」</p>
<h2 id="有錢能使推磨的人變鬼">有錢能使推磨的人變鬼</h2>
<p>當你好不容易在期限前趕完案子後，打算請假好好休養個兩天。</p>
<p>這時你桌上的分機總是會適時的響起，然後你的業務就會告訴你：「不好意思，客戶臨時改變主意，說整個網站的風格要做調整。」</p>
<p>這是因為客戶的大股東昨天開完會，他們覺得韓國風的網站比較好看。</p>
<h2 id="一諾值千金只是今天是愚人節">一諾值千金，只是今天是愚人節</h2>
<p>你的主管告訴你，如果這次這個預估兩個月的案子可以在一個月之內搞定的話，上面會發給大家一筆很豐渥的獎金。</p>
<p>只是在你爆肝趕完案子之後，卻聽到因為客戶預算的關係而取消這個案子，所以也沒有獎金了。</p>
<p>而你的主管今天 (4/1) 答應你，在下次案子裡給你雙倍的獎金。</p>
<h2 id="管理只是個名詞不會是動詞更不會是進行式">管理只是個名詞，不會是動詞，更不會是進行式</h2>
<p>三天前，老闆在網路上看到一篇有關管理的文章，所以發信要大家每天下班前花一個小時記錄今日工作事項，並回報給主管。</p>
<p>昨天你經過老闆辦公室時，餘光瞄到你的工作報表被放在門口那個紙類回收箱裡。</p>
<p>而他今天已經徹底忘記這件事，目前人在日本渡假了。</p>
<h2 id="人人心中都有不願面對的真相">人人心中都有不願面對的真相</h2>
<p>你在電腦開機後，必須花一個小時在噗浪或 MSN 上，跟噗友或同事討論麥扣又被撞飛多遠，才有心情工作。</p>
<p>午餐時，你看到了電視上恰恰被橘爆，所以又在下午上班前開心地跟噗友或同事聊了起來，想撐過飯後那愛睏的一小時&hellip;</p>
<p>所以加班也是剛好而已。</p>
<h2 id="還有更多原因嗎">還有更多原因嗎？</h2>
<p>歡迎撥打螢幕下方的電話號碼報厚哇哉&hellip;</p>
]]></content>
		</item>
		
		<item>
			<title>PHPUnit 實務入門簡介</title>
			<link>https://jaceju.net/head-first-phpunit/</link>
			<pubDate>Tue, 16 Mar 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/head-first-phpunit/</guid>
			<description>註：本文所提及的觀念與技巧已經不適用在目前的 PHPUnit ，這裡只是為了記錄自己學習過的心得。 這幾天在寫折價券攤提到商品的數學演算法邏輯，搞得我七葷八素</description>
			<content type="html"><![CDATA[<p>註：本文所提及的觀念與技巧已經不適用在目前的 PHPUnit ，這裡只是為了記錄自己學習過的心得。</p>
<p>這幾天在寫折價券攤提到商品的數學演算法邏輯，搞得我七葷八素的&hellip;還好先前在製作購物車時，已經把單元測試放到架構裡，因此後面就只要專心應付演算法邏輯就好了。</p>
<p>雖然這樣的規劃聽起來不錯，但單元測試這件事說到底我的實務經驗還是太少，在這次的專案項目裡，才讓我真正有了較為深入的體會。</p>
<!-- raw HTML omitted -->
<h2 id="先對要測試的事情有一個概觀">先對要測試的事情有一個概觀</h2>
<p>其實測試一開始真的很難下手，主要是因為我們不知道我們要測些什麼東西。因此，我們需要對需要測試的東西有個概觀。</p>
<p>就以這次的例子來說吧，我要測試的東西就是「折價券攤提的演算法邏輯」，那它裡面重要的東西是什麼？</p>
<p>在跟客戶討論，我們得知折價券面額要分攤到的商品上時有一定的規則；這時我們就要先在紙上作業，用簡單的例子跟客戶確認清楚規則。</p>
<p><a href="/resources/phpunit/draft.jpg"><img src="/resources/phpunit/draft.jpg" alt="草圖"></a></p>
<p>確認了方向之後，因為我之前已經測試架構準備好了，所以接下來就只要針對要測試的部份撰寫程式碼即可。但如果一開始還沒有準備好測試架構的話，這裡給大家幾個建置環境的簡易流程：</p>
<ul>
<li>
<p>在專案裡開個 <code>tests</code> 資料夾，這裡就是放置測試案例的地方。</p>
</li>
<li>
<p>準備一個 <code>init.php</code> ，目的是用來設置 <code>include_path</code> 及 <code>autoload</code> 機制。</p>
</li>
<li>
<p>按照 PHPUnit 官方的建議，建立一個 AllTests.php 的 Test Suite 。</p>
</li>
</ul>
<p>註：這裡我就不列出程式碼了，讓大家自己試試看。</p>
<p>然後每次測試就用以下指令即可：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">&gt; phpunit AllTests.php
</code></pre></div><p>之後我會以「跑測試」來表示執行這個指令。</p>
<h2 id="描繪程式的輪廓">描繪程式的輪廓</h2>
<p>接下來我們就要把整個系統的測試架構定義出來，不過這時候其實我們還沒開始寫程式，只是把流程和相關的方法先定義出來。</p>
<p>這裡的方法很簡單，就是先透過註解和方法介面來描述整個流程，而不是先寫細部的程式碼。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Shop_Cart_Plugin_Coupon extends Shop_Cart_Plugin
{
    // ... 略 ...
    // 演算法計算後的結果
    protected $_sharedCouponData = array();
    // 取得演算法計算後的結果，也可供測試來驗證
    public functino getSharedCouponData()
    {
        return $this-&gt;_sharedCouponData;
    }
    // 主要的執行方法
    public function doCheckout()
    {
        $this-&gt;_getCouponData(); // 取得
        $this-&gt;_getProductData(); // 取得商品資料
        $this-&gt;_initData(); // 初始化要攤提的資料
        $this-&gt;_shareCouponToProduct(); // 開始攤提
    }
    // ... 略 ...
}

</code></pre></div><p>當然這些都是大概的輪廓，因為可能在我們寫好測試執行時，會再額外加入新的方法及介面。</p>
<p>還有一個要先定義好的是測試用的比對數據格式，它對我們稍後要測試的程式寫法會有影響。</p>
<h2 id="寫第一個測試">寫第一個測試</h2>
<p>到這裡，我們就可以開始寫第一個測試，而接下來的程式碼，都是先以這個測試可以成功為目的。而這個測試要怎麼寫呢？就是把一開始我們在紙上作業的數字拿進來套用。</p>
<p>當然這裡我的 <code>setUp</code> 和 <code>tearDown</code> 也已經在之前準備測試架構時寫好了，它們會讓我們每次的測試數據都能夠獨立。我們關心的就是第一個測試案例：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">class</span> <span class="nc">Shop_Cart_Plugin_CouponTest</span> <span class="k">extends</span> <span class="nx">PHPUnit_Framework_TestCase</span>
<span class="p">{</span>
    <span class="c1">// ... 略 ...
</span><span class="c1"></span>    <span class="k">public</span> <span class="k">function</span> <span class="nf">setUp</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// ... 略 ...
</span><span class="c1"></span>    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">tearDown</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// ... 略 ...
</span><span class="c1"></span>    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">testDoCheckout</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_plugin</span><span class="o">-&gt;</span><span class="na">setValue</span><span class="p">(</span><span class="k">array</span><span class="p">(</span>
           <span class="mi">1</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// C1, ProductCoupon for P1, $100
</span><span class="c1"></span>           <span class="mi">2</span> <span class="o">=&gt;</span> <span class="mi">2</span><span class="p">,</span> <span class="c1">// C2, ProductCoupon for P1, P2, $100
</span><span class="c1"></span>        <span class="p">));</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_cart</span><span class="o">-&gt;</span><span class="na">addItems</span><span class="p">(</span><span class="k">array</span><span class="p">(</span>
            <span class="s1">&#39;P1&#39;</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// $200
</span><span class="c1"></span>            <span class="s1">&#39;P2&#39;</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// $300
</span><span class="c1"></span>        <span class="p">))</span><span class="o">-&gt;</span><span class="na">refresh</span><span class="p">();</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertEquals</span><span class="p">(</span><span class="mi">300</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_cart</span><span class="o">-&gt;</span><span class="na">getTotal</span><span class="p">());</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_plugin</span><span class="o">-&gt;</span><span class="na">doCheckout</span><span class="p">();</span>

        <span class="nv">$resultDataList</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_plugin</span><span class="o">-&gt;</span><span class="na">getSelectedOrderCouponDataList</span><span class="p">();</span>

        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertEquals</span><span class="p">(</span><span class="o">-</span><span class="mi">100</span><span class="p">,</span> <span class="nv">$resultDataList</span><span class="p">[</span><span class="s1">&#39;P1-C1&#39;</span><span class="p">](</span><span class="s1">&#39;discountPrice&#39;</span><span class="p">));</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertEquals</span><span class="p">(</span><span class="o">-</span><span class="mi">25</span><span class="p">,</span>  <span class="nv">$resultDataList</span><span class="p">[</span><span class="s1">&#39;P1-C2&#39;</span><span class="p">](</span><span class="s1">&#39;discountPrice&#39;</span><span class="p">));</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertEquals</span><span class="p">(</span><span class="o">-</span><span class="mi">75</span><span class="p">,</span>  <span class="nv">$resultDataList</span><span class="p">[</span><span class="s1">&#39;P2-C2&#39;</span><span class="p">](</span><span class="s1">&#39;discountPrice&#39;</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>這裡因為我們在上一步就定義好比對用的數據，所以測試時就是用這個輸出的數據來與我們預期的數字相比較。</p>
<p>接下來就先跑跑測試，看看這個 TestCase 有沒有執行錯誤的地方 (例如物件沒有正確初始化或是變數名稱誤寫等等) ；當然如果沒有出現預期值是正常的，因為我們根本還沒有寫計算公式。</p>
<h2 id="繼續完成演算法">繼續完成演算法</h2>
<p>現在回到 <code>Shop_Cart_Plugin_Coupon</code> ，我們就要把剛剛那些只有骨頭的方法開始添血添肉，這裡就請大家自行發擇。</p>
<p>接著只要你覺得程式差不多了，就先跑一下測試，看看是不是符合測試的預期結果。</p>
<p>當你完成第一個測試時，程式的就差不多完成百分之五十啦，到這裡別忘了先把程式 commit 到版本控制系統裡。</p>
<h2 id="加入新測試並修改程式">加入新測試並修改程式</h2>
<p>完成第一個測試時，當然不是沒事了，我們要針對不同的狀況再加入其他的測試數據。</p>
<p>這裡我們就可以開始考慮把第一個測試以 PHPUnit 提供的 Data Provider 改寫，讓我們不必重複過多的程式碼。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">class</span> <span class="nc">Shop_Cart_Plugin_CouponTest</span> <span class="k">extends</span> <span class="nx">PHPUnit_Framework_TestCase</span>
<span class="p">{</span>
    <span class="c1">// ... 略 ...
</span><span class="c1"></span>    <span class="k">public</span> <span class="k">function</span> <span class="nf">setUp</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// ... 略 ...
</span><span class="c1"></span>    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">tearDown</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// ... 略 ...
</span><span class="c1"></span>    <span class="p">}</span>

    <span class="sd">/**
</span><span class="sd">     * @dataProvider provider
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">testDoCheckout</span><span class="p">(</span><span class="nv">$selectedCouponIdList</span><span class="p">,</span> <span class="nv">$productSkuNumberList</span><span class="p">,</span> <span class="nv">$total</span><span class="p">,</span> <span class="nv">$discountDataList</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_plugin</span><span class="o">-&gt;</span><span class="na">setValue</span><span class="p">(</span><span class="nv">$selectedCouponIdList</span><span class="p">);</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_cart</span><span class="o">-&gt;</span><span class="na">addItems</span><span class="p">(</span><span class="nv">$productSkuNumberList</span><span class="p">)</span><span class="o">-&gt;</span><span class="na">refresh</span><span class="p">();</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertEquals</span><span class="p">(</span><span class="nv">$total</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_cart</span><span class="o">-&gt;</span><span class="na">getTotal</span><span class="p">());</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_plugin</span><span class="o">-&gt;</span><span class="na">doCheckout</span><span class="p">();</span>
        <span class="nv">$resultDataList</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_plugin</span><span class="o">-&gt;</span><span class="na">getSharedCouponData</span><span class="p">();</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="nv">$discountDataList</span> <span class="k">as</span> <span class="nv">$key</span> <span class="o">=&gt;</span> <span class="nv">$value</span><span class="p">)</span> <span class="p">{</span>
        	<span class="nv">$this</span><span class="o">-&gt;</span><span class="na">assertEquals</span><span class="p">(</span><span class="nv">$value</span><span class="p">,</span> <span class="nv">$resultDataList</span><span class="p">[</span><span class="nv">$key</span><span class="p">](</span><span class="s1">&#39;discountPrice&#39;</span><span class="p">));</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">function</span> <span class="nf">provider</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">array</span><span class="p">(</span> <span class="c1">// 第一個測試
</span><span class="c1"></span>            <span class="k">array</span><span class="p">(</span><span class="k">array</span><span class="p">(</span>
                <span class="mi">1</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// C1, ProductCoupon for P1, $100
</span><span class="c1"></span>                <span class="mi">2</span> <span class="o">=&gt;</span> <span class="mi">2</span><span class="p">,</span> <span class="c1">// C2, ProductCoupon for P1, P2, $100
</span><span class="c1"></span>            <span class="p">),</span> <span class="k">array</span><span class="p">(</span>
                <span class="s1">&#39;P1&#39;</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// $200
</span><span class="c1"></span>                <span class="s1">&#39;P2&#39;</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="c1">// $300
</span><span class="c1"></span>            <span class="p">),</span> <span class="mi">300</span><span class="p">,</span> <span class="k">array</span><span class="p">(</span>
                <span class="s1">&#39;P1-C1&#39;</span> <span class="o">=&gt;</span> <span class="o">-</span><span class="mi">100</span><span class="p">,</span>
                <span class="s1">&#39;P1-C2&#39;</span> <span class="o">=&gt;</span> <span class="o">-</span><span class="mi">25</span><span class="p">,</span>
                <span class="s1">&#39;P2-C2&#39;</span> <span class="o">=&gt;</span> <span class="o">-</span><span class="mi">75</span><span class="p">,</span>
            <span class="p">)),</span>
            <span class="k">array</span><span class="p">(</span> <span class="c1">// 第二個測試
</span><span class="c1"></span>                <span class="c1">// ... 略 ...
</span><span class="c1"></span>            <span class="p">),</span>
            <span class="c1">// ... 略 ...
</span><span class="c1"></span>        <span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div><p>而加入新測試之後，就可以跑跑測試，看看我們剛寫好的演算法是否正確動作？通常這時候才真正是考驗的開始。因為這時候前面寫好的程式碼可能只對第一個測試正常，接下來的測試也許就會出錯了。</p>
<p>所以我們就會需要修改或重構程式碼，讓後面的測試也能正常執行。當然改過的程式也要讓第一個測試正常運作，才是正確的修改。</p>
<p>當然演算法寫好後，就要真正上到 Web 畫面去測試。至此，你會發現你花在寫測試上的心力都有了回報，因為通常如果你已經定義好介面，而這次的修改只是改寫一個小類別的話，那麼就會發現程式會非常順利地運作了。</p>
<h2 id="心得">心得</h2>
<p>每次寫購物車時，最麻煩的就是測試時要開啟購物車網頁，把一個一個的商品加進來，再加入不同的折價券條件。而有單元測試之後，我就可以省去一大堆開啟網頁，點選連結的功夫，專心地撰寫計算邏輯，只能說單元測試真的個超級便利的工具呀。</p>
]]></content>
		</item>
		
		<item>
			<title>[Web] 常見設計模式介紹</title>
			<link>https://jaceju.net/familiar-patterns/</link>
			<pubDate>Mon, 01 Feb 2010 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/familiar-patterns/</guid>
			<description>設計模式 (Design Patterns) 一直以來都是優秀的程式開發者所必須瞭解的觀念之一，但不論書裡或是網路上所找到的設計模式教學，很少為大家仔細介紹如何把設計模式套用</description>
			<content type="html"><![CDATA[<p>設計模式 (Design Patterns) 一直以來都是優秀的程式開發者所必須瞭解的觀念之一，但不論書裡或是網路上所找到的設計模式教學，很少為大家仔細介紹如何把設計模式套用在 Web 開發上。</p>
<p>因此，我最近試著把 Web 開發常用到的設計模式整理出來分享給大家，沒想到真的是挺累人的一件事。</p>
<p>不管如何，這個投影片介紹就算是個試金石吧，希望大家能夠從中獲得一些東西，並且也能給我一點意見。</p>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --><!-- raw HTML omitted --></p>
<p>註 1 ：看不到投影片的話，請重新整理。
註 2 ：沒有 Part 2 了。</p>
]]></content>
		</item>
		
		<item>
			<title>[五分鐘教室] 重構多參數函式</title>
			<link>https://jaceju.net/refactor-multiple-arguments-function/</link>
			<pubDate>Wed, 16 Dec 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/refactor-multiple-arguments-function/</guid>
			<description>我們在撰寫 PHP 函式 (或類別的方法) 時，多少都會帶入一些參數，例如： function myfunc($arg1, $arg2, ...) {} 一般常見的函式，它們的參數數量大多只會兩三個，但如果有參數的數量很</description>
			<content type="html"><![CDATA[<p>我們在撰寫 PHP 函式 (或類別的方法) 時，多少都會帶入一些參數，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function myfunc($arg1, $arg2, ...) {}
</code></pre></div><p>一般常見的函式，它們的參數數量大多只會兩三個，但如果有參數的數量很多時該怎麼辦？</p>
<!-- raw HTML omitted -->
<h2 id="多參數的困擾">多參數的困擾</h2>
<p>當一個 PHP 函式的參數多於三個以上時，其實就會浮現一些讓程式開發人員困擾的問題。</p>
<h3 id="順序不易記憶">順序不易記憶</h3>
<p>當函式名稱語意不明時，加上如果沒有 IDE 的協助，你會很難瞭解參數的先後順序。</p>
<p>像我自己就很常搞錯 <code>iconv</code> 的參數順序：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">iconv($in_charset, $out_charset, $str)
</code></pre></div><p>每次使用時，我都要回去看看官方手冊，才會記得第一個參數是輸入編碼而不是要轉換的字串。</p>
<h3 id="預設值">預設值</h3>
<p>還有一個狀況是參數如果有預設值時，一定是將它放在最右邊的參數。例如 <code>htmlspecialchars</code> 這個函式：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">htmlspecialchars($string[, $quote_style = ENT_COMPAT[, $charset = &#39;ISO-8859-1&#39;[, $double_encode = true]]])
</code></pre></div><p>如果今天我們只想要載入第一個參數 (要轉換的字串) 和第三個參數 (編碼) ，這是做不到的；我們不得不把第二個參數 (引號型態) 也一併代入，即便我們不想更改它的預設值。</p>
<h2 id="解決方式">解決方式</h2>
<p>我們可以用關聯式陣列來解決前述的問題，也就是把多個參數變成一個陣列。</p>
<p>例如以前面的 <code>iconv</code> 為例，我們自己定義了一個函式將它包裝起來，然後把原來的三個參數變成一個參數陣列：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// 定義
function my_iconv($options)
{
    return iconv($options[&#39;inputCharset&#39;], $options[&#39;outputCharset&#39;], $options[&#39;convertString&#39;]);
}

//用法
echo my_iconv(array(
    &#39;convertString&#39; =&gt; &#39;test&#39;,
    &#39;inputCharset&#39; =&gt; &#39;BIG5&#39;,
    &#39;outputCharset&#39; =&gt; &#39;UTF-8&#39;,
));
</code></pre></div><p>這樣一來，不僅我們不用擔心參數順序的問題，還可以很容易地分辨出每個參數值所代表的意義。</p>
<p>註：這種作法在一些 JavaScript Library 裡也常見到，像是 jQuery 很多方法都是以物件來當做參數組代入。</p>
<h3 id="考慮預設值">考慮預設值</h3>
<p>那麼如果參數需要預設值時該怎麼做呢？這時候我們可以在函式裡預先放置一個含有預設值的陣列，來跟使用者傳入的陣列做合併的動作。例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function my_htmlspecialchars($str, $options = array())
{
    $options = (array) $options;
    $options += array(
        &#39;quoteStyle&#39; =&gt; ENT_COMPAT,
        &#39;charset&#39; =&gt; &#39;ISO-8859-1&#39;,
        &#39;doubleEncode&#39; =&gt; true,
    );
    return htmlspecialchars(
        $str,
        $options[&#39;quoteStyle&#39;],
        $options[&#39;charset&#39;],
        $options[&#39;doubleEncode&#39;]
    );
}

echo my_htmlspecialchars(&#39;<span class="p">&lt;</span><span class="nt">test</span><span class="p">&gt;</span>測試<span class="p">&lt;/</span><span class="nt">test</span><span class="p">&gt;</span>&#39;, array(&#39;charset&#39; =&gt; &#39;UTF-8&#39;));
</code></pre></div><p>這裡我用了「 <code>+</code> 」這個可以用來合併陣列的操作符，它會以左邊陣列裡的值為主，然後將右邊的值合併進來。</p>
<p>這樣一來，右邊的陣列裡我們可以放置預設的參數值，而不必擔心使用者沒有帶入中間必要的參數。</p>
<p>註：你也許會發現我沒有把 <code>$str</code> 放到 <code>$options</code> 裡，這是因為它本身就是必要的參數。</p>
<h3 id="缺點">缺點</h3>
<p>使用關聯式陣列當做參數值也不能說完全沒有缺點，實際應用上我就發現了以下幾個問題：</p>
<ul>
<li>
<p>比原本的程式寫法囉嗦了些，你得多打好多字才能達到相同的目的。</p>
</li>
<li>
<p>參數名稱不易記憶，且預設值要寫在註解裡，而不是寫在定義裡；這樣一來有些 IDE 如果不顯示函式註解的話，還是得看手冊才能知道這些參數的名稱與預設值。</p>
</li>
</ul>
<h2 id="結論">結論</h2>
<p>用關聯式陣列來取代多參數這個方法，最大的好處就是當後續維護的開發人員，可以不必再回頭查看手冊，一眼就能知道每個參數值所代表的意義。</p>
<p>不過這個方法還是得看狀況使用，因為它所帶來的缺點可能會讓很多開發人員覺得很不便。所以在設計函式時，一定要仔細思考如何去定義才能讓開發者方便使用這些它們。</p>
]]></content>
		</item>
		
		<item>
			<title>[五分鐘教室] MySQL 4.1&#43; 編碼快速入門</title>
			<link>https://jaceju.net/mysql-character-set/</link>
			<pubDate>Sat, 12 Dec 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/mysql-character-set/</guid>
			<description>很久以前，我寫過一篇「MySQL 中文編碼徹底研究」，簡單介紹了如何將 MySQL 4.0 轉換到 MySQL 4.1 時要注意的部份。 註：大家可以把這篇「MySQL 中文編碼徹底</description>
			<content type="html"><![CDATA[<p>很久以前，我寫過一篇「<a href="http://www.jaceju.net/blog/archives/116">MySQL 中文編碼徹底研究</a>」，簡單介紹了如何將 MySQL 4.0 轉換到 MySQL 4.1 時要注意的部份。</p>
<p>註：大家可以把這篇「<a href="http://www.jaceju.net/blog/archives/116">MySQL 中文編碼徹底研究</a>」當做是本人在拙作「<a href="http://www.flag.com.tw/book/5105.asp?bokno=F5471">PHP Smarty 樣版引擎</a>」中的觀念修正文。</p>
<p>不過我想還是會有很多朋友會覺得該文寫的東西還是有點雜亂，尤其在 BIG5 和 UTF-8 之間的切換，可能讓很多人看完還是一頭霧水。</p>
<p>所以，就目前時下網路服務都是用 UTF-8 做為溝通間的編碼這點來看，這裡我建議大家乾脆就一律用 4.1 以上的版本吧！省得在切換這些編碼時搞得自己身心俱疲。</p>
<p>接下來我會快速為大家介紹 MySQL 4.1 以上的版本，在使用編碼上的一些觀念。</p>
<!-- raw HTML omitted -->
<h2 id="mysql-41-編碼">MySQL 4.1+ 編碼</h2>
<h3 id="編碼的影響範圍">編碼的影響範圍</h3>
<p>MySQL 4.1 以後的編碼影響層級可細分為三層：資料庫、資料表、欄位。</p>
<p><img src="/resources/mysql_charset/01.png" alt="編碼影響範圍"></p>
<p>一般來說，我們只要對資料表設定編碼即可，其下所擁有的資料表和資料欄位的編碼與資料庫設定的一致。但如果針對資料庫和資料欄位設定不同的編碼也是可以的，只要我們在創建它們時指定字集和校對即可。</p>
<h3 id="字集與校對">字集與校對</h3>
<p>MySQL 4.1 + 之後就引入二階式的編碼設定：字集 (character set) 及校對 (collation) 。</p>
<p>字集一般就是我們說的編碼，例如 UTF-8 或 BIG5 等；校對則是再針對字集裡分出二進制 (bin) 或多種語言並不分大小寫 (xxx_ci) 等特性。</p>
<p>所以一般選完字集後，我們就會需要再選擇校對。對一般欄位來說，校對並不需要區分大小寫 (xxx_ci) ，但是如果較為重要的欄位 (例如密碼) ，就要選擇二進制 (bin) 。</p>
<p>那麼哪個字集比較好呢？</p>
<h2 id="就是-utf-8">就是 UTF-8</h2>
<p>UTF-8 是目前網站開發編碼的不二人選，它的優點這裡我就不再多加闡述了。如果以前各位是慣用 BIG5 或是 Latin-1 等編碼的話，在新網站就改用 UTF-8 吧。</p>
<p>註：舊網站的編碼需不需要升級？這點就留給大家自行判斷吧。</p>
<p>很多朋友在從 MySQL 4.0 轉換成 MySQL 4.1 以上版本時，常會有 PHP 把從 MySQL 取得的資料輸出為問號，或是文字顯示為亂碼的困擾。其實要解決這樣的問題很簡單，以下幾個小地方注意即可。</p>
<p><img src="/resources/mysql_charset/02.png" alt="輸出至瀏覽器的編碼操作"></p>
<ul>
<li>PHP 在從 MySQL 存取資料之前，利用 <code>SET NAMES UTF8</code> 告訴 MySQL 接下來的資料都要轉換為 UTF8 。</li>
<li>PHP 輸出資料給 Browser 時，要在 Header 指定正確的編碼。</li>
<li>有時候瀏覽器需要 meta 標籤指定編碼，所以在 <code>&lt;title&gt;</code> 標籤之前把正確的 meta 標籤設定好。</li>
<li>當然別忘了你的 HTML Template 的編碼也是要用 UTF-8 。</li>
</ul>
<p>就是這麼簡單，別再為 MySQL 的編碼困擾了！更詳盡的介紹就請參考本人的拙作：「<a href="http://www.jaceju.net/blog/archives/116">MySQL 中文編碼徹底研究</a>」吧！</p>
]]></content>
		</item>
		
		<item>
			<title>[五分鐘教室] PHP 檔案引入路徑問題</title>
			<link>https://jaceju.net/php-include-path/</link>
			<pubDate>Sat, 12 Dec 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-include-path/</guid>
			<description>相信大家都知道， PHP 提供了幾個敘述句來協助我們引入外部檔案： include include_once require require_once 那麼它們是怎麼決定引入檔案的路徑呢？ 絕對路徑 絕對路徑就是指檔案在作業系統中</description>
			<content type="html"><![CDATA[<p>相信大家都知道， PHP 提供了幾個敘述句來協助我們引入外部檔案：</p>
<ul>
<li><a href="http://www.php.net/manual/en/function.include.php">include</a></li>
<li><a href="http://www.php.net/manual/en/function.include-once.php">include_once</a></li>
<li><a href="http://www.php.net/manual/en/function.require.php">require</a></li>
<li><a href="http://www.php.net/manual/en/function.require-once.php">require_once</a></li>
</ul>
<p>那麼它們是怎麼決定引入檔案的路徑呢？</p>
<!-- raw HTML omitted -->
<h2 id="絕對路徑">絕對路徑</h2>
<p>絕對路徑就是指檔案在作業系統中所存放的路徑，例如：</p>
<pre><code>/var/lib/php/library/Zend/Loader.php (在 Unix like 環境)
D:\php\library\PEAR.php (在 Windows 環境)
</code></pre><p>所以我們可以在 include 及 require 裡直接引入這樣的檔案：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">require_once &#39;/var/lib/php/library/Zend/Loader.php&#39;;
include &#39;D:\php\library\PEAR.php&#39;;
</code></pre></div><h2 id="相對路徑">相對路徑</h2>
<p>相對路徑看起來比較麻煩一點，這裡也常常是 PHP 開發者一開始容易搞混的地方。</p>
<p>不過只要掌握住幾個重點，那麼引入相對路徑的檔案其實也沒有想像中那麼困難。</p>
<h3 id="相對於目前檔案所在路徑">相對於目前檔案所在路徑</h3>
<p>在 PHP 預設的環境設定下，我們可以引用相對於目前這支程式的其他檔案，例如我們有一支程式 <code>D:\WEB\wwwroot\index.php</code> ，其內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">require &#39;library/Zend/Loader.php&#39;;
</code></pre></div><p>那麼 require 實際引入的檔案就會是 <code>D:\WEB\wwwroot\library\Zend\Loader.php</code> 。</p>
<p>不過為了確保程式不會因為環境的改變而無法正確引入檔案 (稍後會提到怎麼改變) ，我們還可以用以下方式來確保引入的檔案確實是 <code>D:\WEB\wwwroot\library\Zend\Loader.php</code> 這支程式：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">require dirname(__FILE__) . &#39;/library/Zend/Loader.php&#39;;
</code></pre></div><p>從上面的例子可以看出，我們利用 <code>dirname(__FILE__)</code> 來取得目前檔案的實際所在資料夾的完整路徑 (也就是 <code>D:\WEB\wwwroot</code> ) ，然後再引入相對於這個檔案的 <code>/library/Zend/Loader.php</code> 。</p>
<h3 id="相對於-include_path-所設定的路徑">相對於 include_path 所設定的路徑</h3>
<p>前面我們提到 include 和 require 預設可以引入相對於目前檔案路徑的程式，其實這是定義在 <code>php.ini</code> 裡的 <code>include_path</code> 這個設定值：</p>
<div class="highlight"><pre class="chroma"><code class="language-ini" data-lang="ini"><span class="c1">; UNIX: &#34;/path1:/path2&#34;</span>
<span class="c1">;include_path = &#34;.:/php/includes&#34;</span>
<span class="c1">;</span>
<span class="c1">; Windows: &#34;\path1;\path2&#34;</span>
<span class="na">include_path</span> <span class="o">=</span> <span class="s">&#34;.;c:\php\includes&#34;</span>
</code></pre></div><p>可以發現 php.ini 會把「 <code>.</code> 」 (也就是當前目錄) 做為預設的引入路徑。而在「 <code>.</code> 」這個路徑後面，我們也可以加入自訂的引入位置，像是 <code>&quot;c:\php\include&quot;</code> 等等。</p>
<p>註：目錄前的分隔符號，在 Unix 和 Windows 是不同的 (分別是「 <code>:</code> (冒號) 」及「 <code>;</code> (分號) 」) 。在 PHP 程式裡，我們可以用 <code>PATH_SEPARATOR</code> 這個預定義常數來表示。</p>
<p>因此如果在程式裡不指定前面的路徑位置時， PHP 程式就會依照 <code>include_path</code> 所設定的路徑一一去尋找符合的檔案 (有找到就不會再往下找了) 。</p>
<p>例如我們的 include_path 內容為：</p>
<div class="highlight"><pre class="chroma"><code class="language-ini" data-lang="ini"><span class="na">include_path</span> <span class="o">=</span> <span class="s">&#34;.;c:\php5\PEAR;c:\php5\library&#34;</span>
</code></pre></div><p>那麼如果我們在 <code>D:\WEB\wwwroot\index.php</code> 引入：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">require &#39;Zend/Loader.php&#39;;
</code></pre></div><p>那麼 PHP 就會依照以下順序尋找 <code>Zend/Loader.php</code> ：</p>
<ul>
<li><code>D:\WEB\wwwroot\Zend\Loader.php</code></li>
<li><code>c:\php5\PEAR\Zend\Loader.php</code></li>
<li><code>c:\php5\library\Zend\Loader.php</code></li>
</ul>
<h3 id="include_path-的順序很重要">include_path 的順序很重要</h3>
<p>從上面的例子可以看到 PHP 會針對 <code>include_path</code> 所設定的順序去尋找檔案，所以 <code>include_path</code> 設定的路徑會決定 PHP 找到要載入檔案的機會。</p>
<p>不過這樣一來也會浮現一個問題：如果要載入的檔案路徑一直都是在 <code>include_path</code> 的最後一項時，那麼會因為尋找時間過久，導致程式效率變差。</p>
<p>這種狀況通常發生在要載入很多類別檔案的時候，尤其是目前時下流行的 MVC 框架。</p>
<p>所以很多框架都會在程式一開始執行時，去調整 <code>include_path</code> 的引入路徑，把框架常用的類別庫路徑放在 <code>include_path</code> 的第一個；而為了不影響其他程式的執行，最後還是會把當前目錄 (也就是「 . 」) 放在 <code>include_path</code> 的最後一項。</p>
<h2 id="結論">結論</h2>
<p>include 及 require 引入路徑一直都是開發 PHP 時很重要的觀念，不過只要掌握上述的重點後，其實它們也不是這麼難以瞭解。</p>
<p>總之，兩個重點：要不就是用絕對路徑 (善用 <code>dirname</code> ) ，要不就是確定 <code>include_path</code> 所設定的路徑。你就一定能找到你要引入的檔案！</p>
<p>就是這麼簡單！</p>
]]></content>
		</item>
		
		<item>
			<title>談框架</title>
			<link>https://jaceju.net/about-framework/</link>
			<pubDate>Tue, 17 Nov 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/about-framework/</guid>
			<description>在 Web 開發界打滾了幾年，其實對 Web Framework 的熟悉度不算太淺，但也不能算太深。 基於我個人對目前業界使用 Web Framework 的觀察，大略區分了以下數種類型。 註：基本上這是</description>
			<content type="html"><![CDATA[<p>在 Web 開發界打滾了幾年，其實對 Web Framework 的熟悉度不算太淺，但也不能算太深。</p>
<p>基於我個人對目前業界使用 Web Framework 的觀察，大略區分了以下數種類型。</p>
<p>註：基本上這是一篇嘴炮文，部份論點是個人想法，若有謬誤還望指正。</p>
<!-- raw HTML omitted -->
<h2 id="不知框為何物">不知框為何物</h2>
<p>Framework 對有些工程師來說是很陌生的，但這不表示說他們一定是初學者，他們可能是已經在 Web 開發好幾年，只是因為 Web Framework 這種概念是這幾年才較為蓬勃發展。</p>
<p>不過通常這類工程師都有一種特性，那就是墨守成規；他們會自己有自己的一套開發模式，也不會特意去學習新的架構，簡單說&hellip;他們認為活得下去最重要。</p>
<h2 id="只見框形不得框髓">只見框形，不得框髓</h2>
<p>剛開始使用 Framework 的開發者，他們大多是驚奇於框架所帶來的便利性，因為以往要花很多功夫的才做得出來的功能，用了 Framework 兩三下就搞定。</p>
<p>但多數這樣的開發者反而常被 Framework 給綁住，只要 Framework 沒有提供的功能，他們就會開始不知所措，像是失去方向感的螞蟻一樣在各大論譠到處亂竄。</p>
<h2 id="框不為框非框也">框不為框，非框也</h2>
<p>有一些已經對 Framework 處於狂熱階段的開發者，他們認為 Framework 就是要提供快速建立專案的工具、要有強大的 ORM 套件，或是其他能讓他們瞬間擁有超高產能的機制。</p>
<p>所以只要有 Framework 推出時，沒有這些強大的傢私，那麼就會被他們打入 Library 之列，不能正名為 Framework 。</p>
<h2 id="速信達者框也">速、信、達者，框也</h2>
<p>有些開發團隊的領導者，常苦惱沒有一個好的方向能讓大家遵行， Framework 的出現讓他高興了好一陣子。</p>
<p>在研究了數個時下流行的 Framework 後，他發現 Framework 官方的那些特色展示不過就是噱頭而已；讓團隊能擁有快速、穩定、並完成客戶目標，才是一個好的 Framework 真正應該要做到的。</p>
<h2 id="手中有框心中無框">手中有框，心中無框</h2>
<p>某些開發者走到了高手的地步， Framework 在他們的手上，就是一把無形的劍，念到即劍到；即便是殘劍，也無損於他們制敵於機先。</p>
<p>他們對 Framework 已經瞭然於胸。他們不會因為 Framework 而使創意受限，即便有些功能在 Framework 中找不到，他們也能夠用自己所學來彌補 Framework 的不足。</p>
<h2 id="信手捻來都是框">信手捻來都是框</h2>
<p>創作出 Framework 的人，多數都是有著非常豐富經驗的開發者；為了能傳承這些經驗並再利用，所以他們透過程式碼把它包裝起來。</p>
<p>他們能把我們要花好幾個月甚至好幾年才能參透的知識，找出其共通的部份並封裝在框架裡，然後再給我們一個既定的方式，告訴我們怎麼去修改需要變動的地方。</p>
<p>他們懂得一切，也懂得怎麼讓我們用簡單的方式去使用這這一切。</p>
<h2 id="無框即有框善用一切而已">無框即有框，善用一切而已</h2>
<p>像 PHP 本身廣義上而言就是一個 Framework ，它黏合了所有我們所需要的工具，但絕大多數的開發者不會認為自己身處在 PHP 這個 Framework 裡。</p>
<p>但是我們還是能透過 PHP 讓 Web 多采多姿，讓我們的想像力無限延伸在無窮無盡地網路世界&hellip;</p>
<p>只要我們用對了工具。</p>
<h2 id="世上本無框本質為王">世上本無框，本質為王</h2>
<p>在像 Google 這種大公司裡的開發者， Framework 對他們來說反而是一種侷限；他們看到的世界遠比我們廣得多，他們處理的資料早已超過我們的想像。</p>
<p>他們創造的是 Framework 以外的世界，凡人呀！那便是廣大的雲端！</p>
<p>幸好，他們還是給了我們一架飛機。</p>
]]></content>
		</item>
		
		<item>
			<title>[PHP] 瞭解 static 關鍵字</title>
			<link>https://jaceju.net/php-static/</link>
			<pubDate>Wed, 19 Aug 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-static/</guid>
			<description>先前同事詢問有關 PHP static 關鍵字的用法，這裡我簡單整理一下。 static 主要用途在於定義一個變數空間，讓函式或類別可以保留住該變數的值，直到下次的存取。 以下</description>
			<content type="html"><![CDATA[<p>先前同事詢問有關 PHP static 關鍵字的用法，這裡我簡單整理一下。</p>
<p>static 主要用途在於定義一個變數空間，讓函式或類別可以保留住該變數的值，直到下次的存取。</p>
<p>以下就各別來探討 static 在函式與類別中的用法。</p>
<!-- raw HTML omitted -->
<h2 id="函式裡的-static-關鍵字">函式裡的 static 關鍵字</h2>
<p>先來看看以下的例子：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function getCount()
{
    static $count = 0;
    $count ++;
    return $count;
}

for ($i = 0; $i <span class="p">&lt;</span> <span class="nt">10</span><span class="err">;</span> <span class="err">$</span><span class="na">i</span> <span class="err">++)</span> <span class="err">{</span>
   <span class="na">echo</span> <span class="na">getCount</span><span class="err">(),</span> <span class="err">&#34;\</span><span class="na">n</span><span class="err">&#34;;</span>
<span class="err">}</span>
<span class="err">/*</span> <span class="na">output:</span>
<span class="na">1</span>
<span class="na">2</span>
<span class="na">3</span>
<span class="na">4</span>
<span class="na">5</span>
<span class="na">6</span>
<span class="na">7</span>
<span class="na">8</span>
<span class="na">9</span>
<span class="na">10</span>
<span class="err">*/</span>
</code></pre></div><p>首先我們在 <code>getCount()</code> 這個方法中定義了一個 static 變數 <code>$count</code> ，然後每次呼叫 <code>getCount()</code> 時，就會對 $count 作累加的動作。</p>
<p>接著我們透過迴圈執行十次 <code>getCount()</code> 方法，便可得到了 1 ~ 10 的輸出結果。</p>
<p>這是因為將變數宣告為 static 後，第一次呼叫 <code>getCount()</code> 這個方法時，會為 <code>$count</code> 保留一塊記憶體空間；而當脫離了 <code>getCount()</code> 的變數作用域後，這個記憶體空間並不會被消滅掉，而會在下一次呼叫 <code>getCount()</code> 方法時，再次被配置進來，並還原先前的變數值。</p>
<p>因此，除了第一次呼叫 getCount() 方法外，接下來的每次呼叫都會讓 <code>$count</code> 值累加，而得到 1 ~ 10 的輸出結果；如果把 <code>static</code> 關鍵字拿掉，就會輸出十次的 1 。</p>
<h2 id="應用在遞迴上的-static-關鍵字">應用在遞迴上的 static 關鍵字</h2>
<p>瞭解函式中的 static 關鍵字用法後，我們來看一個應用的例子：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function fibV1($n)
{
    if ($n <span class="err">&lt;</span>= 2) {
        return 1;
    } else {
        return fibV1($n - 2) + fibV1($n - 1);
    }
}
</code></pre></div><p>這是 PHP 版的 Fibonacci Sequence 遞迴函式，原理我就不多說明了。先來看看它在參數為 20 時所需要的執行時間：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$start_time = (float) microtime(true);
echo fibV1(20), &#34;\n&#34;;
$end_time = (float) microtime(true);
echo &#34;Spent Time: &#34;, ($end_time - $start_time), &#34;(s)\n&#34;;
/* output:
6765
Spent Time: 0.26100397109985(s)
*/
</code></pre></div><p>這裡代入的數字越大，執行時間會越長。</p>
<p>現在我們用 static 關鍵字來改寫：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function fibV2($n)
{
    static $result = array();
    if (!isset($result[$n])) {
        if ($n <span class="err">&lt;</span>= 2) {
            $result[$n] = 1;
        } else {
            $result[$n] = fibV2($n - 2) + fibV2($n - 1);
        }
    }
    return $result[$n];
}
</code></pre></div><p>然後再來看執行的時間：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$start_time = (float) microtime(true);
echo fibV2(20), &#34;\n&#34;;
$end_time = (float) microtime(true);
echo &#34;Spent Time: &#34;, ($end_time - $start_time), &#34;(s)\n&#34;;
/* output:
6765
Spent Time: 0.0009009838104248(s)
*/
</code></pre></div><p>速度竟然差了快三百倍！為什麼？</p>
<p>其實是因為這裡的 static 關鍵字扮演了 cache 的角色 () ，讓程式不用重新計算已經算好的結果。而使用了 static 關鍵字後，也使得執行時間不會再隨著代入數字變大而變長。</p>
<p>註：不是任何遞迴函式都可以用 static 變數來做 cache ，在使用上要特別注意這一點。</p>
<h2 id="類別裡的-static-關鍵字">類別裡的 static 關鍵字</h2>
<p>在類別裡的 <code>static</code> 關鍵字，也扮演了類似的角色。我們利用 <code>static</code> 在類別裡定義的屬性，會佔用類別的記憶體空間。而透過同一類別所生成的物件，會彼此共享這個 static 屬性；所以不論我們產生多少同類別的物件，它們都會存取到同一個 static 類別屬性。</p>
<p>來看看以下的例子：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class DB
{
    private static $_instance = null;

    private static $_instanceCount = 0;

    private function __construct()
    {
        self::$_instanceCount ++;
    }

    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    public function getInstanceCount()
    {
        return self::$_instanceCount;
    }
}
$db1 = DB::getInstance();
echo $db1-&gt;getInstanceCount(), &#34;\n&#34;; // 1
$db2 = DB::getInstance();
echo $db2-&gt;getInstanceCount(), &#34;\n&#34;; // 1
</code></pre></div><p>範例即為經典的 Singleton 模式，原理這裡先不多做說明。</p>
<p>這裡我們定義了兩個類別 static 屬性，分別是 <code>$_instance</code> 及 <code>$_instanceCount</code> ；而透過 <code>static</code> 定義的類別屬性，在類別裡存取時要用 <code>self</code> 關鍵字加上雙冒號 (<code>::</code>) ，例如 <code>self::$_instance</code> 。</p>
<p>接著我們可以看到在 <code>getInstance()</code> 方法中，如果 <code>self::$_instance</code> 是 null 的話，表示是第一次呼叫，那麼程式就會透過私有的建構式產生一個 DB 物件指定給 <code>self::$_instance</code> 變數，最後再將它回傳出去。這時雖然 getInstance() 裡的變數作用域已經結束，但 <code>self::$_instance</code> 卻會保留下來。</p>
<p>下一次 <code>getInstance()</code> 呼叫時，因為 <code>self::$_instance</code> 已經不再是 <code>null</code> 值，所以就會直接將其內容回傳給呼叫的程式了。</p>
<p>也因為這個原因，所以整個程式執行下來， DB 的 <code>__construct()</code> 方法也只被執行過一次，使得 <code>self::$_instanceCount</code> 也只累加一次，其結果永遠為 1 。</p>
<h2 id="結論">結論</h2>
<p>一般 PHP 開發者很少會去使用 <code>static</code> 關鍵字，因為平常會用到 <code>static</code> 的場合其實也不多。這裡我再做一次 <code>static</code> 使用時機的重點整理：</p>
<ul>
<li>需要記住上一次函式執行的結果。</li>
<li>某些可以保留執行結果的遞迴函式。</li>
<li>不希望因為物件個體不同，進而被影響的類別屬性。</li>
<li>類別的 Singleton 模式。</li>
</ul>
<p>希望透過上面的說明，能讓大家對 static 關鍵字有進一步的瞭解。</p>
]]></content>
		</item>
		
		<item>
			<title>[好書] 值得一讀的物件導向開發相關書籍</title>
			<link>https://jaceju.net/good-books-about-oo/</link>
			<pubDate>Tue, 30 Jun 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/good-books-about-oo/</guid>
			<description>再次介紹一些我個人認為很優質，可以幫助大家學習很多物件導向開發觀念的書籍。 深入淺出 Java 程式設計 要瞭解什麼是物件導向，建議從這本書開始啦~ 深入淺</description>
			<content type="html"><![CDATA[<p>再次介紹一些我個人認為很優質，可以幫助大家學習很多物件導向開發觀念的書籍。</p>
<!-- raw HTML omitted -->
<ul>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794605&amp;sid=28153">深入淺出 Java 程式設計</a>
要瞭解什麼是物件導向，建議從這本書開始啦~</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794524&amp;sid=32306">深入淺出設計模式</a>
設計模式是什麼？這本書看完後包你一清二楚。</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9572054023&amp;sid=8021">物件導向設計模式</a>
設計模式的經典名著，不過我建議先看看「深入淺出設計模式」後再回頭看這本。另外還有<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9572054112&amp;sid=10828">精裝本</a>，收藏者必備。</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9575278356&amp;sid=26836">Design Patterns 於 Java 語言上的實習應用</a>
用許多 Java 實例來介紹設計模式，可以補足前面兩本設計模式書籍的實作觀念。</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9789866761799&amp;sid=49583">大話設計模式</a>
對岸高手寫的一本好書，用了相當有趣的例子來說明設計模式。</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867910311&amp;sid=12245">極致軟體製程</a>
XP 大師著作，包含許多有趣的軟體開發方法，值得一讀！</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9861541489&amp;sid=26120">敏捷軟體開發：原則、樣式及實務</a>
想走物件導向軟體開發必讀書籍！</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=0321146530&amp;sid=15226">Test-Driven Development: By Example</a>
Kent Beck 大師寫的好書，用範例一步一步帶領你進入測試驅動開發 (TDD) 的世界。</li>
<li><a href="http://goo.gl/RxLA">重構 - 改善既有程式的設計</a>
介紹許多簡單的重構手法，可以幫助我們把程式重新修改成較易維護的架構，必讀！</li>
<li><a href="http://goo.gl/01kR">重構 - 向範式前進</a>
如果你對重構與設計模式已經有一定的認知，卻不知道怎麼把它們結合起來的話，這本必讀！</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794702&amp;sid=29190">軟體預先架構之美學</a>
和重構有異曲同工之妙，主要是利用經驗法則來避免掉後續過多的重構動作，值得參考。</li>
<li><a href="http://www.oreilly.com.tw/product2_java.php?id=a210">深入淺出物件導向分析與設計</a>
如果覺得市面上的 OO 分析書籍太過於艱深的話，這本入門書絕對值得一讀！</li>
<li><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=0321127420&amp;sid=14658">Patterns of Enterprise Application Architecture</a>
這本也是超推的書！ Martin Fowler 老大把許多企業常見的設計模式整理出來，也是當下許多 Framework 設計時參考的重要指標！</li>
</ul>
<p>也歡迎大家提供更多好書~</p>
]]></content>
		</item>
		
		<item>
			<title>[Web] Cookie 小觀念</title>
			<link>https://jaceju.net/about-cookie/</link>
			<pubDate>Thu, 16 Apr 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/about-cookie/</guid>
			<description>問題 剛剛被問了一個 Cookie 的觀念，這邊簡單分享給大家。 先來看以下這個程式，請問它第一次執行時結果是什麼？ setcookie(&amp;#39;test&amp;#39;, &amp;#39;abc&amp;#39;); var_dump($_COOKIE); 如果你回答的是空陣列的話，那就表示你</description>
			<content type="html"><![CDATA[<h2 id="問題">問題</h2>
<p>剛剛被問了一個 Cookie 的觀念，這邊簡單分享給大家。</p>
<p>先來看以下這個程式，請問它第一次執行時結果是什麼？</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">setcookie(&#39;test&#39;, &#39;abc&#39;);
var_dump($_COOKIE);
</code></pre></div><p>如果你回答的是空陣列的話，那就表示你瞭解 Cookie 的作用了。</p>
<!-- raw HTML omitted -->
<h2 id="說明">說明</h2>
<p>當我們在使用 <code>setcookie</code> 這個函式時，其實是在告訴瀏覽器： <!-- raw HTML omitted -->Server 要在它身上註冊一個 cookie 變數，這個變數會在下次瀏覽器連到同一個網站時，被送到 Server 上。<!-- raw HTML omitted --></p>
<p>所以第一次我們傾印 <code>$_COOKIE</code> 這個超全域陣列時是抓不到值的 (注意這個動作是在 Server 端) ，因為這時瀏覽器才剛認識 <code>setcookie</code> 丟出來的 test 變數。</p>
<p>當第二次瀏覽同一個網站時，瀏覽器就會把記在自己身上的 cookie 丟回 Server (就像 POST 一樣) ，這時 Server (PHP) 才會知道 cookie 的內容，將它塞到 <code>$_COOKIE</code> 陣列裡。</p>
<p>雖然這只是個小觀念，但希望能對大家在使用 Cookie 有進一步的瞭解。</p>
]]></content>
		</item>
		
		<item>
			<title>[心得感想] 學習技術的步驟</title>
			<link>https://jaceju.net/the-steps-to-learn-technique/</link>
			<pubDate>Wed, 15 Apr 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/the-steps-to-learn-technique/</guid>
			<description>在資訊產業上，持續不斷的進步一直以來都是不被淘汰的不二法則。不過要如何有效學習新的技術呢？其實方法非常多，而且每個人學習的方式也不一定都相同</description>
			<content type="html"><![CDATA[<p>在資訊產業上，持續不斷的進步一直以來都是不被淘汰的不二法則。不過要如何有效學習新的技術呢？其實方法非常多，而且每個人學習的方式也不一定都相同。</p>
<p>前些天在<a href="http://www.plurk.com/p/nim7c">噗浪提到我學習技術的幾個步驟</a>，有網友詢問我能不能進一步解說？所以就藉著這篇文章，來分享一下我的方法吧。</p>
<!-- raw HTML omitted -->
<h2 id="六大步驟">六大步驟</h2>
<h3 id="接觸">接觸</h3>
<p>很多技術都是工作有用到才會想去碰它，這是大多數人學習技術的動機，我其實也不例外。不過我個人也習慣在網路上東翻西找，去嗅探一些我有興趣的東西，這也是一種接觸技術的方式。</p>
<p>在接觸的這個步驟中，很重要的一點就是：要對這個技術產生興趣。如果沒有興趣，那麼在後面的步驟中，多數人都會很快地覺得這個技術很困難，然後就放棄學習它了。</p>
<h3 id="實作">實作</h3>
<p>在接觸技術後，我個人習慣會用它來實作一些簡單的小程式。這樣做的目的有很多，但主要有兩個：一來可以瞭解這個技術實際運作的方式，二來可以明白這個技術的優缺點在哪裡。</p>
<p>而且實作技術還能增加你對這個技術的熟稔度，如果哪一天專案真的需要用到它時，你就能很快知道它該怎麼用。</p>
<h3 id="瞭解">瞭解</h3>
<p>大部份時候，我們只要會「用」技術就可以了。不過身為一個 IT 人，身上帶著&hellip;不對&hellip;去弄清楚這個技術的原理是非常重要的。</p>
<p>就像我們在使用 jQuery 的 Plugin 一樣，多數的 Plugins 都會提供文件或範例，然後我們只要針對它的選項做修改即可。然而這些 Plugins 其實都是把一些技術原理包裝起來，方便我們使用。去瞭解這些 Plugins 背後的運作原理，可以幫助我們未來在不方便使用 Plugins 或是想要進一步改良它們時，能夠更清楚地知道自己該怎麼處理。</p>
<h3 id="應用">應用</h3>
<p>上面提到實作是指自己先寫一些小程式來熟悉這個技術，當然如果覺得這個技術能派上用場時，就別放棄這個機會啦。在應用技術之前，要記得仔細評估它對專案的適用程度，所以這就得依賴自己對這個技術的掌握有多少。</p>
<p>如果這個技術偏向於視覺化應用時，還要特別注意客戶是否很要求頁面上的呈現。如果這個客戶想要的畫面跟你所用的技術的呈現效果不合時，也千萬別氣餒；這時如果你瞭解這個技術的原理，那麼就可以舉一反三，將它套用在客戶所認可的頁面上。</p>
<h3 id="簡化">簡化</h3>
<p>因為我個人記憶力不是很好，所以當我對某項技術瞭解至一定程度時，我會習慣把它簡化。這樣不但可以讓我在回顧這個技術時，能很快掌握它的精髓；而且如果要分享給其他人時，更能讓別人瞭解這個技術的本質是什麼。</p>
<p>當然也不是什麼技術我都能簡化，因為這得看我對這個技術的瞭解程度有多深，以及我對這個技術能不能找到一個能類比的例子。所以這個步驟我也不一定會去做，我在乎的是我是不是真的能運用這個技術而已。</p>
<h3 id="分享">分享</h3>
<p>很多人學到技術以後，就習慣抱著不放，我個人則是喜歡把它們分享出來。分享技術有很大的好處，一則可以讓其他人也學到東西，二則我可以從別人在應用這個技術過程中，得到他們對於這個技術的反饋。然後在他們的回饋裡，我就可以明白我自己對這個技術是不是真的瞭解了，是不是真的能應用自如。</p>
<p>而分享的方法很多，像是撰寫部落格文章、在公司舉辦內部教育訓練等等。最重要的是不要吝於去分享，也不要害怕去分享。只有透過交流分享，你才能獲得更多東西。</p>
<h2 id="感想">感想</h2>
<p>上述的這些步驟都只是輔助我們學習技術而已，走在 IT 這條路上真正需要的是恆心與毅力；雖然聽起來很八股，但卻是不爭的事實。</p>
<p>我的學習步驟不一定適用於其他人，但我希望這樣的解說，可以讓各位在學習的道路上有參考的方向。只有不斷向前，你才能在這個環境中生存下去！</p>
]]></content>
		</item>
		
		<item>
			<title>[心得] 怎麼寫技術文件</title>
			<link>https://jaceju.net/how-to-write-document/</link>
			<pubDate>Mon, 09 Mar 2009 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/how-to-write-document/</guid>
			<description>先說明，這篇文章只是為了給伙伴看的，它不是一篇正規的文件寫作技巧；因為我也沒學過類似的東西，只是把自己的經驗與心得整理出來分享給大家。 擬定大</description>
			<content type="html"><![CDATA[<p>先說明，這篇文章只是為了給伙伴看的，它不是一篇正規的文件寫作技巧；因為我也沒學過類似的東西，只是把自己的經驗與心得整理出來分享給大家。</p>
<!-- raw HTML omitted -->
<h2 id="擬定大綱">擬定大綱</h2>
<p>在寫文件或投影片之前，先把大綱擬定出來是很重要的。寫一篇文章就好像開車上路，有起點也有終點；而且你一定要先確認哪裡是可以走的，把大方向先找出來。大綱的主要目的就是讓我們自己確認文件大致要描述的東西，不致讓我們整個走偏掉。</p>
<p>大綱要怎麼寫呢？首先先把大標題寫下來，確定整篇文件的方向。然後你可以繼續在每個大標題下再補上小標題，將整篇文章的輪廓描繪清楚。</p>
<h2 id="條列說明">條列說明</h2>
<p>擬定好大綱後，我們可以開始寫文章了。不過大多數人都不容易一下子寫出洋洋灑灑、圖文並茂的內容。這時我會建議大家採用條列式說明法，先將重點條列成骨架，再慢慢為它們添血加肉。</p>
<p>條列式說明主要可以簡短扼要先將重點記下來，寫文章的人也不會因為要想落落長的詞句而忘了其他部份的重點。列好重點後，你再用你精美的修辭與曼妙的詞句去補充它們，但切記不要讓文句流於枯燥與繁瑣。</p>
<h2 id="正確地引用與摘要">正確地引用與摘要</h2>
<p>技術文件很重要的一點就是正確性，所以有時候你可能會需要在你的文章裡補充某些技術性的觀念；這時你可以直接引用別人寫過的文章，把重點摘要在段落之間。最重要的是讓看的人不用點選連結就能簡單掌握住這個觀念，需要詳細瞭解時再點選連結即可。</p>
<h2 id="適時加上圖片或範例">適時加上圖片或範例</h2>
<p>有時候寫了一堆不知所云的說明還講不清楚時，你倒不如用張圖來解釋你的想法。不過有時圖片也不容易解釋清楚，這時你還可以直接用範例說明。</p>
<p>畫圖或範例主要的目的是讓看的人自己在腦海裡產生說明，但簡單的文字敘述還是必要的，免得畫者無心，看者有意。</p>
<h2 id="將它讀出來">將它讀出來</h2>
<p>很多人寫文章都是寫完就算了，自己根本沒有回頭再多看幾次；所以糟糕的排版，錯誤的標點符號，完全不通順的句子，常常會出現在這些人的文件裡面。</p>
<p>要解決這個問題，我建議你自己讀一次你自己的文章；最好能大聲唸出來，然後把它錄下來播給自己聽，沒睡著就算成功一半了。</p>
<h2 id="說服自己">說服自己</h2>
<p>沒有什麼項目是比說服自己更重要的了，如果你自己寫的東西都無法說服你自己，那又怎麼能期待別人看得懂呢？</p>
<p>不過說服自己並不是催眠自己對自己的文件有莫明奇妙的信心，最好能用完全忘掉內容的心情去重看一次；如果越讀越覺得自己對這份文件的內容掌握度很夠，那這樣一來你的寫作能力就能更往前邁向一大步。</p>
<h2 id="用部落格來練習">用部落格來練習</h2>
<p>很神奇的是，我在寫部落格會有一種我是寫給別人看的感覺 (其實不一定會有) ；所以我自己在寫部落格文章時，就會對自己寫的東西有一種莫明奇妙的責任感。當然不一定每個人都是這樣，但這的確不失為一種好的自我訓練方式。如果有人指正你寫錯了也沒關係，因為這就是我們練習的目的。</p>
<p>總之，寫技術文件其實並沒有想像中那麼困難 (但會覺得麻煩是很正常的) ，重要的是你有沒有重視它，多寫多練習吧~</p>
]]></content>
		</item>
		
		<item>
			<title>[jQuery] 自製 jQuery Plugin - Part 2</title>
			<link>https://jaceju.net/build-your-own-jquery-plugin-2/</link>
			<pubDate>Fri, 16 May 2008 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/build-your-own-jquery-plugin-2/</guid>
			<description>在 Part 1 我們看到如何建立一個 jQuery Plugin 的雛形，也讓它能夠做一些簡單的動作了。只是這個 Plugin 似乎沒什麼太大的用途，所以接下來我們就來寫個真正能用的東西。 簡</description>
			<content type="html"><![CDATA[<p>在 <a href="http://www.jaceju.net/blog/archives/336">Part 1</a> 我們看到如何建立一個 jQuery Plugin 的雛形，也讓它能夠做一些簡單的動作了。只是這個 Plugin 似乎沒什麼太大的用途，所以接下來我們就來寫個真正能用的東西。</p>
<!-- raw HTML omitted -->
<h2 id="簡單的頁籤功能">簡單的頁籤功能</h2>
<p>要舉個簡單又實用的教學用例子，一直都是非常困難的事情。經過多次的思考與挑選，我決定用頁籤功能 (Tab) 來當做本文練習的重點。</p>
<p>當然 jQuery 已經有一些 Tab 相關的 Plugin 了，而且基於不重造輪子的理念下，我們似乎不應該自己動手；不過站在學習的角度下，如果我們自己實作一個簡單的頁籤的話，將會有助於瞭解 jQuery Plugin 的設計過程。</p>
<p>以下先來瞭解我們需要的功能，再來想想看怎麼實作它。</p>
<p>首先我們的頁籤大概會長成這樣子：</p>
<p><img src="/resources/jquery_plugin_tutorial/mytab01.jpg" alt="頁籤基本形式"></p>
<p><img src="/resources/jquery_plugin_tutorial/mytab02.jpg" alt="頁籤基本形式"></p>
<p><img src="/resources/jquery_plugin_tutorial/mytab03.jpg" alt="頁籤基本形式"></p>
<p>基本上我們要的效果就是在點選上面的 TAB1 、 TAB2 及 TAB3 時，底下的文字區塊會跟著切換，這就是最簡單的頁籤功能了。</p>
<h2 id="頁面結構">頁面結構</h2>
<p>在 HTML 的部份也非常簡單，基本上是一個 UL 清單 (div.tabs ul) 加上三個 DIV 區塊 (div.tabBlock) ，最後再用一個大 DIV 區塊 (div#mytab) 把它們包起來。</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="cp">&lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD XHTML 1.0 Transitional//EN&#34; &#34;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#34;&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">xmlns</span><span class="o">=</span><span class="s">&#34;http://www.w3.org/1999/xhtml&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">&#34;Content-Type&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;text/html; charset=utf-8&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>jQuery TEST<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;mytab.css&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/css&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;mytab&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabs&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;active&#34;</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab1&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB1<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab2&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB2<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab3&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB3<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;tab1&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabBlock&#34;</span><span class="p">&gt;</span>
有時候寫 jQuery 時，常會發現一些簡單的效果可以重複利用。只是每次用 Copy <span class="ni">&amp;amp;</span>amp; Paste 大法似乎不是件好事，有沒有什麼方法可以讓我們把這些效果用到其他地方呢？
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;tab2&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabBlock&#34;</span><span class="p">&gt;</span>
沒錯，就是用 jQuery 的 Plugin 機制。不過 jQuery 的 Plugin 機制好像很難懂？其實一點也不。以下我用最簡單的方式來為大家解說如何自製一個簡單的 Plugin 。
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;tab3&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabBlock&#34;</span><span class="p">&gt;</span>
當然在此之前，你得先瞭解 JavaScript 的 class 、 object 、 variables scope 還有 anonymous function 等基礎...
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;jquery/1.2.3.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;jquery/mytab.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span><span class="p">&gt;</span>
<span class="nx">$</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;#mytab&#39;</span><span class="p">).</span><span class="nx">mytab</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>

</code></pre></div><p>當然別忘了把 JavaScript 檔案引入，這裡我先把 mytoolbox.js 改為 mytab.js ，因為我們要做的是頁籤。</p>
<p>而頁籤樣式的部份則以是 CSS 來完成，請把它存檔並命名為 mytab.css ：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="nt">div</span><span class="p">.</span><span class="nc">tabBlock</span> <span class="p">{</span>
<span class="k">clear</span><span class="p">:</span><span class="kc">both</span><span class="p">;</span>
<span class="k">margin-top</span><span class="p">:</span><span class="mi">-1</span><span class="kt">px</span><span class="p">;</span>
<span class="k">border</span><span class="p">:</span><span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#CCC</span><span class="p">;</span>
<span class="k">padding</span><span class="p">:</span><span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">div</span><span class="p">.</span><span class="nc">tabs</span> <span class="p">{</span>
<span class="k">margin-bottom</span><span class="p">:</span><span class="mi">-1</span><span class="kt">px</span><span class="p">;</span>
<span class="k">overflow</span><span class="p">:</span><span class="kc">hidden</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">div</span><span class="p">.</span><span class="nc">tabs</span> <span class="nt">ul</span> <span class="p">{</span>
<span class="k">margin</span><span class="p">:</span><span class="mi">0</span><span class="p">;</span>
<span class="k">padding</span><span class="p">:</span><span class="mi">0</span><span class="p">;</span>
<span class="k">list-style</span><span class="p">:</span><span class="kc">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">div</span><span class="p">.</span><span class="nc">tabs</span> <span class="nt">ul</span> <span class="nt">li</span> <span class="p">{</span>
<span class="k">float</span><span class="p">:</span><span class="kc">left</span><span class="p">;</span>
<span class="k">height</span><span class="p">:</span><span class="mi">25</span><span class="kt">px</span><span class="p">;</span>
<span class="k">margin</span><span class="p">:</span><span class="mi">0</span> <span class="mi">3</span><span class="kt">px</span><span class="p">;</span>
<span class="k">line-height</span><span class="p">:</span><span class="mi">25</span><span class="kt">px</span><span class="p">;</span>
<span class="k">font-size</span><span class="p">:</span><span class="mi">9</span><span class="kt">pt</span><span class="p">;</span>
<span class="k">border</span><span class="p">:</span><span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#CCC</span><span class="p">;</span>
<span class="k">overflow</span><span class="p">:</span><span class="kc">hidden</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">div</span><span class="p">.</span><span class="nc">tabs</span> <span class="nt">ul</span> <span class="nt">li</span> <span class="nt">a</span> <span class="p">{</span>
<span class="k">display</span><span class="p">:</span><span class="kc">block</span><span class="p">;</span>
<span class="k">padding</span><span class="p">:</span><span class="mi">3</span><span class="kt">px</span><span class="p">;</span>
<span class="k">color</span><span class="p">:</span><span class="mh">#000</span><span class="p">;</span>
<span class="k">text-decoration</span><span class="p">:</span><span class="kc">none</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">div</span><span class="p">.</span><span class="nc">tabs</span> <span class="nt">ul</span> <span class="nt">li</span><span class="p">.</span><span class="nc">active</span> <span class="nt">a</span><span class="o">,</span>
<span class="nt">div</span><span class="p">.</span><span class="nc">tabs</span> <span class="nt">ul</span> <span class="nt">li</span> <span class="nt">a</span><span class="p">:</span><span class="nd">hover</span> <span class="p">{</span>
<span class="k">color</span><span class="p">:</span><span class="mh">#FFF</span><span class="p">;</span>
<span class="k">background</span><span class="p">:</span><span class="mh">#999</span><span class="p">;</span>
<span class="p">}</span>

</code></pre></div><p>這裡我們就不深究 CSS 怎麼做的了，有興趣的朋友請自行參考相關書籍。</p>
<h2 id="準備-plugin-樣版">準備 Plugin 樣版</h2>
<p>現在把 Part 1 的 mytoolbox.js 複製為 mytab.js ，然後稍做修改，如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">;(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytab</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">settings</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">_defaultSettings</span> <span class="o">=</span> <span class="p">{};</span>
        <span class="kd">var</span> <span class="nx">_settings</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">extend</span><span class="p">(</span><span class="nx">_defaultSettings</span><span class="p">,</span> <span class="nx">settings</span><span class="p">);</span>
        <span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
            <span class="c1">// 從這裡開始
</span><span class="c1"></span>        <span class="p">};</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="nx">_handler</span><span class="p">);</span>
    <span class="p">};</span>
<span class="p">})(</span><span class="nx">jQuery</span><span class="p">);</span>

</code></pre></div><p>從上面的程式中可以看到我把 _defaultSettings 的內容清掉了，因為我們暫時用不到特別的設定。然後我把給 each 方法用的 callback 獨立為 _handler 函式，因為後面我們要做的動作大部份都會在這裡發生。</p>
<h2 id="呈現頁籤下的區塊">呈現頁籤下的區塊</h2>
<p>在套上 CSS 後，我們會看到三個 div.tabBlock 區塊同時都顯示出來了，這樣做的目的其實是為了當瀏覽器沒有開 JavaScript 時還能呈現資訊。而在開啟了 JavaScript 後，我希望只呈現第一個 div.tabBlock 區塊，這時我們就可以利用 jQuery 來完成：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="k">this</span><span class="p">).</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
<span class="p">};</span>

</code></pre></div><p>在解釋原理之前，先回頭看一下 HTML 頁面呼叫  Plugin 的地方，你會發現我把 Plugin 套用在 div#mytab 這個元素上，所以在 _handler 裡的 <code>this</code> 其實是指向 div#mytab 。</p>
<p>瞭解 <code>this</code> 代表的意義後，接著回到 _handler 中，我們就可以知道第一行的 $(&lsquo;div.tabBlock&rsquo;, this) 其實就是指抓取 div#mytab 底下的三個 div.tabBlock 元素。所以第一行我們先把所有的 div.tabBlock 隱藏起來，然後利用 .eq(0).show() 把第一個 div.tabBlock 顯示出來。</p>
<h2 id="讓頁籤動起來">讓頁籤動起來</h2>
<p>接著我們先讓頁籤在點選時能夠切換它的反白狀態，而反白的效果我們已經定義 CSS 裡了，也就是讓 LI 的 class 變成 active 即可：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="k">this</span><span class="p">).</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li a&#39;</span><span class="p">,</span> <span class="k">this</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li&#39;</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>這邊的原理也很簡單，首先先讓所有 div.tabs li 都移除反白效果，然後再對我們按的連結的上一層 LI 元素套上 active 。注意這裡也有個 <code>this</code> ，它代表的是目前點選的頁籤連結。</p>
<p>最後我們要 <code>return false</code> ，以阻止 click 事件被往上傳遞。</p>
<p>現在點選看看頁籤連結，是不是能反白了呢？</p>
<p>註：滑過頁籤會呈現反白這個效果也是定義在 CSS 裡，試著找找看吧。</p>
<h2 id="記住這個">記住「這個」</h2>
<p>在繼續完成效果前，有個問題我得先說明一下。雖然我們在測試頁面上只佈置了一個 Tab 元件，不過很難保證頁面上不會有其他地方也會用到頁籤效果，這時我們的 Plugin 可能會產生副作用。</p>
<p>問題出在 <code>$('div.tabs li').removeClass('active')</code> 這行，因為我們不能確定頁面其他地方是不是也有有元素符合 <code>$('div.tabs li')</code> 。所以我在這裡應該要明確指定這些 LI 元素應該屬於那個元素，也就是 <code>$('div.tabs li a', this)</code> 的 <code>this</code> (即 <code>div#mytab</code> ) 。</p>
<p>不過在 click 方法指定的匿名函式中， <code>this</code> 又指到 a 元素，而不是我們要的 <code>div#mytab</code> ；那麼有什麼方法能在 click 中找到 <code>div#mytab</code> 呢？總不能把 <code>div#mytab</code> 寫死在 Selector 中吧？其實方法很簡單，就是在 click 外先定義一個新變數指向外部的 <code>this</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span> <span class="c1">// 加入這行，並將以下表示 div#mytab 的 `this` 改為 container
</span><span class="c1"></span>    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li a&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span> <span class="c1">// 這個 `this` 不用動，它表示 a 元素
</span><span class="c1"></span>        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>因為在 click 的匿名函式會繼承外部的作用域，使得 <code>container</code> 的 scope 得以存在於 click 的 callback 裡；因此我們就能放心的使用 <code>container</code> 來表示 <code>div#mytab</code> 了。</p>
<p>註：在 click 裡的 callback 又有另一個名稱： closure ，因為它用到了外部 function 所定義的變數。</p>
<h2 id="切換對應的區塊">切換對應的區塊</h2>
<p>接著繼續完成我們要的功能，也就是切換頁籤對應的區塊。</p>
<p>這裡我用了連結錨點的技巧，這個技巧本身有個優點：就是當 JavaScript 被禁用時，錨點還能正常動作。而錨點的名稱剛好就是頁籤所對應的內容區塊 ID ，這就方便我們找到要顯示的內容區塊。</p>
<p>程式碼如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li a&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">hide</span><span class="p">();</span> <span class="c1">// 先全部藏起來
</span><span class="c1"></span>        <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">))[</span><span class="mi">1</span><span class="p">];</span> <span class="c1">// 只抓對應的 tabBlock id
</span><span class="c1"></span>        <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span> <span class="c1">// 顯示對應的 tabBlock
</span><span class="c1"></span>        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>原理一樣很簡單，當按下頁籤連結時，先隱藏所有內容區塊 (<code>div.tabBlock</code>) ，然後取得連結的 href 位址中的錨點名稱，以顯示頁籤所對應的內容區塊。不過這裡要注意一點，那就是瀏覽器通常會幫我們在錨點名稱前加入目前完整的網址；因此我這裡便使用正規式來取得帶井字號的錨點名稱，也剛好直接讓 jQuery 使用。</p>
<h2 id="減少多餘的查詢">減少多餘的查詢</h2>
<p>再看一次程式，我發現有個地方重複查詢了兩次，那就是 <code>$('div.tabBlock', container)</code> ；第一次是在我們只顯示第一個內容區塊時，而第二次則在點選連結時要隱藏所有內容區塊時。這裡我們可以利用暫存變數來解決：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">$tabBlocks</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span> <span class="c1">// 加入這行
</span><span class="c1"></span>    <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span> <span class="c1">// 改用 $tabBlocks
</span><span class="c1"></span>    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li a&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="s1">&#39;active&#39;</span><span class="p">);</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">();</span> <span class="c1">// 改用 $tabBlocks
</span><span class="c1"></span>        <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">))[</span><span class="mi">1</span><span class="p">];</span>
        <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>不過也不是每個重複的查詢都要用暫存變數，因為假設當你在 Plugin 運作的過程中，對 <code>div.tabBlock</code> 有進行增減的話，那麼重複再查一次就是必要的動作了，這樣才能確保我們抓到正確數量的元素集。</p>
<p>到這裡我們的 mytab.js 初版算是完成了，試試看它是不是依照我們的要求正確動作呢？</p>
<h2 id="加強-plugin">加強 Plugin</h2>
<p>雖然我們的 Plugin 已經可以動作了，但其實還是有些地方可以加強；而加入這些功能其實就是為了讓 Plugin 能更有彈性，以應付各種不同的狀況。</p>
<p>這裡我簡單介紹兩個加強的功能：</p>
<h3 id="由外部決定-class">由外部決定 class</h3>
<p>我們希望可以指定 active 的名稱，讓外部可以自行決定。我們在 <code>_defaultSettings</code> 中多定義了一個 <code>activeClass</code> 項目，然後把程式裡的所有 <code>'active'</code> 改為 <code>_settings.activeClass</code> 。程式碼修改如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytab</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">settings</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">_defaultSettings</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">activeClass</span><span class="o">:</span> <span class="s1">&#39;active&#39;</span>
    <span class="p">};</span>
    <span class="kd">var</span> <span class="nx">_settings</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">extend</span><span class="p">(</span><span class="nx">_defaultSettings</span><span class="p">,</span> <span class="nx">settings</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
        <span class="kd">var</span> <span class="nx">$tabBlocks</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
        <span class="nx">$tabLinks</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="nx">$tabLists</span><span class="p">.</span><span class="nx">removeClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
            <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
            <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">();</span>
            <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">))[</span><span class="mi">1</span><span class="p">];</span>
            <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
            <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
        <span class="p">});</span>
    <span class="p">};</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="nx">_handler</span><span class="p">);</span>
<span class="p">};</span>

</code></pre></div><p>同樣的原理，我們可以更改 <code>div.tabs</code> 和 <code>div.tabBlock</code> 的 class 名稱，這邊我留給各位自行試試。</p>
<h3 id="自動切換內容區塊">自動切換內容區塊</h3>
<p>現在我希望進入頁面時，能讓內容區塊自動跳到我指定的頁籤。這裡我有兩種方式可以指定：由網址決定以及用 Server 程式決定。</p>
<p>以網址決定就是我們在瀏覽器的網址列的網址後面再加上錨點名稱，例如：</p>
<pre><code>http://localhost/mytab.htm#tab2
</code></pre><p>這個網址可以由外部的網頁以連結的方式指定，這樣的話進入我們這個頁籤畫面時，我們就可以依照這個錨點來決定要顯示的內容區塊。程式很簡單：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">$tabBlocks</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="c1">// 加入以下這段
</span><span class="c1"></span>    <span class="kd">var</span> <span class="nx">matches</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="kc">null</span> <span class="o">!==</span> <span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>   <span class="c1">// 有找到網址錨點的話就切換內容
</span><span class="c1"></span>        <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="nx">matches</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">();</span>    <span class="c1">// 先把全部的內容區塊藏起來
</span><span class="c1"></span>        <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>         <span class="c1">// 顯示錨點對應的內容區塊
</span><span class="c1"></span>        <span class="c1">// 將對應的頁籤連結反白
</span><span class="c1"></span>        <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">removeClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li a&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">).</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">!==</span> <span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> <span class="p">{</span>
                <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">});</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="c1">// ... 略 ...
</span><span class="c1"></span><span class="p">};</span>

</code></pre></div><p>原理就是透過錨點來顯示對應的內容區塊，再跟著把對應頁籤連結反白即可；如果沒有指定錨點的話，就顯示第一個內容區塊。</p>
<p>當然別忘了重構，把多餘的查詢動作用暫存變數取代：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">$tabBlocks</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">$tabLists</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">$tabLinks</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li a&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">matches</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="kc">null</span> <span class="o">!==</span> <span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="nx">matches</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">();</span>
        <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
        <span class="nx">$tabLists</span><span class="p">.</span><span class="nx">removeClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
        <span class="nx">$tabLinks</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">!==</span> <span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> <span class="p">{</span>
                <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">});</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="nx">$tabLinks</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">$tabLists</span><span class="p">.</span><span class="nx">removeClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">();</span>
        <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">))[</span><span class="mi">1</span><span class="p">];</span>
        <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>回到主題，我們的另外一種指定內容區塊的方式就是在 Server 端輸出 HTML 時，就先決定好 active 的 LI 元素了。假設現在頁籤部份的 HTML 如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabs&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab1&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB1<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;active&#34;</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab2&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB2<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab3&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB3<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

</code></pre></div><p>現在 TAB2 這個 LI 元素的 class 是 active ，那麼我們應該怎麼自動切換到對應的內容區塊呢？很簡單，就讓 <code>li.active</code> 底下的 a 「點下去」就可以了。程式如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="c1">// ... 略 ...
</span><span class="c1"></span>    <span class="nx">$tabLinks</span><span class="p">.</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">$tabLists</span><span class="p">.</span><span class="nx">removeClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">();</span>
        <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">))[</span><span class="mi">1</span><span class="p">];</span>
        <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="c1">// 加入這段
</span><span class="c1"></span>    <span class="kd">var</span> <span class="nx">$activeLink</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li.&#39;</span> <span class="o">+</span> <span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span> <span class="o">+</span> <span class="s1">&#39; &gt; a&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">!==</span> <span class="nx">$activeLink</span><span class="p">.</span><span class="nx">size</span><span class="p">())</span> <span class="p">{</span>
        <span class="nx">$activeLink</span><span class="p">.</span><span class="nx">trigger</span><span class="p">(</span><span class="s1">&#39;click&#39;</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">};</span>

</code></pre></div><p>原理就是在我們指定好頁籤連結的 click 事件後，再觸發 <code>li.active</code> 底下的連結的 click 事件即可，很簡單吧。</p>
<h2 id="測試再測試">測試再測試</h2>
<p>以上我們都只在單一個頁籤元件上測試而已 (也就是 <code>div#mytab</code> ) ，但一般來說大部份的 Plugin 應該要能在頁面中被套用到多個元件上；換句話說就是頁面上會好幾個有頁籤的區塊，所以這裡我們得測試這個可能發生的狀況。</p>
<p>我在原來的 <code>div#mytab</code> 底下再加入一個 <code>div#mytab2</code> ，內容和 <code>div#mytab</code> 差不多，只是把 <code>#tab1</code>, <code>#tab2</code>, <code>#tab3</code> 改成 <code>#tab4</code>, <code>#tab5</code>, <code>#tab6</code> 而已：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;mytab2&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabs&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab4&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB4<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;active&#34;</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab5&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB5<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;#tab6&#34;</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>TAB6<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">a</span><span class="p">&gt;&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;tab4&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabBlock&#34;</span><span class="p">&gt;</span>
aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa aaa
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;tab5&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabBlock&#34;</span><span class="p">&gt;</span>
bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb bbb
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;tab6&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;tabBlock&#34;</span><span class="p">&gt;</span>
ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

</code></pre></div><p>然後我也為 <code>div#mytab</code> 和 <code>div#mytab2</code> 加入一組 <code>class=&quot;mytab&quot;</code> ：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;mytab&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;mytab&#34;</span><span class="p">&gt;</span>
... 略 ...
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;mytab2&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;mytab&#34;</span><span class="p">&gt;</span>

</code></pre></div><p>現在我們把 HTML 頁面上套用 Plugin 的部份稍作修改，也就是把 id 換成 class ：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.mytab&#39;</span><span class="p">).</span><span class="nx">mytab</span><span class="p">();</span>
<span class="p">});</span>

</code></pre></div><p>然後我們重新瀏覽一下，一切看起來很正常。直到我測試了用網址錨點來自動切換內容區塊時&hellip;出現了下圖的奇怪現象：</p>
<p><img src="/resources/jquery_plugin_tutorial/mytab04.jpg" alt="頁籤基本形式"></p>
<p>原因出在我們發現錨點時，就會先隱藏所有內容區塊，然後才顯示錨點對應的內容區塊；但是這樣就會使得沒有錨點對應的內容區塊通通不顯示，出現了上圖的狀況。</p>
<p>怎麼辦？其實也很簡單，就是將該頁籤元件所擁有的頁籤連結對應的錨點先記下來，然後找找看網址描點有沒有在這裡面，有的話才做上面的動作，不然就一定要顯示第一個內容區塊：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">_handler</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">container</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">$tabBlocks</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabBlock&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">$tabLists</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">$tabLinks</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;div.tabs li a&#39;</span><span class="p">,</span> <span class="nx">container</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">tabIdList</span> <span class="o">=</span> <span class="p">[];</span>
    <span class="c1">// 先記住所有頁籤連結對應的錨點
</span><span class="c1"></span>    <span class="nx">$tabLinks</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">matches</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">));</span>
        <span class="k">if</span> <span class="p">(</span><span class="kc">null</span> <span class="o">!==</span> <span class="nx">matches</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">tabIdList</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">matches</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
        <span class="p">}</span>
    <span class="p">});</span>
    <span class="kd">var</span> <span class="nx">matches</span> <span class="o">=</span> <span class="p">(</span><span class="nb">String</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">match</span><span class="p">(</span><span class="sr">/(#.+)$/</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="kc">null</span> <span class="o">!==</span> <span class="nx">matches</span> <span class="c1">// 錨點在列表裡的話就顯示
</span><span class="c1"></span>    <span class="o">&amp;</span><span class="nx">amp</span><span class="p">;</span><span class="o">&amp;</span><span class="nx">amp</span><span class="p">;</span> <span class="o">-</span><span class="mi">1</span> <span class="o">!==</span> <span class="nx">$</span><span class="p">.</span><span class="nx">inArray</span><span class="p">(</span><span class="nx">matches</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nx">tabIdList</span><span class="p">))</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="nx">matches</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">();</span>
        <span class="nx">$</span><span class="p">(</span><span class="nx">id</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
        <span class="nx">$tabLists</span><span class="p">.</span><span class="nx">removeClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
        <span class="nx">$tabLinks</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span> <span class="o">!==</span> <span class="nb">String</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">href</span><span class="p">).</span><span class="nx">indexOf</span><span class="p">(</span><span class="nx">id</span><span class="p">))</span> <span class="p">{</span>
                <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">parent</span><span class="p">(</span><span class="s1">&#39;li&#39;</span><span class="p">).</span><span class="nx">toggleClass</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">activeClass</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">});</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">$tabBlocks</span><span class="p">.</span><span class="nx">hide</span><span class="p">().</span><span class="nx">eq</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">show</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="c1">// ... 略 ...
</span><span class="c1"></span><span class="p">};</span>

</code></pre></div><p>這裡我用了最簡單 JavaScritp 的陣列，還有 jQuery 的 <code>$.inArray</code> 方法來解決這個問題；想想看，有沒有更方便的解法呢？</p>
<p>之後當然我們還得再多試試幾個不同的狀況，看看還有沒有需要解決的部份，這裡就留給大家試試看囉。</p>
<h2 id="還有什麼">還有什麼</h2>
<p>在看完以上的介紹後，我想自己寫一個 jQuery 的 Plugin 其實並不困難，困難的是我們要怎麼去完成裡面的內容。所以像是對 JavaScript 的作用域的認知、各家瀏覽器的差異、 DHTML 的基本功，還有如何去呈現效果的想像力等，這些都是在開發 jQuery Plugin 非常重要的。</p>
<p>另外就是一定要對程式碼做重構與測試的動作，因為它們會影響這個 Plugin 的效能與穩定性。這裡可以多參考其他 Plugin 作者的程式碼，觀察他們是如何處理效能問題；然後最好能建立一個測試頁面，把有可能遇到的使用方式儘可能地包含進來，以便測試 Plugin 的正確性。</p>
<p>希望透過這兩篇簡單的教學，能讓大家能快速進入 jQuery Plugin 的世界；也希望大家如果開發了好的 Plugin 後，能不吝分享出來。</p>
<p>感謝大家~~謝謝收看。</p>
]]></content>
		</item>
		
		<item>
			<title>[jQuery] 自製 jQuery Plugin - Part 1</title>
			<link>https://jaceju.net/build-your-own-jquery-plugin-1/</link>
			<pubDate>Tue, 13 May 2008 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/build-your-own-jquery-plugin-1/</guid>
			<description>有時候寫 jQuery 時，常會發現一些簡單的效果可以重複利用。只是每次用 Copy &amp;amp; Paste 大法似乎不是件好事，有沒有什麼方法可以讓我們把這些效果用到其他地方呢？ 沒錯</description>
			<content type="html"><![CDATA[<p>有時候寫 jQuery 時，常會發現一些簡單的效果可以重複利用。只是每次用 Copy &amp; Paste 大法似乎不是件好事，有沒有什麼方法可以讓我們把這些效果用到其他地方呢？</p>
<p>沒錯，就是用 jQuery 的 Plugin 機制。</p>
<p>不過 jQuery 的 Plugin 機制好像很難懂？其實一點也不。以下我用最簡單的方式來為大家解說如何自製一個簡單的 Plugin 。</p>
<p>當然在此之前，你得先瞭解 JavaScript 的 class 、 object 、 variables scope 還有 anonymous function  等基礎，這些可以參考「<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9789866840036&amp;sid=37518"> JavaScript 大全</a>」一書。</p>
<!-- raw HTML omitted -->
<h2 id="plugin-樣版">Plugin 樣版</h2>
<p>寫 jQuery 的 Plugin 最快的方法就是拿現成的 Plugin 來改，只是在那麼多的 Plugin 中怎麼找到好的範例呢？別擔心，這邊我提供一個最簡單的範例樣版：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">jQuery</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytoolbox</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>首先， <code>mytoolbox</code> 就是我們的 plugin 名稱，利用 <code>jQuery.fn</code> 我們可以將它註冊為 jQuery 的 plugin 。然後我們把  <code>jQuery.fn.mytoolbox</code> 指向一個匿名函式 (anonymous function) ，又稱為 callback ；而這個 callback 的內容很簡單，就是利用 jQuery 的 <code>each</code> 方法，來一一執行對應的動作。</p>
<p>特別要注意匿名函式裡的 <code>this</code> 關鍵字，它會指向一個 jQuery 物件；而這個 jQuery 物件則是我們要指定的，稍後我會再進一步說明。</p>
<h2 id="使用-plugin">使用 Plugin</h2>
<p>現在將上面的樣版存成 mytoolbox.js ，和 jquery.js 放在一起。然後建立一個 HTML 測試檔案，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="cp">&lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD XHTML 1.0 Transitional//EN&#34; &#34;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#34;&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">xmlns</span><span class="o">=</span><span class="s">&#34;http://www.w3.org/1999/xhtml&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">&#34;Content-Type&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;text/html; charset=utf-8&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>jQuery TEST<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">style</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/css&#34;</span><span class="p">&gt;</span>
<span class="p">.</span><span class="nc">test</span> <span class="p">{</span>
<span class="k">border</span><span class="p">:</span><span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#CCC</span><span class="p">;</span>
<span class="k">cursor</span><span class="p">:</span><span class="kc">pointer</span><span class="p">;</span>
<span class="k">padding</span><span class="p">:</span><span class="mi">3</span><span class="kt">px</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;test1&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;test&#34;</span><span class="p">&gt;</span>
點我！
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;test2&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;test&#34;</span><span class="p">&gt;</span>
點我！
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;debug&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;jquery/1.2.3.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;jquery/mytoolbox.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text/javascript&#34;</span><span class="p">&gt;</span>
<span class="nx">$</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.test&#39;</span><span class="p">).</span><span class="nx">mytoolbox</span><span class="p">();</span>
<span class="p">});</span>
<span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>

</code></pre></div><p>首先 HTML 中引用了 jQuery 函式庫及我們寫的 Plugin 檔案，然後我在畫面上佈置了兩個 class 為 <code>test</code> 的 div 元素。接著我們用以下程式碼來呼叫我們的 Plugin ：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.test&#39;</span><span class="p">).</span><span class="nx">mytoolbox</span><span class="p">();</span>
<span class="p">});</span>

</code></pre></div><p>這邊的用意就是將上面那兩個 div 套上 mytoolbox 這個 Plugin ，這樣 Plugin 就能動了，很簡單吧？</p>
<h2 id="加入動作">加入動作</h2>
<p>當然，這個 Plugin 什麼事都還沒開始做，是個空骨架而已。現在我們要為它加血添肉，讓它動起來。</p>
<p>先簡單在 <code>each</code> 的 callback 裡加入一行：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">jQuery</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytoolbox</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span> <span class="c1">// 加入此行
</span><span class="c1"></span>    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>再重新瀏覽測試用的 HTML 檔，你會發現頁面自動跳出了兩次訊息視窗，內容分別是 test1 和 test2 ；這證明了我們的 Plugin 的確有套用在 class 為 <code>test</code> 的兩個 div 上面。</p>
<p>不過現在有兩個 <code>this</code> ，它們是一樣的東西嗎？不，因為 scope 及觸發對象的不同，它們兩個是不同的東西。在外面的 <code>this</code> 是一個 jQuery 物件，指向我們指定的 <code>$('.test')</code> 這個物件；而 <code>each</code> callback 裡的 <code>this</code> 則是 div 元素，因為 <code>each</code> 是個 iterator function ，因此 <code>alert(this.id)</code> 會執行兩次。在第一次的 <code>this</code> 會指向 <code>#test1</code> 這個 div ，第二次則指向 <code>#test2</code> 這個 div 。</p>
<p>註：這裡我用 <code>#test1</code> 表示 id 為 <code>test1</code> 的元素。</p>
<p>現在我希望改成按下 div 元素後才會 alert 該元素的 id ，這要怎麼做呢？我們要改用 click 事件，做法如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">jQuery</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytoolbox</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="nx">jQuery</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
        <span class="p">});</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>由於 <code>each</code> callback 裡的 <code>this</code> 是 DOM 元素，所以我們要用 jQuery() 把 <code>this</code> 包起來，這樣才能方便指定該元素的 click 事件。現在重新瀏覽頁面，點選任何一個 div ，應該就會跳出對應的訊息視窗了。</p>
<h2 id="再包一層">再包一層</h2>
<p>如果在 <code>each</code> 的 callback 裡會呼叫到多次的 jQuery 的話，一直寫 <code>jQuery</code> 這幾個字實在是很累人的一件事；而且 <code>jQuery</code> 不是可以簡寫成 <code>$</code> 號嗎？不能直接用嗎？當然可以，只是這樣可能會和其他 JavaScript Library 發生衝突；所以我們要改用以下的方式來包覆我們的 Plugin ：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">;(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytoolbox</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
            <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
                <span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
            <span class="p">});</span>
        <span class="p">});</span>
    <span class="p">};</span>
<span class="p">})(</span><span class="nx">jQuery</span><span class="p">);</span>

</code></pre></div><p>JavaScript 可以直接用一組小括號 <code>()</code> 包覆一個匿名函式，然後後面再接一組小括號 <code>()</code>  表示呼叫這個匿名函式；而第二組小括號中就可以放置這個匿名函式的參數。所以在上面的程式碼中，我們把 Plugin 的程式碼用一個匿名函式包覆起來，然後參數就用我們常用的 <code>$</code> 符號；接著在利用前述的原理，將 <code>jQuery</code> 這個類別導入給我們的 Plugin ，這樣我們就可以很快樂地在 Plugin 中使用我們熟悉的 <code>$</code> 符號了。至於最前面的分號 <code>;</code> ，主要是考慮這個 Plugin 檔案會和其他 JS 檔合併壓縮而放進來的。</p>
<p>註： <code>$</code> 在 JavaScript 裡是合法的變數名稱。</p>
<p>後面的說明我會略過這個包覆動作，在實際檔案中請別忘了加。</p>
<h2 id="加入選項設定">加入選項設定</h2>
<p>接下來我希望讓 <code>each</code> 的 callback 函式能讓使用者自訂，因此我需要一個讓使用者能設定的選項。就像其他的 Plugin 一樣，我們讓我們的 <code>mytoolbox</code> 可以接受一個 <code>settings</code> 物件：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytoolbox</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">settings</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">_defaultSettings</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">callback</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">};</span>
    <span class="kd">var</span> <span class="nx">_settings</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">extend</span><span class="p">(</span><span class="nx">_defaultSettings</span><span class="p">,</span> <span class="nx">settings</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">callback</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>首先我們為 Plugin 加入 <code>settings</code> 參數，也就是一般 Plugin 常見的設定值。然後則是 <code>_defaultSettings</code> ，它能幫我們在使用者沒有指定任何設定值給 <code>settings</code> 時，還能夠提供預設的設定值。</p>
<p>接著我用 jQuery 提供的 <code>extend</code> 方法，將 <code>settings</code> 中有設定的值覆蓋掉 <code>_defaultSettings</code> 所設定的預設值，再把結果存放在 <code>_settings</code> 這個變數中；後面我們就會用新的 <code>_settings</code> 變數當做我們的設定值。</p>
<p>現在我們在 <code>_settings</code> 中指定了一個 callback 項目 (預設是用 <code>alert</code> ) ，然後將它指定給 div 元素的 <code>click</code> 觸發器。現在我要在 HTML 頁面中更改這個事件處理器，使它不再使用 <code>alert</code> ，而是把結果顯示在 <code>div#debug</code> 裡。程式如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">debug</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;#debug&#39;</span><span class="p">);</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.test&#39;</span><span class="p">).</span><span class="nx">mytoolbox</span><span class="p">({</span>
        <span class="nx">callback</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="nx">debug</span><span class="p">.</span><span class="nx">html</span><span class="p">(</span><span class="nx">debug</span><span class="p">.</span><span class="nx">html</span><span class="p">()</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">id</span> <span class="o">+</span> <span class="s1">&#39;&lt;br /&gt;&#39;</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">});</span>

</code></pre></div><p>再重新瀏覽一次頁面，看看效果是不是依照我們想像的完成呢？</p>
<h2 id="修改觸發事件">修改觸發事件</h2>
<p>假設現在我們不想用 <code>click</code> ，而是想讓滑鼠移過就觸發 callback 呢？這時就要借重 jQuery 的 <code>bind</code> 方法了：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">mytoolbox</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">settings</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">_defaultSettings</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">bind</span><span class="o">:</span> <span class="s1">&#39;click&#39;</span><span class="p">,</span>
        <span class="nx">callback</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">};</span>
    <span class="kd">var</span> <span class="nx">_settings</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">extend</span><span class="p">(</span><span class="nx">_defaultSettings</span><span class="p">,</span> <span class="nx">settings</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">bind</span><span class="p">(</span><span class="nx">_settings</span><span class="p">.</span><span class="nx">bind</span><span class="p">,</span> <span class="nx">_settings</span><span class="p">.</span><span class="nx">callback</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">};</span>

</code></pre></div><p>這裡我加入一個 <code>bind</code> 設定項目，預設是用 click 事件觸發。回到 HTML 頁面，我們改用 <code>mouseover</code> 來觸發 callback ：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">debug</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;#debug&#39;</span><span class="p">);</span>
    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.test&#39;</span><span class="p">).</span><span class="nx">mytoolbox</span><span class="p">({</span>
        <span class="nx">bind</span><span class="o">:</span> <span class="s1">&#39;mouseover&#39;</span><span class="p">,</span>
        <span class="nx">callback</span><span class="o">:</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
            <span class="nx">debug</span><span class="p">.</span><span class="nx">html</span><span class="p">(</span><span class="nx">debug</span><span class="p">.</span><span class="nx">html</span><span class="p">()</span> <span class="o">+</span> <span class="k">this</span><span class="p">.</span><span class="nx">id</span> <span class="o">+</span> <span class="s1">&#39;&lt;br /&gt;&#39;</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">});</span>

</code></pre></div><p>重新瀏覽 HTML 頁面，當滑鼠移過 div 元素時，是不是會出現對應的 id 呢？</p>
<p>到這裡，相信大家都應該大致瞭解如何建立一個 jQuery Plugin 了吧？接下來，我將透過實際的例子為大家介紹更多自製 jQuery Plugin 所需要注意的地方。</p>
<p>請觀賞 <a href="/2008/05/16/337/">Part 2</a> 。</p>
<h2 id="參考網址">參考網址</h2>
<ul>
<li><a href="http://www.learningjquery.com/2007/10/a-plugin-development-pattern">A Plugin Development Pattern</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>[PHP] 在 PHP5 中實作 AOP 的概念</title>
			<link>https://jaceju.net/php-aop/</link>
			<pubDate>Mon, 14 Apr 2008 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-aop/</guid>
			<description>這篇積在我電腦裡很久了，一直沒公開&amp;hellip;這次趁著要幫我的 Library 加料，順便拿出來分享一下心得。 什麼是 AOP AOP 全名為 Aspect-Oriented Programming ，基本的觀念可以參考良</description>
			<content type="html"><![CDATA[<p>這篇積在我電腦裡很久了，一直沒公開&hellip;這次趁著要幫我的 Library 加料，順便拿出來分享一下心得。</p>
<!-- raw HTML omitted -->
<h2 id="什麼是-aop">什麼是 AOP</h2>
<p>AOP 全名為 Aspect-Oriented Programming ，基本的觀念可以參考良葛格的 AOP 入門：</p>
<ul>
<li><a href="http://caterpillar.onlyfun.net/Gossip/SpringGossip/FromProxyToAOP.html">從代理機制初探 AOP</a></li>
<li><a href="http://caterpillar.onlyfun.net/Gossip/SpringGossip/DynamicProxy.html">動態代理</a></li>
<li><a href="http://caterpillar.onlyfun.net/Gossip/SpringGossip/AOPConcept.html">AOP 觀念與術語</a></li>
</ul>
<p>這裡我簡單提一下 AOP 的基本想法：</p>
<p>假設當我們呼叫物件的某些方法 (或是<strong>業務流程</strong>) 之後，會想要把相關的資訊記錄到 log 檔裡，我們也許會這樣寫：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="sd">/**
</span><span class="sd"> * Test
</span><span class="sd"> */</span>
<span class="k">class</span> <span class="nc">Test</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * 某個方法
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">doSomething</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="c1">// 建立 Log 物件
</span><span class="c1"></span>        <span class="nv">$logger</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Log</span><span class="p">();</span>
        <span class="c1">// 寫入前置 Log
</span><span class="c1"></span>        <span class="nv">$logger</span><span class="o">-&gt;</span><span class="na">save</span><span class="p">(</span><span class="s1">&#39;before do something.&#39;</span><span class="p">);</span>
        <span class="c1">// 主要的動作
</span><span class="c1"></span>        <span class="c1">// ...
</span><span class="c1"></span>        <span class="c1">// 寫入 Log
</span><span class="c1"></span>        <span class="nv">$logger</span><span class="o">-&gt;</span><span class="na">save</span><span class="p">(</span><span class="s1">&#39;before do something.&#39;</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div><p>可是如果今天這個記錄 log 的這個動作只是臨時的，或是在未來可能會需要再加入不同的動作時 (例如寄信) ，難道我們還要在原有方法的程式碼裡修修改改嗎？有沒有什麼方式能協助我們動態地把記錄的動作插在原有動作之後呢？</p>
<p>AOP 就是從這個角度所延伸出來的一種觀念，它能協助我們在不侵入原有類別程式碼的狀況下，動態地為類別方法新增額外的權責；簡單來說， AOP 主要的目的就是<!-- raw HTML omitted -->切入類別原有方法執行之前或之後，並安插我們想要執行的動作<!-- raw HTML omitted -->。</p>
<p>註： IT 界似乎很喜歡發明深奧的名詞來詮釋一個簡單的概念，然後像我這樣不學無術的開發者就常被唬得一楞一楞的。</p>
<h2 id="aop-和-decorator">AOP 和 Decorator</h2>
<p>先介紹幾篇實作 AOP 的文章：</p>
<ul>
<li><a href="http://racklin.blogspot.com/2007/08/aop-for-jquery.html">AOP for jQuery</a></li>
<li><a href="http://blog.jonnay.net/archives/637-Aspect-Oriented-Programming-in-PHP-as-a-contrast-to-other-languages..html">Aspect Oriented Programming in PHP as a contrast to other languages.</a></li>
<li><a href="http://wiki.jonnay.net/bunny/bunnyaspectsphp">Bunny Aspects</a></li>
<li><a href="http://jaxn.org/article/2004/10/16/more-on-aspect-oriented-php/">More on Aspect Oriented PHP</a></li>
<li><a href="http://hi.baidu.com/thinkinginlamp/blog/item/864a0ef46d93b86eddc474f3.html">在PHP里利用魔术方法实现准AOP</a></li>
<li><a href="http://sushener.spaces.live.com/blog/cns!BB54050A5CFAFCDD!546.entry">AOP在PHP中的实现方式</a></li>
<li><a href="http://www.phpclasses.org/browse/package/2633.html">Class: AOP Library for PHP</a></li>
</ul>
<p>其實一開始我以為 AOP 和 Decorator 模式在 PHP 上的實作方式是差不多的，不過實際上還有是些許的差別。</p>
<p>一般在 Decorator 模式中，具體類別和 Wrapper 類別都會有個共同的祖先，亦即一個抽象類別或介面，因此所產生出來的物件對 Client 程式來說，其抽象型態可以說是一樣的。</p>
<p>但是在 AOP in PHP 中，我們必須透過一個代理類別來切入原有的類別方法裡，雖然這個代理類別也能夠提供原有類別中的所有方法，但是實際上它卻已經失去了與原有類別所擁有的抽象型態了。</p>
<h2 id="用-php-實作-aop">用 PHP 實作 AOP</h2>
<p>首先我們來看看還沒有切入任何事件的目標類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="sd">/**
</span><span class="sd"> * Test class
</span><span class="sd"> *
</span><span class="sd"> */</span>
<span class="k">class</span> <span class="nc">TestClass</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * Method 1
</span><span class="sd">     *
</span><span class="sd">     * @param string $message
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">method1</span><span class="p">(</span><span class="nv">$message</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">echo</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="no">__METHOD__</span><span class="p">,</span> <span class="s2">&#34;:</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="nv">$message</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Method 2
</span><span class="sd">     *
</span><span class="sd">     * @return int
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">method2</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">echo</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="no">__METHOD__</span><span class="p">,</span> <span class="s2">&#34;:</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
        <span class="k">return</span> <span class="nx">rand</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Method 3
</span><span class="sd">     *
</span><span class="sd">     * @throws Exception
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">method3</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">echo</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">,</span> <span class="no">__METHOD__</span><span class="p">,</span> <span class="s2">&#34;:</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nx">Exception</span><span class="p">(</span><span class="s1">&#39;Test Exception.&#39;</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div><p>這個類別提供了三個方法，其中 method1 和 method2 只是簡單的顯示資料而已，而 method3 則會丟出一個異常。</p>
<p>另外我們需要一個 Log 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="sd">/**
</span><span class="sd"> * Log
</span><span class="sd"> *
</span><span class="sd"> */</span>
<span class="k">class</span> <span class="nc">Log</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * log message
</span><span class="sd">     *
</span><span class="sd">     * @param string $message
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">save</span><span class="p">(</span><span class="nv">$message</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">echo</span> <span class="nv">$message</span><span class="p">,</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div><p>這個 Log 類別只提供一個 save() 方法，以顯示 log 訊息。</p>
<p>現在我們要完成的目標如下：</p>
<ul>
<li>
<p>在 method1 執行前呼叫 Log::save() 。</p>
</li>
<li>
<p>在 method2 執行後呼叫 Log::save() 。</p>
</li>
<li>
<p>在 method3 發生異常時呼叫 Log::save() 。</p>
</li>
</ul>
<p>這裡我用很簡單的方式來做，那就是直接使用一個 Aspect 類別：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="sd">/**
</span><span class="sd"> * Aspect
</span><span class="sd"> *
</span><span class="sd"> */</span>
<span class="k">class</span> <span class="nc">Aspect</span>
<span class="p">{</span>
    <span class="sd">/**
</span><span class="sd">     * Name of target class
</span><span class="sd">     *
</span><span class="sd">     * @var string
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="nv">$_className</span> <span class="o">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="sd">/**
</span><span class="sd">     * Target object
</span><span class="sd">     *
</span><span class="sd">     * @var object
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="nv">$_target</span> <span class="o">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="sd">/**
</span><span class="sd">     * Event callback
</span><span class="sd">     *
</span><span class="sd">     * @var array
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="nv">$_eventCallbacks</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span>
    <span class="sd">/**
</span><span class="sd">     * Add object
</span><span class="sd">     *
</span><span class="sd">     * @param object $target
</span><span class="sd">     * @return Aspect
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">function</span> <span class="nf">addObject</span><span class="p">(</span><span class="nv">$target</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nx">Aspect</span><span class="p">(</span><span class="nv">$target</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Contructor
</span><span class="sd">     *
</span><span class="sd">     * @param object $target
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span><span class="nv">$target</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">is_object</span><span class="p">(</span><span class="nv">$target</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span> <span class="o">=</span> <span class="nv">$target</span><span class="p">;</span>
            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_className</span> <span class="o">=</span> <span class="nx">get_class</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Register event
</span><span class="sd">     *
</span><span class="sd">     * @param string $eventName
</span><span class="sd">     * @param string $methodName
</span><span class="sd">     * @param callback $callback
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="k">function</span> <span class="nf">_registerEvent</span><span class="p">(</span><span class="nv">$eventName</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">,</span> <span class="nv">$args</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isset</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_eventCallbacks</span><span class="p">[</span><span class="nv">$methodName</span><span class="p">]))</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_eventCallbacks</span><span class="p">[</span><span class="nv">$methodName</span><span class="p">]</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">is_callable</span><span class="p">(</span><span class="k">array</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">)))</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nx">Exception</span><span class="p">(</span><span class="nx">get_class</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span><span class="p">)</span> <span class="o">.</span> <span class="s1">&#39;::&#39;</span> <span class="o">.</span> <span class="nv">$methodName</span> <span class="o">.</span> <span class="s1">&#39; is not exists.&#39;</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">is_callable</span><span class="p">(</span><span class="nv">$callback</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_eventCallbacks</span><span class="p">[</span><span class="nv">$methodName</span><span class="p">](</span><span class="nv">$eventName</span><span class="p">)</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="nv">$callback</span><span class="p">,</span> <span class="nv">$args</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nv">$callbackName</span> <span class="o">=</span> <span class="nx">Aspect</span><span class="o">::</span><span class="na">getCallbackName</span><span class="p">(</span><span class="nv">$callback</span><span class="p">);</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nx">Exception</span><span class="p">(</span><span class="nv">$callbackName</span> <span class="o">.</span> <span class="s1">&#39; is not callable.&#39;</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Register &#39;before&#39; handler
</span><span class="sd">     *
</span><span class="sd">     * @param string $methodName
</span><span class="sd">     * @param callback $callback
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">before</span><span class="p">(</span><span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">,</span> <span class="nv">$args</span> <span class="o">=</span> <span class="k">array</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_registerEvent</span><span class="p">(</span><span class="s1">&#39;before&#39;</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">,</span> <span class="p">(</span><span class="k">array</span><span class="p">)</span> <span class="nv">$args</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Register &#39;after&#39; handler
</span><span class="sd">     *
</span><span class="sd">     * @param string $methodName
</span><span class="sd">     * @param callback $callback
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">after</span><span class="p">(</span><span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">,</span> <span class="nv">$args</span> <span class="o">=</span> <span class="k">array</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_registerEvent</span><span class="p">(</span><span class="s1">&#39;after&#39;</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">,</span> <span class="p">(</span><span class="k">array</span><span class="p">)</span> <span class="nv">$args</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Register &#39;on catch exception&#39; handler
</span><span class="sd">     *
</span><span class="sd">     * @param string $methodName
</span><span class="sd">     * @param callback $callback
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">onCatchException</span><span class="p">(</span><span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">,</span> <span class="nv">$args</span> <span class="o">=</span> <span class="k">array</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_registerEvent</span><span class="p">(</span><span class="s1">&#39;onCatchException&#39;</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">,</span> <span class="p">(</span><span class="k">array</span><span class="p">)</span> <span class="nv">$args</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Trigger event
</span><span class="sd">     *
</span><span class="sd">     * @param string $eventName
</span><span class="sd">     */</span>
    <span class="k">private</span> <span class="k">function</span> <span class="nf">_trigger</span><span class="p">(</span><span class="nv">$eventName</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$target</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">isset</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_eventCallbacks</span><span class="p">[</span><span class="nv">$methodName</span><span class="p">](</span><span class="nv">$eventName</span><span class="p">)))</span> <span class="p">{</span>
            <span class="k">list</span><span class="p">(</span><span class="nv">$callback</span><span class="p">,</span> <span class="nv">$args</span><span class="p">)</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_eventCallbacks</span><span class="p">[</span><span class="nv">$methodName</span><span class="p">](</span><span class="nv">$eventName</span><span class="p">);</span>
            <span class="nv">$args</span><span class="p">[]</span> <span class="o">=</span> <span class="nv">$target</span><span class="p">;</span>
            <span class="nx">call_user_func_array</span><span class="p">(</span><span class="nv">$callback</span><span class="p">,</span> <span class="nv">$args</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Execute method
</span><span class="sd">     *
</span><span class="sd">     * @param string $methodName
</span><span class="sd">     * @param array $args
</span><span class="sd">     * @return mixed
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">function</span> <span class="fm">__call</span><span class="p">(</span><span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$args</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">is_callable</span><span class="p">(</span><span class="k">array</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">)))</span> <span class="p">{</span>
            <span class="k">try</span> <span class="p">{</span>
                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_trigger</span><span class="p">(</span><span class="s1">&#39;before&#39;</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span><span class="p">);</span>
                <span class="nv">$result</span> <span class="o">=</span> <span class="nx">call_user_func_array</span><span class="p">(</span><span class="k">array</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">),</span> <span class="nv">$args</span><span class="p">);</span>
                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_trigger</span><span class="p">(</span><span class="s1">&#39;after&#39;</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_target</span><span class="p">);</span>
                <span class="k">return</span> <span class="nv">$result</span> <span class="o">?</span> <span class="nv">$result</span> <span class="o">:</span> <span class="k">null</span><span class="p">;</span>
            <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">Exception</span> <span class="nv">$e</span><span class="p">)</span> <span class="p">{</span>
                <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_trigger</span><span class="p">(</span><span class="s1">&#39;onCatchException&#39;</span><span class="p">,</span> <span class="nv">$methodName</span><span class="p">,</span> <span class="nv">$e</span><span class="p">);</span>
                <span class="k">throw</span> <span class="nv">$e</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nx">Exception</span><span class="p">(</span><span class="s2">&#34;Call to undefined method </span><span class="si">{</span>$this-&gt;_className<span class="si">}</span><span class="s2">::</span><span class="si">$methodName</span><span class="s2">.&#34;</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="sd">/**
</span><span class="sd">     * Get name of callback
</span><span class="sd">     *
</span><span class="sd">     * @param callback $callback
</span><span class="sd">     * @return string
</span><span class="sd">     */</span>
    <span class="k">public</span> <span class="k">static</span> <span class="k">function</span> <span class="nf">getCallbackName</span><span class="p">(</span><span class="nv">$callback</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nv">$className</span>  <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>
        <span class="nv">$methodName</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">is_array</span><span class="p">(</span><span class="nv">$callback</span><span class="p">)</span> <span class="o">&amp;</span><span class="nx">amp</span><span class="p">;</span><span class="o">&amp;</span><span class="nx">amp</span><span class="p">;</span> <span class="mi">2</span> <span class="o">==</span> <span class="nx">count</span><span class="p">(</span><span class="nv">$callback</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">if</span> <span class="p">(</span><span class="nx">is_object</span><span class="p">(</span><span class="nv">$callback</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span> <span class="p">{</span>
                <span class="nv">$className</span> <span class="o">=</span> <span class="nx">get_class</span><span class="p">(</span><span class="nv">$callback</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                <span class="nv">$className</span> <span class="o">=</span> <span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="nv">$callback</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
            <span class="p">}</span>
            <span class="nv">$methodName</span> <span class="o">=</span> <span class="p">(</span><span class="nx">string</span><span class="p">)</span> <span class="nv">$callback</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
        <span class="p">}</span> <span class="k">elseif</span> <span class="p">(</span><span class="nx">is_string</span><span class="p">(</span><span class="nv">$callback</span><span class="p">))</span> <span class="p">{</span>
            <span class="nv">$methodName</span> <span class="o">=</span> <span class="nv">$callback</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="nv">$className</span> <span class="o">.</span> <span class="p">((</span><span class="nv">$className</span><span class="p">)</span> <span class="o">?</span> <span class="s1">&#39;::&#39;</span> <span class="o">:</span> <span class="s1">&#39;&#39;</span><span class="p">)</span> <span class="o">.</span> <span class="nv">$methodName</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div><p>這個類別有點小長，簡單說明如下：</p>
<ul>
<li>
<p>我們利用 Aspect::addObject() 方法來指定要被切入的物件； addObject() 方法會回傳一個透明的 Aspect 物件。</p>
</li>
<li>
<p>利用 before 、 after 和 onCatchException 三個方法來指定切入的時機，它們會呼叫 _registerEvent() 方法來註冊要執行的回呼函式 (callback) 。</p>
</li>
<li>
<p>執行原來被切入物件的方法，這時會觸動 Aspect 的 __call() 方法，並在指定的切入時機呼叫 _trigger() 方法來執行我們所切入的回呼函式。</p>
</li>
</ul>
<p>先來看看還沒有使用 AOP 前，我們對 TestClass 類別的測試：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require_once</span> <span class="s1">&#39;TestClass.php&#39;</span><span class="p">;</span>
<span class="nv">$test</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TestClass</span><span class="p">();</span>
<span class="cm">/* @var $test TestClass */</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="nv">$test</span><span class="o">-&gt;</span><span class="na">method1</span><span class="p">(</span><span class="s1">&#39;abc&#39;</span><span class="p">);</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="k">echo</span> <span class="nv">$test</span><span class="o">-&gt;</span><span class="na">method2</span><span class="p">(),</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="nv">$test</span><span class="o">-&gt;</span><span class="na">method3</span><span class="p">();</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="cm">/* 執行結果：
</span><span class="cm">=======
</span><span class="cm">TestClass::method1:
</span><span class="cm">abc
</span><span class="cm">=======
</span><span class="cm">TestClass::method2:
</span><span class="cm">2
</span><span class="cm">=======
</span><span class="cm">TestClass::method3:
</span><span class="cm">Exception: Test Exception. in TestClass.php on line 38
</span><span class="cm">*/</span>

</code></pre></div><p>接下來我們利用 Aspect 類別來對 TestClass 物件的三個方法切入 Log::save() ：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require_once</span> <span class="s1">&#39;Aspect.php&#39;</span><span class="p">;</span>
<span class="k">require_once</span> <span class="s1">&#39;TestClass.php&#39;</span><span class="p">;</span>
<span class="k">require_once</span> <span class="s1">&#39;Log.php&#39;</span><span class="p">;</span>
<span class="nv">$test</span> <span class="o">=</span> <span class="nx">Aspect</span><span class="o">::</span><span class="na">addObject</span><span class="p">(</span><span class="k">new</span> <span class="nx">TestClass</span><span class="p">());</span>
<span class="nv">$logger</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Log</span><span class="p">();</span>
<span class="nv">$test</span><span class="o">-&gt;</span><span class="na">before</span><span class="p">(</span><span class="s1">&#39;method1&#39;</span><span class="p">,</span> <span class="k">array</span><span class="p">(</span><span class="nv">$logger</span><span class="p">,</span> <span class="s1">&#39;save&#39;</span><span class="p">),</span> <span class="s1">&#39;Log saved (method1).&#39;</span><span class="p">);</span>
<span class="nv">$test</span><span class="o">-&gt;</span><span class="na">after</span><span class="p">(</span><span class="s1">&#39;method2&#39;</span><span class="p">,</span> <span class="k">array</span><span class="p">(</span><span class="nv">$logger</span><span class="p">,</span> <span class="s1">&#39;save&#39;</span><span class="p">),</span> <span class="s1">&#39;Log saved (method2).&#39;</span><span class="p">);</span>
<span class="nv">$test</span><span class="o">-&gt;</span><span class="na">onCatchException</span><span class="p">(</span><span class="s1">&#39;method3&#39;</span><span class="p">,</span> <span class="k">array</span><span class="p">(</span><span class="nv">$logger</span><span class="p">,</span> <span class="s1">&#39;save&#39;</span><span class="p">),</span> <span class="s1">&#39;Log saved (method3).&#39;</span><span class="p">);</span>
<span class="cm">/* @var $test TestClass */</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="nv">$test</span><span class="o">-&gt;</span><span class="na">method1</span><span class="p">(</span><span class="s1">&#39;abc&#39;</span><span class="p">);</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="k">echo</span> <span class="nv">$test</span><span class="o">-&gt;</span><span class="na">method2</span><span class="p">(),</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="nv">$test</span><span class="o">-&gt;</span><span class="na">method3</span><span class="p">();</span>
<span class="k">echo</span> <span class="s2">&#34;=======</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">;</span>
<span class="cm">/* 執行結果：
</span><span class="cm">=======
</span><span class="cm">Log saved (method1).
</span><span class="cm">TestClass::method1:
</span><span class="cm">abc
</span><span class="cm">=======
</span><span class="cm">TestClass::method2:
</span><span class="cm">Log saved (method2).
</span><span class="cm">8
</span><span class="cm">=======
</span><span class="cm">TestClass::method3:
</span><span class="cm">Log saved (method3).
</span><span class="cm">Exception: Test Exception. in TestClass.php on line 38
</span><span class="cm">*/</span>

</code></pre></div><h2 id="結論">結論</h2>
<p>我們可以從範例看到， AOP 能幫我們在某類別的方法中插入一些額外的動作，同時又能不破壞原有類別的程式碼。而它與 Decorator 最大的不同是， Decorator 必須用很多小類別來完成相同的動作，但是 AOP 則透過 PHP 的動態特性解決了這個問題。</p>
<p>當然 AOP 也不是萬靈丹，像在本文的實作裡它就不能接觸目標類別的非公開屬性。而之前也跟 <a href="http://blog.markplace.net/">Mark</a> 聊了一下，其實 AOP 偏向於程式的整體設計，所以這裡的範例尚不能用於實戰之中，僅僅只是我個人一個概念的實作而已。</p>
<p>供大家參考看看吧。也歡迎一起討論~</p>
]]></content>
		</item>
		
		<item>
			<title>[PHP] PHP 密技： include 與 require</title>
			<link>https://jaceju.net/return-value-from-included-file/</link>
			<pubDate>Fri, 22 Feb 2008 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/return-value-from-included-file/</guid>
			<description>可以接受回傳資料？ 先調查一下，知道 include 或 require 可以取得回傳資料的請舉手&amp;hellip; (眺望) 呃&amp;hellip;不知道的朋友也不用煩惱，我來解釋一</description>
			<content type="html"><![CDATA[<h2 id="可以接受回傳資料">可以接受回傳資料？</h2>
<p>先調查一下，知道 include 或 require 可以取得回傳資料的請舉手&hellip; (眺望)</p>
<p>呃&hellip;不知道的朋友也不用煩惱，我來解釋一下。</p>
<!-- raw HTML omitted -->
<p>如何回傳資料呢？假設現在有個 php 檔叫做 <code>config.php</code> ，內容如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">return</span> <span class="k">array</span><span class="p">(</span><span class="s1">&#39;123&#39;</span><span class="p">,</span> <span class="s1">&#39;456&#39;</span><span class="p">);</span>
</code></pre></div><p>咦？那邊有人說 <code>return</code> 放錯地方了？不不不， PHP 能接受這樣的寫法。</p>
<p>好，現在我們來證明 <code>include</code> 或 <code>require</code> 能取得 <code>config.php</code> 所 return 回來的資料。請建立一支 <code>test.php</code> ，其內容是：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$config</span> <span class="o">=</span> <span class="k">require</span> <span class="s1">&#39;config.php&#39;</span><span class="p">;</span>
<span class="nx">var_dump</span><span class="p">(</span><span class="nv">$config</span><span class="p">);</span>
</code></pre></div><p>執行看看，是不是可以跑呀？</p>
<p>所以我們可以在某支 PHP 程式中 return 一個資料 (任何型態) ，然後在另一支 PHP 程式中用 <code>include</code> 或 <code>require</code> 來取得這個資料。</p>
<h2 id="把-require-放在參數裡">把 require 放在參數裡</h2>
<p>什麼？這不是密技？不不不，密技在底下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">function test($config) {
    var_dump($config);
}
test(require &#39;config.php&#39;);
</code></pre></div><p>對！你沒看錯！直接把 <code>require</code> 放在函式的參數裡！</p>
<p>還沒完呢，再看：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Test
{
    public function __construct($config)
    {
        var_dump($config);
    }
}

$a = new Test(require &#39;config.php&#39;);
</code></pre></div><p>連 new 建構子的參數都可以接受 require ！</p>
<p>所以只要能放變數的地方，都可以放 <code>include</code> 或 <code>require</code> ，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">if (require &#39;config.php&#39;) {
    var_dump(require &#39;config.php&#39;);
}

if ($config = require &#39;config.php&#39;) {
    var_dump($config);
}
</code></pre></div><p>而且不僅是 <code>include</code> 及 <code>require</code> ，連 <code>include_once</code> 和 <code>require_once</code> 都可以這麼做。</p>
<p>我在<a href="http://blog.astrumfutura.com/archives/340-The-Zend-Framework,-Dependency-Injection-and-Zend_Di.html">某篇文章</a>發現這個密技以後，分享給辦公室裡的同事們；沒想到玩了 PHP 這麼多年的他們也沒看過這個方法，看來大家對 PHP 的瞭解需要更深入一點囉！</p>
<h2 id="scope-的問題">Scope 的問題</h2>
<p>接著我同事問了我一個問題：如果在參數使用 <code>require</code> 敘述，而且被 require 的 PHP 程式裡如果有定義全域變數的話，那麼這個變數在執行的 PHP 程式裡，它的 scope 在哪裡呢？</p>
<p>答案是：它還是全域。</p>
<p>怎麼說呢？現在我們在剛剛的 <code>config.php</code> 的 <code>return</code> 敘述前加上一行程式，如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$data</span> <span class="o">=</span> <span class="s1">&#39;789&#39;</span><span class="p">;</span> <span class="c1">// 加上這行
</span><span class="c1"></span><span class="k">return</span> <span class="k">array</span><span class="p">(</span><span class="s1">&#39;123&#39;</span><span class="p">,</span> <span class="s1">&#39;456&#39;</span><span class="p">);</span>
</code></pre></div><p>然後在 test.php 裡的 Global 部份 (也就是不在函式或類別定義裡) 的任意處加入：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">var_dump($data);
</code></pre></div><p>是不是也可以正確顯示 <code>config.php</code> 中 <code>$data</code> 變數所指定的內容呢？這就表示在參數中使用 <code>require</code> 不會影響全域變數的 scope 。</p>
<p>還有其他 <code>include</code> 或 <code>require</code> 的密技嗎？歡迎大家一起討論囉~</p>
]]></content>
		</item>
		
		<item>
			<title>[PHP] 交換兩個變數 (不使用 tmp 變數) 程式寫法</title>
			<link>https://jaceju.net/php-swap-variables/</link>
			<pubDate>Fri, 23 Nov 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-swap-variables/</guid>
			<description>在宗董的 Blog 看到這篇：交換兩個變數 (不使用 tmp 變數) 程式寫法，本來想留言，不過宗董的 Blog 系統似乎有問題。 宗董的方法是這樣的： $a ^= $b; $b ^= $a; $a ^= $b; 我是</description>
			<content type="html"><![CDATA[<p>在宗董的 Blog 看到這篇：<a href="http://plog.longwin.com.tw/programming/2007/11/23/variable_swap_programming_2007">交換兩個變數 (不使用 tmp 變數) 程式寫法</a>，本來想留言，不過宗董的 Blog 系統似乎有問題。</p>
<p>宗董的方法是這樣的：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">$a ^= $b;
$b ^= $a;
$a ^= $b;
</code></pre></div><p>我是想說既然是用 PHP 了，就應該好好善用一下 PHP 的原生語法：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">[$a, $b] = [$b, $a];
</code></pre></div><p>搞定~~</p>
<p>這個是從 <a href="http://www.pearsoned.com.tw/chinese_show_title.asp?bkid=9867910672">PHP 程式設計專家必備手冊</a>一書看來的。</p>
]]></content>
		</item>
		
		<item>
			<title>[PHP] 神奇的 $this</title>
			<link>https://jaceju.net/magic-this-in-php-class/</link>
			<pubDate>Fri, 26 Oct 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/magic-this-in-php-class/</guid>
			<description>今天發現了一個 PHP 5.2.4 的奇怪現象，查了官方手冊也沒發現有人特別提起 (也可能是我沒找到) 。 學過 PHP 物件導向的人都知道， $this 這個關鍵字是在生成一個物件後</description>
			<content type="html"><![CDATA[<p>今天發現了一個 PHP 5.2.4 的奇怪現象，查了官方手冊也沒發現有人特別提起 (也可能是我沒找到) 。</p>
<!-- raw HTML omitted -->
<p>學過 PHP 物件導向的人都知道， $this 這個關鍵字是在生成一個物件後才能使用的。例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Foo
{
    private $_foo = &#39;_foo in class Foo.&#39;;
    public function test()
    {
        echo $this-&gt;_foo;
    }
}
$foo = new Foo();
$foo-&gt;test(); // _foo in class Foo.

</code></pre></div><p>而且 <code>$this</code> 在 Class 的程式碼裡代表的也是這個物件本身，在上例中即為 <code>$foo</code> 。</p>
<p>不過在 method 裡使用 <code>$this</code> 有個限制，那就是該 method 不能以 <code>static</code> 的方式來呼叫；也就是說，以下的執行方式是錯的：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">Foo::test(); // Fatal error: Using $this when not in object context in xxx.php
</code></pre></div><p>可是請看以下的程式碼：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">class Foo
{
    private $_foo = &#39;_foo in class Foo.&#39;;

    public function test()
    {
        echo $this-&gt;_foo;
    }
}

class Bar
{
    public function test()
    {
        Foo::test();
    }
}

$b = new Bar();
$b-&gt;test(); // Notice: Undefined property:  Bar::$_foo in xxx.php
</code></pre></div><p>發現什麼問題了嗎？在 <code>Bar::test()</code> 裡我們竟然可以用 static 的方式呼叫 <code>Foo::test()</code> ！ 而且在 <code>Foo::test()</code> 裡的 <code>$this-&gt;_foo</code> 竟然變成了 <code>Bar</code> 類別的 <code>$_foo</code> 屬性！</p>
<p>至於這倒底是 PHP 的特色還是 Bug ？我也不知道，還望高手賜教。</p>
]]></content>
		</item>
		
		<item>
			<title>Linux 上的 Port Foward 的問題</title>
			<link>https://jaceju.net/linux-port-forward/</link>
			<pubDate>Thu, 13 Sep 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/linux-port-forward/</guid>
			<description>問題 Mark 和我兩個人在研究怎麼把對外的 Port 9000 轉移到內部的 Port 80 ，結果我這個 Linux 新手搞了半天還是搞不出來。 環境 我的環境是這樣的： |-- (DMZ) --&amp;gt; Linux Server 1(192.168.1.1) IP 分享器 -| |-----------&amp;gt;</description>
			<content type="html"><![CDATA[<h2 id="問題">問題</h2>
<p>Mark 和我兩個人在研究怎麼把對外的 Port 9000 轉移到內部的 Port 80 ，結果我這個 Linux 新手搞了半天還是搞不出來。</p>
<h2 id="環境">環境</h2>
<p>我的環境是這樣的：</p>
<pre><code>           |-- (DMZ) --&gt; Linux Server 1(192.168.1.1)
IP 分享器 -|
           |-----------&gt; Linux Server 2(192.168.1.2)

</code></pre><p>註：之前示意圖畫錯了，更正一下。</p>
<p>然後從外面來的 Port 9000 要 Forward 到 Linux Server 2 的 Port 80 。</p>
<!-- raw HTML omitted -->
<h2 id="嘗試">嘗試</h2>
<p>我們用的 IP 分享器僅支援單一 Port 對應的功能 (就是 Port 80 就只能對到 Port 80) ，所以只好把腦筋動到 Linux Server 1 上。</p>
<p>本來以為用 iptables 的 NAT 功能可以用，看了一下<a href="http://linux.vbird.org/linux_server/0250simple_firewall.php">鳥哥的 iptables 教學</a>，得到以下的方式：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="c1"># iptables -t nat -A PREROUTING -p tcp -i eth0 --dport 9000 -j DNAT --to 192.168.1.2:80</span>
</code></pre></div><p>不過加入去以後還是不行&hellip;後來 Mark 又找到一篇 <a href="http://linux.tnc.edu.tw/techdoc/firewall/iptables-intro.html">iptables 入門</a>，裡面寫了：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="c1"># iptables -t nat -A PREROUTING -i eth0 -p tcp -d 192.168.1.1 --dport 9000 -j DNAT --to-destination 192.168.1.2:80</span>
</code></pre></div><p>也是沒用&hellip;真是令人洩氣&hellip;</p>
<h2 id="解決方案">解決方案</h2>
<p>後來我問我的同學 (他是個 FreeBSD 強者) ，他給我一個連結： <a href="http://antontw.blogspot.com/2007/05/linux-port-forward.html">[Linux] 簡單的 port forward 工具</a>，裡面介紹了 <a href="http://www.linux.org/apps/AppId_865.html">redir</a> 這個小程式，似乎符合我的要求。</p>
<p>然而我用的 OS 是 CentOS ，要自己編譯 redir ；把 redir 的 source 抓下來後，按照以下方式編譯：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="c1"># cd /usr/local/src</span>
<span class="c1"># wget http://sammy.net/~sammy/hacks/redir-2.2.1.tar.gz</span>
<span class="c1"># tar xvzf redir-2.2.1.tar.gz</span>
<span class="c1"># cd redir-2.2.1</span>
<span class="c1"># make (沒有 install)</span>
<span class="c1"># mv redir /usr/local/bin/</span>
</code></pre></div><p>接下來我下的指令是這樣子的：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="c1"># redir --lport 9000 --caddr=192.168.1.2 --cport=80</span>
</code></pre></div><p>不過還是沒用，而且就 redir 就沒有回應了，因為 redir 主要是跑 Daemon 模式。我想如果加上 Listen 的 Address 應該就可以，所以我把指令改成：</p>
<div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash"><span class="c1"># redir --laddr 192.168.1.1  --lport 9000 --caddr=192.168.1.2 --cport=80 &amp;amp;</span>
</code></pre></div><p>沒想到就成功了！讓 Mark 和我著實感動了一陣子。</p>
<p>最後把它加到 <code>/etc/rc.d/rc.local</code> 裡，讓它能在開機時自動執行就完成了。</p>
<h2 id="後記">後記</h2>
<p>雖然我有在玩 Linux ，不過很多技術還是不熟，這次 Mark 出的問題真的讓我學到了不少東西。也感謝我的強者同學，他在 Unix like 方面的知識真的幫了我不少忙。</p>
<p>另外， redir 真的是不錯的小工具，如果要完全靠 iptables 來處理這個問題的話，我這個新手大概弄個三天三夜也搞不定&hellip;Orz</p>
<p>當然，如果大家有更好的方法的話，也歡迎分享喔。</p>
]]></content>
		</item>
		
		<item>
			<title>PHP4 的盡頭</title>
			<link>https://jaceju.net/the-end-of-php4/</link>
			<pubDate>Fri, 13 Jul 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/the-end-of-php4/</guid>
			<description>PHP 官方宣佈停止對 PHP4 的支援了，原文如下： PHP 4 end of life announcement [13-Jul-2007]Today it is exactly three years ago since PHP 5 has been released. In those three years it has seen many improvements over PHP 4. PHP 5 is fast, stable &amp;amp; production-ready and as PHP 6 is on the way, PHP 4 will be discontinued.</description>
			<content type="html"><![CDATA[<p><a href="http://www.php.net">PHP 官方</a>宣佈停止對 PHP4 的支援了，原文如下：</p>
<!-- raw HTML omitted -->
<h3 id="php-4-end-of-life-announcement">PHP 4 end of life announcement</h3>
<p><!-- raw HTML omitted -->[13-Jul-2007]<!-- raw HTML omitted --></p>
<p>Today it is exactly three years ago since PHP 5 has been released. In those three years it has seen many improvements over PHP 4. PHP 5 is fast, stable &amp; production-ready and as PHP 6 is on the way, PHP 4 will be discontinued.</p>
<p>The PHP development team hereby announces that support for PHP 4 will continue until the end of this year only. After 2007-12-31 there will be no more releases of PHP 4.4. We will continue to make critical security fixes available on a case-by-case basis until 2008-08-08. Please use the rest of this year to make your application suitable to run on PHP 5.</p>
<p>For documentation on migration for PHP 4 to PHP 5, we would like to point you to our <a href="/manual/en/migration5.php">migration guide</a>. There is additional information available in the <a href="/manual/en/migration51.php">PHP 5.0 to PHP 5.1</a> and <a href="/manual/en/migration52.php">PHP 5.1 to PHP 5.2</a> migration guides as well.</p>
<!-- raw HTML omitted -->
<p>對某些人來說大概是壞消息，但對我而言卻是很棒的新聞。</p>
<p>PHP 終於醒了&hellip;</p>
]]></content>
		</item>
		
		<item>
			<title>我也來實作 PHP mix-in 的概念 - Part 3</title>
			<link>https://jaceju.net/php-mix-in-3/</link>
			<pubDate>Tue, 27 Mar 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-mix-in-3/</guid>
			<description>說明 石頭成老大把他心目中的 mix-in 目標做出來了，他主要的實作有以下兩個重點： 物件實體生成後彼此做 mix-in 是不相干的。 類別方法在動態委派後要能遵守繼承原則</description>
			<content type="html"><![CDATA[<h2 id="說明">說明</h2>
<p>石頭成老大把他心目中的 mix-in <a href="http://blog.roodo.com/rocksaying/archives/2884871.html">目標</a>做出來了，他主要的實作有以下兩個重點：</p>
<ul>
<li>物件實體生成後彼此做 mix-in 是不相干的。</li>
<li>類別方法在動態委派後要能遵守繼承原則，也就是說子承父、父不承子。</li>
</ul>
<p>另外他也提到要儲存方法是一件困難的事情，因為 PHP 有三種函式的呼叫方法：一般函數、類別靜態方法、實例方法。而我在 <a href="http://blog.roodo.com/jaceju/archives/2853761.html">Part 2</a> 裡的概念實作則是用 <a href="http://blog.roodo.com/jaceju/archives/409709.html">callback</a> 虛擬型態來儲存，不過<!-- raw HTML omitted --><!-- raw HTML omitted -->卻忘了把一般函式給放進去。</p>
<p>不過 Part 2 的實作已經實現了第一個目標，所以在這次 Part 3 的實作裡，我除了決定把一般函式也納入 mix-in 的實作裡，而且還要達成石頭成老大說的第二個目標。</p>
<!-- raw HTML omitted -->
<p>另外我自己也加入了以下實作重點：</p>
<!-- raw HTML omitted -->
<p>經過一番努力，我終於試出來了；先來看看成果，後面我才來一一分析。</p>
<h2 id="可以-mix-in-方法的測試類別">可以 mix-in 方法的測試類別</h2>
<p>首先我定義一個繼承自 Prototype 的類別稱為 ParentClass ：</p>
<pre><code>// 繼承自 Prototype 的類別
class ParentClass extends Prototype
{
    public function method7($param)
    {
        echo get_class($this), '::method7';
        echo &quot;(&quot;, $param, &quot;)\n&quot;;
        echo &quot;\n&quot;;
    }
}

</code></pre><p>註：可以 mix-in 的抽象類別我還是稱為 Prototype ，它必須被其他類別繼承，而無法獨自生成實體。</p>
<p>為了要實現石頭成老大的第一個目標，我再新增三個類別如下：</p>
<pre><code>// 測試用的 Prototype 子類別 1
class ChildClass1 extends ParentClass
{
}
// 測試用的 Prototype 子類別 2
class ChildClass2 extends ParentClass
{
    public function __construct()
    {
    parent::__construct();
    echo &quot;I'm &quot; . __CLASS__ . '!!', &quot;\n&quot;;
    }
}
// 測試用的 Prototype 子類別 3
class ChildClass3 extends ChildClass1
{
}

</code></pre><p>這裡 ChildClass1 和 ChildClass2 都繼承自 ParentClass ，而 ChildClass3 則繼承自 ChildClass1 ，而且它們也都是 Prototype 的子類別。</p>
<h2 id="可用來做-mix-in-的-callback-函式">可用來做 mix-in 的 Callback 函式</h2>
<p>接下來我要準備可以當成 callback 的函式與類別，這裡分成一般函式、類別函式及 MethodObject 物件。</p>
<p>不過上次的 MethodObject 類別名稱我覺得還是不太好，這次我改用 Callback 這個名稱，但實際上它的功能還是一樣：</p>
<pre><code>// mix-in 方法的抽象類別
abstract class Callback
{
    protected $object;
    public function __construct($object = NULL)
    {
        $this-&gt;object = $object;
    }
    abstract function run();
}

</code></pre><p>不過抽象類別是無法產生實體的，所以和前一版相同，我用一個 TestMethod 類別來繼承 Callback ：</p>
<pre><code>// 方法類別
class TestMethod extends Callback
{
    public function run()
    {
        $n = func_num_args();
        echo __METHOD__;
        echo '(', (1 == $n) ? func_get_arg(0) : '', &quot;)\n&quot;;
        echo &quot;\n&quot;;
    }
}

</code></pre><p>然後是一個普通的類別 Util ，並提供了兩個公開方法：</p>
<pre><code>// 一般類別
class Util
{
    public function method1($param)
    {
        echo __METHOD__;
        echo &quot;(&quot;, $param, &quot;)\n&quot;;
        echo &quot;\n&quot;;
    }
    public function method2($param)
    {
        echo __METHOD__;
        echo &quot;(&quot;, $param, &quot;)\n&quot;;
        echo &quot;\n&quot;;
    }
}

</code></pre><p>最後是兩個函式 normalFunc1 及 normalFunc2 ，它們就是一般的自訂函式而已：</p>
<pre><code>// 一般函式
function normalFunc1($param)
{
    echo 'normalFunc1(' . $param, &quot;)\n&quot;;
    echo &quot;\n&quot;;
}
function normalFunc2($param, $object = NULL)
{
    echo 'normalFunc2(' . $param, &quot;)\n&quot;;
    var_export($object);
    echo &quot;\n&quot;;
}

</code></pre><h2 id="測試結果">測試結果</h2>
<p>現在重頭戲來了，我先把需要的 callback 變數準備好：</p>
<pre><code>// 測試用的程式碼
$a = new Util();
$callback1 = array ('Util', 'method1');
$callback2 = array ($a, 'method2');
$callback3 = 'TestMethod';
$callback4 = array ('TestMethod', 'run');
$callback5 = 'normalFunc1';
$callback6 = 'normalFunc2';

</code></pre><p>這裡分成 $callback1 到 $callback6 ，其中 $callback3 和 $callback4 其實是一樣的，只不過寫法不同而已。</p>
<p>先來看看石頭成老大要的第二個目標，在我這裡是怎麼做的：</p>
<pre><code>Prototype::delegate('ParentClass::method4', $callback4);
Prototype::delegate('ChildClass1::method5', $callback5);

</code></pre><p>和石頭成老大的作法不同，我在 Prototype 定義了一個靜態的 delegate 函式，然後用上面的方法來指定靜態的 mix-in 。本來我想是直接用 <!-- raw HTML omitted -->ParentClass::delegate(&lsquo;method4&rsquo;, $callback)<!-- raw HTML omitted --> 這種方式來完成，不過 <!-- raw HTML omitted -->PHP 並沒有辦法在繼承下來的靜態函式裡取得該類別的類別名稱<!-- raw HTML omitted -->，所以這裡我想還是透過 Prototype 來完成，而這也符合我自己的第一個實作重點。</p>
<p>這裡要注意一點，那就是這時我已經將 $callback4 混入 ParentClass ，此時 ParentClass 除了自己的 <!-- raw HTML omitted -->method7<!-- raw HTML omitted --> 方法外，還會有 <!-- raw HTML omitted -->method4<!-- raw HTML omitted --> 方法共兩個方法。而 $callback5 則是混入 ChildClass1 ，又因為 ChildClass1 繼承自 ParentClass ，所以 ChildClass1 就會有三個方法： <!-- raw HTML omitted -->method4<!-- raw HTML omitted --> 、 <!-- raw HTML omitted -->method5<!-- raw HTML omitted --> 、 <!-- raw HTML omitted -->method7<!-- raw HTML omitted --> 。</p>
<p>接下來我先把 ParentClass 和 ChildClass 的實體產生出來，分別是 $parent1 和 $child1 。然後我再一次把 $callback6 混入 ParentClass ，接著再建立 $parent2 、 $child2 與 $child3 等三個分別為 ParentClass 、 ChildClass2 和 ChildClass3 類別的實體。</p>
<pre><code>$parent1 = new ParentClass;
$child1 = new ChildClass1;
Prototype::delegate('ParentClass::method6', $callback6);
$parent2 = new ParentClass();
$child2 = new ChildClass2();
$child3 = new ChildClass3();

</code></pre><p>猜猜看，這時 $parent1 這個物件實體能夠呼叫 method6 這個方法嗎？如果不行，那 $parent2 呢？而 $child2 和 $child3 裡混入的方法又有哪些呢？</p>
<p>在我的想法裡，因為 method6 的混成是在 $parent1 產生之後，所以 $parent1 並沒有辦法呼叫 method6 (符合石頭成老大的要求) ；不過 $parent2 就可以了，因為它是在混成 method6 以後才建立的。</p>
<p>另外 $child2 因為繼承自 ParentClass ，而且還是在 method6 混入後才建立的，所以它就會有 <!-- raw HTML omitted -->method4<!-- raw HTML omitted --> 、 <!-- raw HTML omitted -->method6<!-- raw HTML omitted --> 和 <!-- raw HTML omitted -->method7<!-- raw HTML omitted --> 三個方法可用。再看 $child3 ，因為它繼承了 ChildClass1 ，所以就會有 <!-- raw HTML omitted -->method4<!-- raw HTML omitted --> 、 <!-- raw HTML omitted -->method5<!-- raw HTML omitted --> 、 <!-- raw HTML omitted -->method6<!-- raw HTML omitted --> 與 <!-- raw HTML omitted -->method7<!-- raw HTML omitted --> 。</p>
<p>搞混了嗎？我想應該還不至於，現在我再把第一版的 mix-in 方式加進來：</p>
<pre><code>$parent1-&gt;method1 = $callback1;
$parent1-&gt;method2 = $callback2;
$parent1-&gt;method3 = $callback3;

</code></pre><p>注意這裡 $callback1 、 $callback2 和 $callback3 是直接混入 $parent1 這個物件實體，所以 $parent2 、 $child1 、 $child2 與 $child3 是沒辦法呼叫 method1 、 method2 及 method3 的 (石頭成老大的第一個目標) 。</p>
<p>好了，現在來試試呼叫 $parent1 的方法；猜猜看哪些方法是能運作的？</p>
<pre><code>try
{
    $parent1-&gt;method1('param1');
    $parent1-&gt;method2('param2');
    $parent1-&gt;method3('param3');
    $parent1-&gt;method4('param4');
    $parent1-&gt;method5('param5');
    $parent1-&gt;method6('param6');
    $parent1-&gt;method7('param7');
} catch (Exception $e) {
    echo $e, &quot;\n&quot;;
}

</code></pre><p>只有 method1 、 method2 、 method3 、method4 及 method7 才能運作，猜對了嗎？</p>
<p>註：這裡的 try 寫法其實有點不太對，因為在呼叫 method5 而出現 Exception 後就會跳出 try 區塊了。我是利用註解讓它繼續往下做，下面的範例也是相同，這裡請大家別太在意。</p>
<p>同樣的方法再來試試 $parent2 、 $child1 、 $child2 及 $child3：</p>
<pre><code>try
{
    $parent2-&gt;method1('param1'); // Exception!!
    $parent2-&gt;method2('param2'); // Exception!!
    $parent2-&gt;method3('param3'); // Exception!!
    $parent2-&gt;method4('param4'); // Work!!
    $parent2-&gt;method5('param5'); // Exception!!
    $parent2-&gt;method6('param6'); // Work!!
    $parent2-&gt;method7('param7'); // Work!!
} catch (Exception $e) {
    echo $e, &quot;\n&quot;;
}
try
{
    $child1-&gt;method1('param1'); // Exception!!
    $child1-&gt;method2('param2'); // Exception!!
    $child1-&gt;method3('param3'); // Exception!!
    $child1-&gt;method4('param4'); // Work!!
    $child1-&gt;method5('param5'); // Work!!
    $child1-&gt;method6('param6'); // Exception!!
    $child1-&gt;method7('param7'); // Work!!
} catch (Exception $e) {
    echo $e, &quot;\n&quot;;
}
try
{
    $child2-&gt;method1('param1'); // Exception!!
    $child2-&gt;method2('param2'); // Exception!!
    $child2-&gt;method3('param3'); // Exception!!
    $child2-&gt;method4('param4'); // Work!!
    $child2-&gt;method5('param5'); // Exception!!
    $child2-&gt;method6('param6'); // Work!!
    $child2-&gt;method7('param7'); // Work!!
} catch (Exception $e) {
    echo $e, &quot;\n&quot;;
}
try
{
    $child3-&gt;method1('param1'); // Exception!!
    $child3-&gt;method2('param2'); // Exception!!
    $child3-&gt;method3('param3'); // Exception!!
    $child3-&gt;method4('param4'); // Work!!
    $child3-&gt;method5('param5'); // Work!!
    $child3-&gt;method6('param6'); // Work!!
    $child3-&gt;method7('param7'); // Work!!
} catch (Exception $e) {
    echo $e, &quot;\n&quot;;
}

</code></pre><p>運作方式就如同上面註解所示。</p>
<p>這樣一來，所有的測試結果都符合石頭成老大的目標以及我自己所要求的重點。要注意的事項我也不再多提了，請自行參考石頭成老大的文章，以及我前面的說明。</p>
<p>另外或許大家會注意到，為什麼我都是利用物件來呼叫 mix-in 方法，而不是使用靜態呼叫。這是因為 PHP 也沒提供靜態的 __call 魔術方法，所以這裡並沒辦法用 ParentClass::method1() 這樣的語法，這是此法美中不足的地方。</p>
<h2 id="主角-prototye-類別">主角 Prototye 類別</h2>
<p>最後來看看我是如何實作 Prototype 類別的，這裡我提出幾個重點：</p>
<ul>
<li>因為類別方法和實體方法是不同的，所以我將它們分開存放；等到物件生成時，再將它們依照繼承關係合併在一起。</li>
<li>執行時期， Prototype 類別的 __call 魔術方法只執行實體方法。</li>
</ul>
<p>先看看 Prototype 的屬性成員宣告：</p>
<p>註：從以下程式開始到文章的最後，除了說明文字外的程式區塊都是屬於 Prototype 類別的程式碼範圍。</p>
<pre><code>// 可接受 mix-in 物件的抽象類別
abstract class Prototype
{
    const PROTOTYPE_METHOD = 0;
    const INSTANCE_METHOD = 1;
    const CALLBACK_LENGTH = 2;
    public static $class_methods = array ();
    public $instance_methods = array ();

</code></pre><p>這裡我定義了兩個存放 mix-in 方法的 陣列， $class_method 用以存放類別 mix-in 方法，而 $instance_methods 則是存放該實體的 mix-in 方法，這個和石頭成老大的想法相似；另外 PROTOTYPE_METHOD 和 INSTANCE_METHOD 則是用來表示該 mix-in 方法是屬於類別方法還是實體方法，後面的 setCallback 方法會用到。</p>
<p>先看 Prototype 的 delegate 方法：</p>
<pre><code>    public final function delegate($name, $callback)
    {
        $class_name = '';
        $method_name = '';
        $delegate = explode('::', $name);
        if (Prototype::CALLBACK_LENGTH == count($delegate)) {
            list($class_name, $method_name) = $delegate;
        } else {
            throw new Exception('Syntax error!!');
        }
        $class_name = strtolower($class_name);
        if (!class_exists($class_name)) {
            throw new Exception(&quot;Class $class_name not exists!!&quot;);
        }
        if (!is_subclass_of($class_name, __CLASS__)) {
            throw new Exception(&quot;Class $class_name not subclass of &quot; . __CLASS__ . &quot;!!&quot;);
        }
        self::setCallback($class_name, $method_name, $callback, Prototype::PROTOTYPE_METHOD);
    }

</code></pre><p>delegate 要做的事情很簡單，就是將 callback 虛擬型態變數放到 $class_methods 中對應的類別裡；換句話說，我在 $class_methods 裡會把繼承自 Prototype 的類別都用一個陣列來存放。另外我也希望 delegate 方法是不能被子類別所推翻的，所以我使用 final 關鍵字來宣告它。</p>
<p>而存放的 setCallback 方法如下：</p>
<pre><code>    private final function setCallback($class_name, $method_name, $callback, $method_type)
    {
        if (is_array($callback)) {
            if (is_object($callback[0])
                    || (is_string($callback[0])
                    &amp;amp;&amp;amp; class_exists($callback[0]))) {
                $callback = array ($callback[0], $callback[1]);
            }
        }
        if (is_callable($callback) ||
                (class_exists($callback) &amp;amp;&amp;amp; is_subclass_of($callback, 'Callback'))) {
            if (Prototype::PROTOTYPE_METHOD == $method_type) {
                self::$class_methods[$class_name]($method_name) = $callback;
            } elseif (Prototype::INSTANCE_METHOD == $method_type) {
                $this-&gt;instance_methods[$method_name] = $callback;
            }
        }
    }

</code></pre><p>因為 setCallback 只在 Prototype 裡被使用，所以宣告成 private final 。 setCallback 方法會依照指定的類型，把 callback 放在對應的 method 陣列裡。另外我還在石頭成老大那邊也學會 is_callable 函式的用法 (發現自己以前好蠢) ， 這樣看起來程式就比之前自己判斷 callback 型態來得簡單多了。</p>
<p>然後是 Prototype 類別的重點，因為石頭成老大的程式給我一個啟發，利用 for 迴圈我可以<!-- raw HTML omitted -->推展出目前類別的繼承關係，進而把所有父類別的類別 mix-in 方法全部合併到目前物件實體裡的 mix-in 方法陣列中<!-- raw HTML omitted -->。</p>
<pre><code>    public function __construct()
    {
        $current_class = strtolower(get_class($this));
        $this-&gt;instance_methods = array ();
        for ($class_name = $current_class;
                $parent_name = strtolower(get_parent_class($class_name));
                $class_name = $parent_name) {
            if (isset(self::$class_methods[$class_name])) {
                $this-&gt;instance_methods = array_merge(
                $this-&gt;instance_methods,
                self::$class_methods[$class_name]);
            }
        }
    }

</code></pre><p>這裡 get_parent_class 函式會幫我們把指定類別的上一層父類別找出來；知道這個方式後，再仔細看看 for 迴圈裡的寫法，就會發現石頭成老大真的很神 (這是從他的程式裡偷來的 XD ) 。</p>
<p>相對之下， __set 魔術方法就很簡單了：</p>
<pre><code>    private final function __set($method_name, $callback)
    {
        $class_name = strtolower(get_class($this));
        self::setCallback($class_name, $method_name, $callback, Prototype::INSTANCE_METHOD);
    }

</code></pre><p>這裡就是取得目前類別的名稱，然後再交由 setCallback 去處理。</p>
<p>接下來是 __call 魔術方法：</p>
<pre><code>    private final function __call($callback, $args)
    {
        if (isset($this-&gt;instance_methods[$callback])) {
            $callback = $this-&gt;instance_methods[$callback];
            $this-&gt;executeCallback($callback, $args);
            return;
        }
        throw new Exception(get_class($this) . &quot;::$callback is not exists!&quot;);
    }

</code></pre><p>它也非常簡單，就是找出對應的實體 mix-in 方法，並交由 executeCallback 方法來執行。</p>
<p>最後是 executeCallback 方法，也是仿照 setCallback 的方式，只不過它是執行 callback 。</p>
<pre><code>    private final function executeCallback($callback, $args)
    {
        $args[] = $this;
        if (is_callable($callback)) {
            call_user_func_array($callback, $args);
        } elseif (class_exists($callback)
                &amp;amp;&amp;amp; is_subclass_of($callback, 'Callback')) {
            $method_object = new $callback($this);
            call_user_func_array(array ($method_object, 'run'), $args);
        }
    }
} // End of Prototype

</code></pre><p>特別注意一點，我把 Prototype 物件實體放在 $args 的最後一個，這樣如果像 normalFunc2 這樣的函式被混入的話，就可以存取到這些物件實體了。</p>
<h2 id="後記">後記</h2>
<p>雖然我覺得這些方法有時候會令人迷惑，不過其實它也有好用的一面。這裡我也向石頭成老大學習到很多特別的技巧，也讓我自己對靜態變數與實體變數之間有更深的瞭解。</p>
<p>PHP 也許不像 Ruby 的語法那樣地富有變化性，不過我想只要應用得當， PHP 還是能夠發展出屬於自己的特性的。</p>
]]></content>
		</item>
		
		<item>
			<title>好書推薦</title>
			<link>https://jaceju.net/good-books/</link>
			<pubDate>Tue, 20 Mar 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/good-books/</guid>
			<description>新書預告 以下為最近將會出版的書籍，值得期待： CSS 大全 第三版 好書推薦 另外整理一些我個人覺得不錯的 Web 開發相關中文書籍供大家參考 (我看過的才介紹) ，</description>
			<content type="html"><![CDATA[<h2 id="新書預告">新書預告</h2>
<p>以下為最近將會出版的書籍，值得期待：</p>
<ul>
<li><a href="http://www.oreilly.com.tw/product2_web.php?id=a212">CSS 大全 第三版</a></li>
</ul>
<h2 id="好書推薦">好書推薦</h2>
<p>另外整理一些我個人覺得不錯的 Web 開發相關中文書籍供大家參考 (我看過的才介紹) ，它們都是非常值得大家閱讀與收藏的技術書籍。</p>
<p>註：只介紹中文書的原因是為了讓經驗較少的伙伴也能快速瞭解這些技術，事實上還有更多原文的好書值得參考。</p>
<!-- raw HTML omitted -->
<h3 id="css">CSS</h3>
<ul>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9575279409&amp;sid=32178">向世界最TOP的網站學 CSS 網頁設計</a>可以學到一些真實的 CSS 設計概念。* <a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9864217828&amp;sid=27407">Eric Meyer 再談 CSS 網頁排版設計</a> CSS 大師著作，必讀！</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=986719912X&amp;sid=26908">The Zen of CSS Design─網頁視覺設計的王道</a> 這本書也可以學到很多 <a href="http://www.csszengarden.com/">Zen Garden</a> 在 CSS 設計的概念。* <a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9575278259&amp;sid=26381"> 網頁設計標準規格</a> 要瞭解標準網頁與 CSS 如何搭配的話，看這本就對了。* <a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867529758&amp;sid=24438">CSS 網頁設計師手札</a> 多到不行的 CSS 技巧說明，必讀！</p>
</li>
</ul>
<h3 id="javascript-amp-ajax">JavaScript &amp; AJAX</h3>
<ul>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867199855&amp;sid=32845">JavaScript 網頁設計師手札</a> 用 DOM 的角度來介紹 JavaScript ，值得一讀。</p>
</li>
<li>
<p><a href="http://www.oreilly.com.tw/product_web.php?id=a124">JavaScript 大全</a>
看完它才能算是瞭解 JavaScript 。<a href="http://www.oreilly.com.tw/product_web.php?id=a124">中文</a>目前為第 4 版，<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=0596101996&amp;sid=32500">原文</a>則已經到第 5 版了。不過天瓏網站竟然找不到了，可以找其他家書店看看。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9574423522&amp;sid=30354">Ajax 網頁程式設計─Google 成功背後的技術</a> 這是台灣第一本有完整實作的 Ajax 書籍。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794869&amp;sid=31332">Ajax 快速上手</a> 維持深入淺出系列的良好風格，個人建議沒碰過 Ajax 的人先看這本。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794842&amp;sid=31446">Ajax Hacks 駭客八十招</a> 介紹很多 Ajax 技術，好書一本！</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9789861810362&amp;sid=31801">Ajax 實戰手冊</a> 這本有介紹到實作必須考量的地方，值得參考。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=986779494X&amp;sid=34207">Ajax 設計模式</a> 把 Ajax 相關模式及需要注意的地方解釋得很清楚，必讀！</p>
</li>
</ul>
<h3 id="php">PHP</h3>
<ul>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9575279530&amp;sid=32684">PHP 5 徹底研究</a> PHP 5 有哪些東西？看這本書後就能掌握住七、八成了。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867198417&amp;sid=30123">究極 PHP：進階網路應用指南</a> 本書可以學習到很多 PHP 處理網路通訊的技術。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9574423131&amp;sid=28289">PHP Smarty 樣版引擎</a>  內舉不避親，本書主要透過 Smarty 來介紹團隊開發的方式。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9864218204&amp;sid=27385">PHP 精華工具集模組、擴充功能、效能加速</a> 本書介紹很多 PHP 工具與技術，值得一看。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867529677&amp;sid=23815">專業 PHP 5 程式設計指南</a>  目前我看過最好的 PHP 物件導向中文書籍，可惜買不到了。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867944704&amp;sid=15967">PHP 之戀</a> 天瓏竟然還有在賣，本書介紹很多你沒看過的 PHP 應用。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867910672&amp;sid=13233">PHP 程式設計專家必備手冊</a> 一大堆的 PHP 技巧，但是也買不到了，還好<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=0672323257&amp;sid=12096">原文</a>已經有第二版。</p>
</li>
</ul>
<h3 id="web">Web</h3>
<ul>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794176&amp;sid=17711">Apache 技術手冊</a> 安裝與設定 Apache 必看。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794311&amp;sid=21291">Apache 錦囊妙技</a> 傳授許多 Apache 的秘技，設定 Apache 必看。</p>
</li>
<li>
<p><a href="http://www.books.com.tw/exep/prod/booksfile.php?item=0010336375">Web Site 錦囊妙計</a> 架站必讀好書，不過天瓏不知道為什麼沒有？而且竟然還是上奇出版的歐萊禮書籍&hellip;出版界真是令人摸不透呀&hellip;</p>
</li>
</ul>
<h3 id="design">Design</h3>
<ul>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867199499&amp;sid=29566">如何設計好網站</a> 網站不是漂亮就好了，這本書教你如何從使用者的角度去設計網站。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=986779480X&amp;sid=30859">操作介面設計模式</a> 現行的軟體介面有哪些呢？本書整理了許多令人讚嘆的介面設計。</p>
</li>
</ul>
<h3 id="programming">Programming</h3>
<ul>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794524&amp;sid=32306">深入淺出設計模式</a> 設計模式是什麼？這本書看完後包你一清二楚。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9572054023&amp;sid=8021">物件導向設計模式</a> 設計模式的經典名著，不過我建議先看看「深入淺出設計模式」後再回頭看這本。另外還有<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9572054112&amp;sid=10828">精裝本</a>，收藏者必備。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9575278356&amp;sid=26836">Design Patterns 於 Java 語言上的實習應用</a> 用許多 Java 實例來介紹設計模式，可以補足前面兩本設計模式書籍的實作觀念。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867594061&amp;sid=17667">重構 - 改善既有程式的設計</a> 介紹許多簡單的重構手法，可以幫助我們把程式重新修改成較易維護的架構，必讀！</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794702&amp;sid=29190">軟體預先架構之美學</a> 和重構有異曲同工之妙，主要是利用經驗法則來避免掉後續過多的重構動作，值得參考。</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867910311&amp;sid=12245">極致軟體製程</a> XP 大師著作，包含許多有趣的軟體開發方法，值得一讀！</p>
</li>
<li>
<p><a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9861541489&amp;sid=26120">敏捷軟體開發：原則、樣式及實務</a> 想走物件導向軟體開發必讀書籍！</p>
</li>
<li>
<p><a href="http://www.oreilly.com.tw/product2_java.php?id=a210">深入淺出物件導向分析與設計</a>如果覺得市面上的 OO 分析書籍太過於艱深的話，這本入門書絕對值得一讀！</p>
</li>
</ul>
<p>當然一定還有其他中文的好書，只是我力有未逮無法一一拜讀；這裡也歡迎大家一起推薦自己心目中的好書，讓彼此的功力進步再進步。</p>
]]></content>
		</item>
		
		<item>
			<title>透視 WebMVC</title>
			<link>https://jaceju.net/web-mvc/</link>
			<pubDate>Fri, 16 Mar 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/web-mvc/</guid>
			<description>前言 以往我所開發的 Web 專案，大部份都是把核心放在操作 HTML ；就算後來使用了 Smarty ，卻還是迷失在視覺為重的設計觀點裡，使得後續開發與維護都變得非常麻煩。</description>
			<content type="html"><![CDATA[<h2 id="前言">前言</h2>
<p>以往我所開發的 Web 專案，大部份都是把核心放在操作 HTML ；就算後來使用了 Smarty ，卻還是迷失在視覺為重的設計觀點裡，使得後續開發與維護都變得非常麻煩。後來我自己歸納出問題發生的原因，絕大部份在於我接觸的專案常常是「畫面先行」。</p>
<p>「畫面先行」是由視覺設計人員來主導專案的架構，而這個架構則通常是因應客戶在網站流程上的要求而建立的；這也使得我在開發程式時就必須地採用他們決定好的頁面來套用程式，最後的結果就是導致重複的業務邏輯遍布在整個專案裡。</p>
<p><img src="/resources/webmvc/images/smarty_core.png" alt="以 Smarty 為核心"></p>
<p>不過就算使用了 Smarty ，這種問題也還是沒有得到更進一步的解決。原因就在於我只是把 PHP 程式和 HTML 頁面拆開，而實際上同樣的業務邏輯卻沒有能夠包裝起來重複使用。直到我接觸了物件導向的觀念後，我才驚覺這種設計真的是不堪一擊。</p>
<p>於是我很認真地使用物件導向來封裝我的業務邏輯，並天真地以為讓它們能夠 reuse 後，程式的複雜度就會降低；但是逐漸地我發現到，我花在修改這些物件的時間竟然變得比以前長，我必須不斷地為它加入不同的判斷條件，好讓它應付所有可能發生的變化。可是這不是我所希望發生的結果呀！到底為什麼會變成這樣呢？</p>
<p>我發現很多時候我讓物件在發生錯誤時，自行丟出一個訊息給瀏覽器，或是透過 Smarty 將這些訊息包裝成畫面。但是過了幾個月之後的改版，這個物件的重用性變得越來越差，而且也變得越來越大。另外它跟 Smarty 的耦合性也太高，畢竟有時候我根本不想在這個時候使用 Smarty 丟出畫面，而是想讓上一層程式自己去決定！</p>
<p>後來我歸納出這一切問題的原因，都是我錯把 Smarty 當成主角 - 也就是網站架構的核心！</p>
<p>剛接觸 Smarty 時，我同時也耳聞了 MVC 這個設計模式。不過那時剛剛接觸物件導向，也還不瞭解什麼是設計模式；只知道使用 Smarty 的前輩們倡導大型的應用項目都應該朝向 MVC 的架構去設計。雖然我那時仍不清楚 MVC 的核心概念，然而這個開發方式卻帶給我一個很大的思考空間：為什麼要把一個應用程式分成三個部份？而在 Web 上這種開發方式也行得通嗎？在參考過一些書籍，這些問題仍似有若無的存在我的心中，所以我的程式架構依然沒有什麼改進。</p>
<p>直到在寫「 <a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9574423131">PHP Smarty 樣版引擎</a>」這本書時，我也剛好讀到了「<a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9867794524">深入淺出設計模式</a>」這本好書；而該書對 MVC 模式的介紹，正好解開我長年以來的疑惑。</p>
<p>原來真正的 MVC 是將「資料處理邏輯」、「程式流程」與「資料呈現」三者分離，而 Smarty 只是扮演了「資料呈現」的角色而已！</p>
<p>不過懂是懂了，但要如何將這個概念融入 Web 開發中，這點讓我覺得很頭痛。雖然發現很早就<a href="http://www.phpmvc.net/">有人</a>把 MVC 帶進 PHP 裡了，只可惜用的人不多，文件也比較少。所以在我的書中的 MVC 架構用得非常簡陋，只能說是 WebMVC 的雛形而已。而這時 Ruby on Rails 剛好在網路界萌芽，號稱將 MVC 帶入了 Web 開發中；並且首開先例，用影片來示範一個 Blog 系統的誕生，所以我也不可免俗地去抓回來玩看看 (我想很多寫 Java 的人大概就是這點所吸引) 。可惜我一看到 Ruby 那個奇怪的語法，就覺得看不下去了，所以只是照本宣科地把範例作一作，最後不了了之 (後來想一想真是可惜) 。</p>
<p>直到又過了好一陣子，在 CakePHP 、 Code Igniter 等號稱 PHP 界的 Rails 開發框架出現後，我便興致勃勃地去下載回來研究。而參考了它們的範例後，這時我才猛然醒悟！原來 Web 上的 MVC 架構是長成這個樣子！後來為了專案需要，更把 LifeType 的程式碼追了一遍，這才發現 WebMVC 竟是如此彈性而富有變化！</p>
<p>由於深刻體認到物件導向所帶給我的衝擊，我不得不承認以往我所嚮往的開發方式仍然有很大的改進空間。所以我想就從架構的改進開始，讓自己重頭去認識新的 Web 開發方式。</p>
<h2 id="傳統的程式架構">傳統的程式架構</h2>
<p>以往我所見到的程式架構，大多都是一個功能一個頁面。就拿留言板來說好了，以前的我大概會這樣規劃：</p>
<ul>
<li>
<p>index.php (留言列表頁)</p>
</li>
<li>
<p>add.php (新增留言表單)</p>
</li>
<li>
<p>do_add.php (處理新增留言)</p>
</li>
<li>
<p>rss.php (假設這個留言板有提供 RSS 服務)</p>
</li>
</ul>
<p>以上的規劃方式很明顯地是一個功能一支程式，這在小項目裡面很常見。為了探討這種架構的優缺點，以下我便採用這種方式來實作一個傳統的留言板。</p>
<p>註：當然也可以將它們全部放在同一支程式中處理掉，不過我還是暫時不要變得那麼「聰明」。</p>
<h3 id="程式碼">程式碼</h3>
<p>先以 index.php 來說，以下是一個簡單的實作：</p>
<pre><code>&lt;?php
// 載入設定
require_once 'config.php';
// 從檔案中取得所有留言
$messages = (file_exists(APP_STORAGE))
          ? unserialize(file_get_contents(APP_STORAGE))
          : array ();
// 輸出 HTML
header('Content-Type: text/html; charset=utf-8');
?&gt;&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot;
&quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;
&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;
&lt;head&gt;
.... (略) ....
&lt;div id=&quot;content&quot;&gt;
&lt;?php if (count($messages)): ?&gt;
&lt;?php foreach ($messages as $id =&gt; $message): ?&gt;
&lt;div class=&quot;messageBlock&quot;&gt;
&lt;h2&gt;&lt;?php echo $message['title']; ?&gt;&lt;/h2&gt;
&lt;p&gt;&lt;?php echo nl2br($message['body']); ?&gt;&lt;/p&gt;
&lt;p class=&quot;messageBlockFunctions&quot;&gt;By &lt;?php echo $message['author']; ?&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;?php endforeach; ?&gt;
&lt;?php else: ?&gt;
&lt;p&gt;沒有任何文章&lt;/p&gt;
&lt;?php endif; ?&gt;
&lt;/div&gt;
.... (略) ....
&lt;/body&gt;
&lt;/html&gt;

</code></pre><p><img src="/resources/webmvc/images/php_html.png" alt="PHP with HTML"></p>
<p>主要在這裡我是利用文字檔來存放留言內容，然後利用程式取得留言後再將它們輸到到 HTML 。這樣的寫法把留言版的處理邏輯和 HTML 混在一起，是 PHP 常見的開發方式。</p>
<p>而 add.php 純粹是表單頁，這裡我把它略過。接下來是我在 do_add.php 上的實作：</p>
<pre><code>&lt;?php
// 載入設定
require_once 'config.php';
// 取得表單變數
$title = $_POST['title'];
$author = $_POST['author'];
$body = $_POST['body'];
// 從檔案中取得所有留言
$messages = (file_exists(APP_STORAGE))
          ? unserialize(file_get_contents(APP_STORAGE))
          : array ();
// 在所有留言的最後加上一筆
$messages[] = array (
    'id'     =&gt; count($messages) + 1,
    'title'  =&gt; $title,
    'author' =&gt; $author,
    'body'   =&gt; $body,
);
// 將所有留言寫回檔案
file_put_contents(APP_STORAGE, serialize($messages));
// 導回列表頁
header('Location: ./');

</code></pre><p>看起來 do_add.php 做的事多了一點，不過我想還不至於太困難。這裡的 do_add.php 沒有介面可讓使用者操作，像這樣的程式通常是為了處理表單變數而存在的。</p>
<p>註：以上用檔案存取留言的方式是為了簡單起見，所以寫得非常偷懶，我想不值得學習。</p>
<p>最後來看看 rss.php 的實作：</p>
<pre><code>&lt;?php
// 載入設定
require_once 'config.php';
// 從檔案中取得所有留言
$messages = (file_exists(APP_STORAGE))
          ? unserialize(file_get_contents(APP_STORAGE))
          : array ();
// 組合 XML
$xml = '';
$xml .= '&lt;' . '?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?' . '&gt;' . &quot;\n&quot;;
$xml .= '&lt;rss version=&quot;2.0&quot;&gt;' . &quot;\n&quot;;
$xml .= '&lt;channel&gt;' . &quot;\n&quot;;
$xml .= '&lt;title&gt;TEST&lt;/title&gt;' . &quot;\n&quot;;
$xml .= '&lt;description&gt;TEST&lt;/description&gt;' . &quot;\n&quot;;
foreach ($messages as $message)
{
    $xml .= '&lt;item&gt;' . &quot;\n&quot;;
    $xml .= '&lt;title&gt;' . $message['title'] . '&lt;/title&gt;' . &quot;\n&quot;;
    $xml .= '&lt;description&gt;' . $message['body'] . '&lt;/description&gt;' . &quot;\n&quot;;
    $xml .= '&lt;/item&gt;' . &quot;\n&quot;;
}
$xml .= '&lt;/channel&gt;' . &quot;\n&quot;;
$xml .= '&lt;/rss&gt;';
// 輸出 XML
header('Content-Type: text/xml; charset=utf-8');
echo $xml;

</code></pre><p>這裡我遵循最簡單的規範，把留言內容輸出成可供讀取的 RSS 格式。</p>
<p>上面的程式其實花不到我幾分鐘，倒是 CSS 的部份用的時間長了點 (大概半小時) ，這也說明 PHP 在小型應用程式上的開發效率是很高的 (也許是因為我用了偷懶的方式 :P ) 。然而我捫心自問：如果今天沿用這樣的架構來開發大型專案的話，那麼對團隊合作會有什麼樣的影響呢？</p>
<h3 id="面臨的問題">面臨的問題</h3>
<p>我先簡單把上面幾支程式拆解開來分析，便發現以下的通則：</p>
<ul>
<li>
<p>如果需要的話則取得表單變數 (do_add.php) 。</p>
</li>
<li>
<p>存取資料並透過相關邏輯處理 (index.php 、 rss.php 、 do_add.php) 。</p>
</li>
<li>
<p>輸出資訊給瀏覽器或其他外部程式 (index.php 、 rss.php 、 add.php) 。</p>
</li>
</ul>
<p>註：雖然 do_add.php 的 header(&lsquo;Location&rsquo;) 指令也是會傳送 HTTP 的 Header 資訊給瀏覽器，但是我把它視為流程控制而非資料呈現。</p>
<p>我手邊在維護的大部份 PHP 程式都是以類似的流程所寫成的，當然這樣寫是非常直覺的。但是就拿上面留言板的例子來說好了，我就發現這樣的架構會帶來以下幾個主要的缺點：</p>
<ul>
<li>如果我想將文字檔換成正式的資料庫系統時，我得同時更改三支程式檔。</li>
<li>如果在多人開發環境下，程式開發沒辦法和視覺設計並行；如果頁面風格要更改的話可能會更慘。</li>
<li>重複的程式碼太多 (例如讀取資料部份) ，一旦發現錯誤，必須把所有相關的程式找出來一個一個修改。</li>
</ul>
<p>雖然在留言板這種小項目中，以上的問題並不會影響太大；可是我所維護或開發的應用程式通常都會包含數十支甚至上百支的頁面，這時候上面的問題就會隨著頁面數量而呈現等比級數的成長。</p>
<p>我曾在「 <a href="http://tlsj.tenlong.com.tw/WebModule/BookSearch/bookSearchViewAction.do?isbn=9574423131">PHP Smarty 樣版引擎</a>」提過，網站開發上絕對不是只有一個人的事情；除了專案企劃人員的參與外，程式開發人員及視覺設計人員之間的協調更是重要。如果一個架構不能同時讓這兩方人馬有良好的互動，那麼這個架構就不值得團隊採用。</p>
<p>當然我可以在這裡導入樣版引擎 (例如 Smarty ) ，然而這樣依舊無法有效解決後續維護上的問題；而且我先前也過於依賴以樣版引擎為主幹的開發方式，而沒有認真去思考如何把相似的部份整合在一起。因此若是要一次解決以上的問題，我便得好好思考如何真正有效去切割並重組這樣的程式架構。</p>
<h2 id="轉換思考模式">轉換思考模式</h2>
<p>在前面的程式裡面，除了 PHP 和 HTML 看起來是還算有明顯的區隔，其他部份其實還滿不容易看出它們之間的關係。在遇到這樣的問題時，我個人非常喜歡使用圖形化的思考模式，這會有助於對程式整體架構的分析，所以我便把以上四支程式所做的事情圖形化。在圖形化之後，我發現每支程式中都有相關的部份；因此我就把相關的功能對齊，如下圖所示：</p>
<p><img src="/resources/webmvc/images/transition.png" alt="傳統架構"></p>
<p>現在整個程式架構相當清楚了，像是讀取留言或新增留言都是留言板的核心功能，已經對齊在同一個水平上；而輸出資訊的部份也是一樣，不論輸出格式是 HTML 還是 RSS ，都可以一視同仁。然而比較特別的是，我決定把頁面流程也看成是相類似的功能集合，換句話說就是淺藍色的部份也算是相關的。</p>
<p>接著我把這些重複或相似的部份沿著虛線將它們切割開來並重新群組在一起：</p>
<p><img src="/resources/webmvc/images/group.png" alt="轉換成MVC"></p>
<p>現在我有留言板的核心部份 (也就是取得所有留言以及新增留言) 、兩種輸出的形態 (HTML 與 RSS ) 以及相關的頁面動作 (index 、 add 、 do_add 、 rss) 三個群組，看起來好像很棒，但是有什麼用呢？</p>
<p>回想一下前面的問題，首先我遇到了資料庫會改變這個事實，不過看來我只需要把 Guestbook 這個群組的實作方式抽換掉就可以解決。當然抽換的過程中也不會影響到其他兩個群組的運作，因為我已經將留言板的核心部份排除在頁面之外了。換句話說，不論我的留言訊息是以檔案存放或是使用資料庫存放，只要 Guestbook 群組提供相同的操作介面，都能讓其他程式要更動的部份減至最少。</p>
<p>而第二個問題我可以在 Output 群組中找到解答，因為這裡我只需要導入樣版的概念，那麼 HTML 或是 RSS 都可以獨立在程式之外。而且 HTML 的部份我可以搭配 Smarty 或其他樣版引擎，也可以使用單純的 PHP 套版；而 RSS 的部份除了上面直接使用字串來輸出的方式外，我也可以改用其他方便的第三方類別庫來協助產生。這樣一來不僅解決協同開發的問題，還能夠讓專案選用適當的方法來產生輸出。</p>
<p>最後我再把頁面的流程放在 Actions 群組裡，也就是說我可以把重複的部份提煉出來放在一起。而這樣的調整就能使我在增加新功能或是修正某些錯誤時，能夠避免相關的程式碼重複；而且也能將相關功能放在同一個群組下以便管理。</p>
<h3 id="程式碼-1">程式碼</h3>
<p>上面的理論雖然感覺起來很棒，但是還缺少實務的支持，所以我必須把這些群組變成程式碼。不過在實務上，「群組」並不是真正能夠實作的東西，所以我想這時就應該引進物件導向的觀念了。因為前面我把相類似的功能放在一起，所以它們就能形成一個物件導向中的類別。換句話說 Guestbook 是一個類別，而 Output 和 Actions 也是一樣，所以在新的架構裡我就可以使用類別來實作程式。然而事實上我只是先把舊的程式碼按照上面的邏輯重新組合在類別代碼裡，並求能正常執行。</p>
<p>註：事實上並非將相似功能放在一起就能夠轉換成類別，我個人只是為了簡化思考，才會採用這樣的概念。</p>
<p>首先是 Guestbook.php ，它的類別代碼如下：</p>
<pre><code>&lt;?php
// 留言版類別
class Guestbook
{
    // 留言
    private $messages;
    // 建構函式
    // 預先從檔案中取得所有留言
    public function __construct()
    {
        $this-&gt;messages = (file_exists(APP_STORAGE))
                        ? unserialize(file_get_contents(APP_STORAGE))
                        : array ();
    }
    // 取得所有留言
    public function getAllMessages()
    {
        return $this-&gt;messages;
    }

    // 新增留言
    // 在所有留言的最後加上一筆後寫回檔案
    public function addMessage($title, $author, $body)
    {
        $this-&gt;messages[] = array (
            'id'     =&gt; count($this-&gt;messages) + 1,
            'title'  =&gt; $title,
            'author' =&gt; $author,
            'body'   =&gt; $body,
        );
        file_put_contents(APP_STORAGE, serialize($this-&gt;messages));
    }
    // 解構函式
    public function __destruct()
    {
        $this-&gt;messages = NULL;
    }
}

</code></pre><p>在 Guestbook 類別裡，我把讀取留言 (getAllMessages) 和新增留言 (addMessage) 的程式變成類別方法，在實作上還是採用原來檔案存取的方式。在這裡 getAllMessages 和 addMessage 兩個方法，就是供外部呼叫用的統一介面。如果之後如果我需要更換成資料庫，那麼就可以只更改這些方法的實作即可。</p>
<p>接下來是 Output.php ，它的類別代碼如下：</p>
<pre><code>&lt;?php
// 輸出類別
class Output
{
    // 輸出類型
    private $type;
    // 樣版變數
	private $vars = array ();
    // 建構函式
    public function __construct($type)
    {
        $this-&gt;type = in_array($type, array('HTML', 'RSS'))
                    ? $type
                    : 'HTML';
    }
    // 設定樣版變數
    // 這裡不用 __set 是因為我不想在 PHP 程式裡直接對物件指定屬性
    public function setVar($tpl_var, $value = null)
    {
        if (is_array($tpl_var))
        {
            foreach ($tpl_var as $key =&gt; $val)
            {
                if ($key != '')
                {
                    $this-&gt;vars[$key] = $val;
                }
            }
        }
        else
        {
            if ($tpl_var != '')
            {
                $this-&gt;vars[$tpl_var] = $value;
            }
        }
    }
    // 自動取得對應的樣版變數
    // 在樣版裡直接取得變數會比較方便
	private function __get($name)
	{
		return isset($this-&gt;vars[$name]) ? $this-&gt;vars[$name] : NULL;
	}
    // 依照類型輸出結果
    // 輸出前先用 header 函式讓瀏覽器得知正確的輸出型態與編碼
    public function render($template_file = '')
    {
        if ('HTML' == $this-&gt;type)
        {
            // 輸出 HTML
            header('Content-Type: text/html; charset=utf-8');
            echo $this-&gt;fetchHTML('templates/' . $template_file);
        }
        else
        {
            // 輸出 RSS
            header('Content-Type: text/xml; charset=utf-8');
            echo $this-&gt;fetchRSS();
        }
    }
    // 擷取 HTML 結果
    // 透過 Output Buffer 來擷取結果，這樣方便視覺套版
    private function fetchHTML($template_file)
    {
        $html = '';
        ob_start();
        include $template_file;
        $html = ob_get_contents();
        ob_end_clean();
        return $html;
    }

    // 擷取 RSS 結果
    // 因為 RSS 格式固定，所以直接使用字串串接
    private function fetchRSS()
    {
        $xml = '';
        $xml .= '&lt;' . '?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?' . '&gt;' . &quot;\n&quot;;
        $xml .= '&lt;rss version=&quot;2.0&quot;&gt;';
        $xml .= '&lt;channel&gt;';
        $xml .= '&lt;title&gt;TEST&lt;/title&gt;';
        $xml .= '&lt;description&gt;TEST&lt;/description&gt;';
        foreach ($this-&gt;items as $item)
        {
            $xml .= '&lt;item&gt;';
            $xml .= '&lt;title&gt;' . $item['title'] . '&lt;/title&gt;';
            $xml .= '&lt;description&gt;' . $item['body'] . '&lt;/description&gt;';
            $xml .= '&lt;/item&gt;';
        }
        $xml .= '&lt;/channel&gt;';
        $xml .= '&lt;/rss&gt;';
        return $xml;
    }
}

</code></pre><p>我讓 Output 負責 HTML 和 RSS 兩種型態的輸出，並在建立物件時就決定輸出的型態。在 Output 類別裡的 HTML 輸出部份採用了樣版的概念，也就是取得 HTML 樣版並代入相關的變數，最後才取得代入後的結果。而 HTML 樣版自己會決定要輸出什麼，我只需要呼叫正確的樣版即可。不過值得注意的是 HTML 樣版的樣版變數，因為我在 Output 類別裡使用了 setVar() 方法來指定樣版變數，而以 __get() 這個魔術方法來取得變數內容；因此以列表頁的 HTML 樣版 (index.tpl.htm) 為例，原來的 $messages 就會變成 Output 物件的屬性，因此要將 $messages 改寫為 $this-&gt;messages ：</p>
<pre><code>&lt;?php if (count($this-&gt;messages)): ?&gt;
&lt;?php foreach ($this-&gt;messages as $id =&gt; $message): ?&gt;
&lt;div class=&quot;messageBlock&quot;&gt;
&lt;h2&gt;&lt;?php echo $message['title']; ?&gt;&lt;/h2&gt;
&lt;p&gt;&lt;?php echo nl2br($message['body']); ?&gt;&lt;/p&gt;
&lt;p class=&quot;messageBlockFunctions&quot;&gt;By &lt;?php echo $message['author']; ?&gt;&lt;/p&gt;

</code></pre><p>而 RSS 輸出的部份，我則是把原來的程式用類別方法包裝起來，當然 $items 也要改成 $this-&gt;items 。</p>
<p>以上的動作會由 fetchHTML 、 fetchRSS 及 render 三個方法來幫我完成；我只要在建立物件時指定好要輸出的型態， render 函式就會自行決定要呼叫 fetchHTML 還是 fetchRSS 。</p>
<p>然後是 Actions.php ，它的類別代碼如下：</p>
<pre><code>&lt;?php
// 動作類別
class Actions
{
    // 共用的留言版物件
    private $guestbook = NULL;
    // 使用者選擇的動作
    private $action = 'index';
    // 建構函式
    // 初始化要執行的動作以及留言板物件
    public function __construct()
    {
        $this-&gt;action = isset($_GET['act'])
                      ? strtolower($_GET['act'])
                      : 'index';
        $this-&gt;guestbook = new GuestBook;
    }
    // 執行選擇的動作
    public final function run()
    {
        $this-&gt;{$this-&gt;action}();
    }
    // 重新導向
    // 借用 header 函式來導向指定的網址
    private function redirectTo($url)
    {
        header('Location: ' . $url);
    }
    // 預設的列表功能
    // 等同於原來的 index.php
    private function index()
    {
        $output = new Output('HTML');
        $output-&gt;setVar('messages', $this-&gt;guestbook-&gt;getAllMessages());
        $output-&gt;render('index.tpl.php');
    }
    // 新增表單頁
    // 等同於原來的 add.php
    private function add()
    {
        $output = new Output('HTML');
        $output-&gt;setVar('action', 'doAdd');
        $output-&gt;render('edit.tpl.php');
    }
    // 新增留言
    // 等同於原來的 do_add.php
    private function doAdd()
    {
        $title = $_POST['title'];
        $author = $_POST['author'];
        $body = $_POST['body'];
        $this-&gt;guestbook-&gt;addMessage($title, $author, $body);
        $this-&gt;redirectTo('./');
    }
    // 輸出 RSS
    // 等同於原來的 rss.php
    private function rss()
    {
        $output = new Output('RSS');
        $output-&gt;setVar('items', $this-&gt;guestbook-&gt;getAllMessages());
        $output-&gt;render();
    }
    // 解構函式
    public function __destruct()
    {
        $this-&gt;guestbook = NULL;
    }
}

</code></pre><p>在 Actions 類別裡我把原來的頁面全部改成方法，每個方法我稱它為一個 Action 。在建構程式裡會解析 $_GET[&lsquo;act&rsquo;] 變數所擁有的內容當做 Action 的名稱，這樣外部程式會就可以透過網址參數 act 來決定要呼叫哪一個 Action 。當然如果沒有指定 Action 名稱的話，那麼預設就是 index (列表頁) 。</p>
<p>每個 Action 中會透過呼叫物件的方法 (也可說成傳遞訊息) ，來達成原來頁面裡的流程動作。換句話說 Actions 類別中的每個方法會去指導 Guestbook 該怎麼動作，例如取得所有留言或是新增留言；而如果需要輸出的話，就會透過 Output 物件的 setVar 來拉取 Guestbook 裡的資料，以產生適當格式的輸出。</p>
<p>比較特別的是在 Actions 類別裡的 run 這個方法，我會透過它來執行 act 網址參數所指定的 Action 。換句話說， run 方法是唯一讓外界操作 Actions 物件的介面。</p>
<p>不過要能夠正常運作這些流程的話，我想我還需要一個進入點 (Bootstrap) ，負責建立一個 Actions 物件來開始執行程式；當然這個工作就要交給 index.php ，程式碼改寫如下：</p>
<pre><code>&lt;?php
// 載入設定
require_once 'config.php';
// 自動載入類別
function __autoload($class_name)
{
    require_once $class_name . '.php';
}
// 執行對應的動作
$actions = new Actions;
$actions-&gt;run();

</code></pre><p>我發現改用新的架構後，不但 index.php 的程式變得簡單俐落許多，而且透過 PHP5 自動載入類別的機制，我也不需要在一開始就載入所有的類別檔案。</p>
<h3 id="執行流程">執行流程</h3>
<p><img src="/resources/webmvc/images/run.png" alt="執行"></p>
<p>光看上面程式的話，其實還滿不容易懂它的執行方式，我還是一步步來分析這個架構的流程好了。</p>
<p>以列表頁的流程為例，它的步驟如下：</p>
<ul>
<li>首先從 index.php 進入程式， index.php 會建立一個 Actions 物件。</li>
<li>在建立 Actions 物件的同時，因為沒有 act 這個網址參數，所以 Actions 物件的 action 屬性會預設為 index ；此時 Actions 也會同時在它的內部建立一個 Guestbook 物件。</li>
<li>Guestbook 物件建立的同時，會預先從檔案中取得所有留言。</li>
<li>回到 index.php ，這時會執行 Actions 物件的 run 方法；在 run 方法裡會執行 action 屬性所指向的 index 方法。</li>
<li>在 Actions 物件的 index 方法裡，會建立一個 HTML 型態的 Output 物件，並從剛剛建立的 Guestbook 物件裡抓出留言，再塞給 Output 物件。最後用 Output 物件的 render 方法來顯示樣版。</li>
<li>Output 物件會依照剛剛給的留言內容，一一代換樣版裡的樣版變數，最後呈現結果給瀏覽器。</li>
</ul>
<p>當然同樣的分析方法，也可以用在新增留言這個動作上面。雖然這樣的流程看起來有點囉嗦，不過我想這就是物件交互的方式，這大概也是初學者很容易搞混的地方。</p>
<p>依照以上的架構，如果我想加入新動作的話，就會變得很直覺。而且我不用把相似的邏輯散放在各個頁面裡，而是獨立在每個層級裡。每個層級可以交由不同的成員去設計或開發，甚至可以加入第三方的函式庫，一切看起來是這麼的美好！</p>
<h3 id="還是有缺點">還是有缺點</h3>
<p>雖然我覺得上面的架構已經很不錯了，但是從我以前的經驗來看，這樣的實作方式還是有些許的缺點。</p>
<!-- raw HTML omitted -->
<p>「不知道要從哪裡開始看懂程式碼！」這是我剛接觸到新架構的感想。其實我在一開始並沒有透過上面的分析來推導出這個架構，而是用蠻幹的方式加上似懂非懂的理論背景去死讀別人的框架原始碼。我身邊有些開發者大多也是遇到了同樣的問題，以致每個人所理解出來的架構都有些許不同。</p>
<p>另外因為新的架構是建構在物件導向的基礎上，所以我如果沒有物件導向的相關知識或經驗的話，也很難想像這樣的架構會如何執行。時下很多框架也是如此，如果初學者一開始沒有正確的觀念，直接照本宣科地把範例做完，可能還是會難以理解框架採用這種架構的好處。</p>
<p>註：架構與框架在這裡我認為是不一樣的概念；架構是指理論上程式交互的方式，並定義出明確而有層次的規範；而框架則是帶有實作，且可以重複利用的程式架構。</p>
<!-- raw HTML omitted -->
<p>傳統架構的新增留言表單頁原本只要是一個靜態頁面，但是現在改用樣版技術而造成了些許的效能耗損。然而這點我個人認為是有利有弊，因為有時候我會需要統一化的版型，也就是所謂的 Master Page 。利用樣版技術可以達到這個目標，也是靜態頁所無法提供的。因此用一點點的效能來換取較有彈性的架構，我個人認為是值得的。</p>
<!-- raw HTML omitted -->
<p>從 HTML 樣版和 Output 類別的 fetchRSS 實作中可以看得出來，在輸出留言的部份是以陣列元素的方式去獲取留言裡的內容。這表示其實 Output 和 Guestbook 還是有密切的關係，也就是所謂的高耦合；高耦合表示兩個類別無法獨立使用，這在物件導向原則裡是需要盡可能避免的。</p>
<p>不過事實上這個問題也必須考量到專案的大小，像留言版這種等級的應用項目，目前這樣的耦合程度還可以令人接受。</p>
<!-- raw HTML omitted -->
<p>改用新架構後，現在入口只會剩下 index.php ；不過這樣一來，原來的網址就得跟著改變。換句話說，原來如下的網址：</p>
<pre><code>http://localhost/webmvc/index.php
http://localhost/webmvc/add.php
http://localhost/webmvc/do_add.php
http://localhost/webmvc/rss.php

</code></pre><p>就要改為：</p>
<pre><code>http://localhost/webmvc/index.php (不變)
http://localhost/webmvc/index.php?act=add
http://localhost/webmvc/index.php?act=doAdd
http://localhost/webmvc/index.php?act=rss

</code></pre><p>有一些文章指出帶有參數的網址不利人類記憶，而且在 SEO (搜尋引擎最佳化) 上也會有不良的影響。當然我想如果要改善開發效率的話，就勢必要有一點犧牲；不過還好這方面的困擾還能透過網址重寫技術 (URL Rewriting) 來彌補，因此不算是太嚴重的問題。</p>
<!-- raw HTML omitted -->
<p>採用新架構之後的留言版，我想應該已經比原來的架構更具有彈性，不過如果今天我有多個專案都想使用同樣的架構時該怎麼辦呢？難道裡面的程式碼我還得再重寫一次嗎？或是複製現在這個程式然後做修改呢？</p>
<p>我想答案應該是否定的，因為所謂的框架不是把原來的已經穩定的功能變成另一個專案的垃圾。在我的經驗裡除了同性質的活動只需要更改樣版以外，鮮少有兩個專案的功能是會長得一模一樣；換句話說，如果只是用複製並修改現有程式的方式來開發，那麼可能就有碰到程式炸彈的危險性。減少甚至去除原有客製化的功能，保留基本所需的要素，我想這樣的框架才能夠擁有最大的彈性。</p>
<h2 id="建立基本-mvc-框架">建立基本 MVC 框架</h2>
<p>從上面改良過的留言版來看，似乎有些功能是可以讓往後的專案重複使用的；但是這些重複的部份，依舊參雜在每個獨立的類別裡。所以我決定把這些功能抽離出來，獨立成一個簡易的框架；但在此之前，我想我有必要先為自己釐清前面架構在 MVC 上的關係。</p>
<p>在「深入淺出設計模式」裡，對 MVC 三個角色的描述如下：</p>
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted -->Model<!-- raw HTML omitted --> - 持有資料、狀態、程式邏輯，並提供介面供人取得資料與狀態。</p>
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted -->View<!-- raw HTML omitted --> - 用來呈現 Model 中的資料與狀態。</p>
<!-- raw HTML omitted -->
<p><!-- raw HTML omitted -->Controller<!-- raw HTML omitted --> - 取得使用者的輸入後，並解讀此輸入以轉換成 Model 對應的動作。</p>
<!-- raw HTML omitted -->
<p>很清楚地，我發現前面的架構已經呈現出 MVC 的基本雛形了，也就是 Guestbook 、 Output 及 Actions 其實可以一一對應到 Model 、 View 和 Controller 。</p>
<p><img src="/resources/webmvc/images/pre_to_mvc.png" alt="轉換成MVC"></p>
<p>為什麼 Guestbook 是位於 Model 的層級呢？因為我想 Guestbook 這個類別主要是在操作留言訊息的存取，換句話說它掌控了資料與狀態。而且 getAllMessages 和 addMessage 就是 Guestbook 類別對外的介面，所以它就非常符合 Model 的描述。</p>
<p>而在 View 的層級來說，仔細研究桌面應用軟體的 MVC 實作就可以發現，大部份程式是利用視覺元件來組合出讓使用者操作的畫面。在 Output 類別裡雖然我是使用 Template 的作法，不過這很適合 Web 平台的設計與實作。因為要能夠完全以物件來建構畫面，對 HTML 來說是很不方便也非常不直覺；而且如果要能夠快速套用視覺設計人員所製作出來的範本，樣版引擎就會是一個比較好的解決方案。</p>
<p>對應到 Controller 層級的 Actions 類別，所謂的使用者輸入就可以看成是 act 這個網址參數所代表的動作名稱。在 Actions 物件裡會自動轉換這個參數，以呼叫正確的動作來執行。而 Controller 也必須明確地指示 Model 與 View 兩者間的互動方式，而在 Actions 類別裡的確也是這麼處理的。</p>
<p>知道了這層關係之後，我就可以想想哪些部份是可以提煉出來反覆利用的。</p>
<h3 id="model">Model</h3>
<p>從 Guestbook 的程式來看，事實上我可以完全把檔案操作從裡面獨立出來，然後只定義 Guestbook 的抽象化操作方式。這樣一來我可以直接以參數定義我想用的儲存方式，讓儲存用的類別也變成共用的工具之一。</p>
<p>可是在這裡我決定不這麼做。</p>
<p>為什麼呢？雖然我很明白抽象化的重要性，不過也不能任何事物件做抽象化，因為這樣會陷入「過度設計」的泥淖中。在 Guestbook 這種等級的應用項目裡，我面對的是很簡單的檔案操作，就算有更動也不至於影響其他程式。換句話說，如果今天真的有需要切換搭配的儲存方式時，我還是可以透過重構 Guestbook 來因應環境上的變化。</p>
<p>當然如果今天面對的是大型的應用項目，而且已經確定有重複或相似的變化時，那麼一開始就使用抽象化的設計就會是比較好的選擇。總之，在設計時期就必須要考量專案的大小與變化，以採取不同的方式來因應。</p>
<p>因此這裡我將保留 Guestbook 類別的實作方式，另外留言項目採用陣列輸出的方法也將加以保留。</p>
<h3 id="view">View</h3>
<p>不同於 Guestbook ，我發現 Output 分成了 HTML 和 RSS 兩種輸出格式；而且在未來可能會套用 AJAX 的狀況下，資料格式也可能加入 JSON 。因此 Output 就需要抽象化，將共用的部份提煉出來，架構圖如下：</p>
<p><img src="/resources/webmvc/images/to_view.png" alt="轉換成 View"></p>
<!-- raw HTML omitted -->
<p>那些部份需要被抽離出來呢？從原來的 Output 類別中，我發現 setVar 和 __get 這兩個方法和輸出格式無關，它們的實作我想就可以推到上一層去。至於 render 方法則會因為輸出格式而有不同的實作，那我就把它們變成抽象方法，並且把實作放在子類別裡。比較特別的是 fetchHTML 和 fetchRSS 兩個方法，因為到時候會有子類別來加以區分實作方式；所以這邊我就改用 fetch 這個抽象名稱，讓 render 方法不必為了輸出型態而個別呼叫。</p>
<p>至於抽出來的部份，我想我就用較正式的名稱 View 來做為類別名。所以 View.php 程式如下：</p>
<pre><code>&lt;?php
// 抽象視圖類別
abstract class View
{
    // 樣版變數
    protected $vars = array ();
    // 設定樣版變數
    public function setVar($tpl_var, $value = null)
    {
        if (is_array($tpl_var))
        {
            foreach ($tpl_var as $key =&gt; $val)
            {
                if ($key != '')
                {
                    $this-&gt;vars[$key] = $val;
                }
            }
        }
        else
        {
            if ($tpl_var != '')
            {
                $this-&gt;vars[$tpl_var] = $value;
            }
        }
    }
    // 自動取得對應的樣版變數
    private function __get($name)
    {
        return isset($this-&gt;vars[$name]) ? $this-&gt;vars[$name] : NULL;
    }
    // 抽象：擷取結果
    public abstract function fetch();
    // 抽象：輸出結果
    public abstract function render();
}

</code></pre><p>既然已經將共用的函式抽離出來了，子類別就可以用繼承的方式來實作。</p>
<!-- raw HTML omitted -->
<p>首先是用來輸出 HTML 格式內容的子類別，這裡我把原來在 Output 類別中屬於 HTML 的部份放到它身上。而這個子類別因為是 View 的一種，只不過輸出格式為 HTML ，所以它的名稱就以 HtmlView 來表示。以下就是 HtmlView.php 的內容：</p>
<pre><code>&lt;?php
// 輸出 HTML 格式內容
class HtmlView extends View
{
    // 取得樣版並解析
    public function fetch()
    {
        $args = func_get_args();
        $template_filename = $args[0];
        $html = '';
        ob_start();
        include 'templates/' . $template_filename;
        $html = ob_get_contents();
        ob_end_clean();
        return $html;
    }
    // 輸出
    public function render()
    {
        // 因為 View 類別的 render 函式沒有參數
        // 所以 render 要自行取得
        $args = func_get_args();
        $template_filename = $args[0];
        header('Content-Type: text/html; charset=utf-8');
        echo $this-&gt;fetch($template_filename);
    }
}

</code></pre><p>而 RSS 輸出格式也比照辦理：</p>
<pre><code>&lt;?php
// 輸出 RSS 格式內容
class RssView extends View
{

	// 轉換成 RSS 內容
    public function fetch()
    {
        $xml = '';
        $xml .= '&lt;' . '?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?' . '&gt;' . &quot;\n&quot;;
        $xml .= '&lt;rss version=&quot;2.0&quot;&gt;';
        $xml .= '&lt;channel&gt;';
        $xml .= '&lt;title&gt;TEST&lt;/title&gt;';
        $xml .= '&lt;description&gt;TEST&lt;/description&gt;';
        foreach ($this-&gt;items as $item)
        {
            $xml .= '&lt;item&gt;';
            $xml .= '&lt;title&gt;' . $item['title'] . '&lt;/title&gt;';
            $xml .= '&lt;description&gt;' . $item['body'] . '&lt;/description&gt;';
            $xml .= '&lt;/item&gt;';
        }
        $xml .= '&lt;/channel&gt;';
        $xml .= '&lt;/rss&gt;';
        return $xml;
    }
    // 輸出
    public function render()
    {
        header('Content-Type: text/xml; charset=utf-8');
        echo $this-&gt;fetch();
    }
}

</code></pre><p>現在 View 的部份已經完成了抽象化，未來如果要加入新的輸出格式，我只需要繼承 View 這個抽象類別，再實作 fetch 和 render 兩個方法即可。</p>
<h3 id="controller">Controller</h3>
<p>Actions 本身並沒有對應到多種格式的問題，不過它還是有些部份是一些應用程式常會需要用到的；換句話說，我可以把與留言版無關的部份抽離出來。</p>
<!-- raw HTML omitted -->
<p>仔細觀察 Actions 類別的實作，我發現可以抽離的部份有 run 、 redirectTo 兩個方法，所以我決定把這些部份抽離到新的 Controller 類別裡。另外我也希望預設的 index 方法 (動作) 一定要被子類別實作，所以我也把它變成抽象類別裡的抽象方法。</p>
<pre><code>&lt;?php
// 抽象控制類別
abstract class Controller
{
    // 動作名稱
    protected $action = '';
    // 抽象：預設動作
    protected abstract function index();
    // 執行動作
    public final function run()
    {
        $this-&gt;{$this-&gt;action}();
    }
    // 頁面重導向
    protected function redirectTo($url)
    {
        header('Location: ' . $url);
    }
}

</code></pre><p>然而在 Actions 類別的建構式中有段程式很特別，那就是解析 $_GET[&lsquo;act&rsquo;] 的部份，它似乎也是開發新的應用程式會使用到的功能。可是先前我自己說過使用 GET 參數的方式，會讓網址不容易讓瀏覽者記憶，因此在新專案裡我想使用網址重寫的技術。但這又有另一個問題，因為現存的這個留言板專案已經提供瀏覽者固定的 RSS 位址，所以想更動原有解析 $_GET[&lsquo;act&rsquo;] 的部份也變得不可行。</p>
<p>那麼有沒有什麼方法可以讓不同的專案使用不同的動作解析？而且還能不更動到抽象的 Controller 類別呢？</p>
<p>註：在 IIS 上，網址重寫技術也必須依賴其他 ISAPI 擴展才能實現。所以預設的情況下，還是得靠 GET 參數來提供 action 的名稱。</p>
<!-- raw HTML omitted -->
<p>如果說我把解析網址的動作抽離到一個獨立的類別裡，讓 Controller 能以透過更換類別的方式來因應不同專案的網址樣式，這樣不是很好嗎？路由器就是在這樣的想法下，所產生出來的類別。路由器可以讓瀏覽者輸入不同網址時，協助 Controller 判斷該調用哪個動作來執行。</p>
<p>有了這樣的想法後，我便將解析 GET 參數的程式碼移到了 Router 類別：</p>
<pre><code>&lt;?php
// 預設的路由器
class Router
{
    // 預設的動作
    protected $action = 'index';
    // 在建構函式中解析 GET 變數
    public function __construct()
    {
        $this-&gt;action = isset($_GET['act']) ? strtolower($_GET['act']) : 'index';
    }
    // 取得解析後的動作名稱
    public function getAction()
    {
        return $this-&gt;action;
    }
}

</code></pre><p>然後在 Controller 類別加入以下部份：</p>
<pre><code>&lt;?php
// 抽象控制類別
abstract class Controller
{
    // 略 ...
    // 路由器
    protected $router = NULL;
    // 設定路由器
    public function setRouter(Router $router)
    {
        if (method_exists($this, ($action = $router-&gt;getAction())))
        {
            $this-&gt;action = $action;
        }
    }
    // 略...
}

</code></pre><p>這樣一來，我就可以透過更換 Router 的實作，來達到解析不同網址樣式的目的。</p>
<!-- raw HTML omitted -->
<p>有了抽象的 Controller 類別，現在我就需要把留言板原來動作的部份分離出來，一般框架都是用 IndexController 來當做預設的名稱，它是繼承自 Controller 的類別：</p>
<pre><code>&lt;?php
// 自訂的 Controller
class IndexController extends Controller
{
    // 共用的留言板物件
    private $guestbook = NULL;
    // 建構函式
    public function __construct()
    {
        $this-&gt;guestbook = new GuestBook;
    }
    // 預設的列表功能
    protected function index()
    {
        $view = new HtmlView;
        $view-&gt;setVar('messages', $this-&gt;guestbook-&gt;getAllMessages());
        $view-&gt;render('index.tpl.php');
    }
    // 新增表單頁
    protected function add()
    {
        $view = new HtmlView;
        $view-&gt;setVar('action', 'doAdd');
        $view-&gt;render('edit.tpl.php');
    }
    // 新增留言
    protected function doAdd()
    {
        $title = $_POST['title'];
        $author = $_POST['author'];
        $body = $_POST['body'];

        $this-&gt;guestbook-&gt;addMessage($title, $author, $body);
        $this-&gt;redirectTo('./');
    }
    // 輸出 RSS
    protected function rss()
    {
        $view = new RssView;
        $view-&gt;setVar('items', $this-&gt;guestbook-&gt;getAllMessages());
        $view-&gt;render();
    }
    // 解構函式
    public function __destruct()
    {
        $this-&gt;guestbook = NULL;
    }
}

</code></pre><p>從上面的程式可以看到，原來的 Output 類別已經被代換成 HtmlView 和 RssView 兩個類別了；而這兩個類別用法和原來的 Output 類別相差無幾，但是擴充性上卻好更多。</p>
<!-- raw HTML omitted -->
<p>現在我已經有了幾個共用的抽象類別，還有繼承自它們的子類別實作，但是我不希望把它們都放在一起，這樣的網站架構看起來非常淩亂。而且如果後面需要建立一個新專案時，我希望能花費最小的更動就能從目前的專案中把共用的框架分離出來。</p>
<p>以下是我對整個檔案結構的安排：</p>
<pre><code>Project (專案目錄)
|
|- core (核心框架)
|  |
|  |- Controller.php
|  |
|  |- Router.php
|  |
|  |- View.php
|
|- controllers (自訂的 Controller 存放目錄)
|  |
|  |- IndexController.php
|
|- models (自訂的 Model 存放目錄)
|  |
|  |- Guestbook.php
|
|- views (自訂的 View 存放目錄)
|  |
|  |- HtmlView.php
|  |
|  |- RssView.php
|
|- database (資料庫存放位置，非必要)
|  |
|  |- guestbook.txt
|
|- templates (HTML 樣版)
|  |
|  | - edit.tpl.php
|  |
|  | - index.tpl.php
|
|- theme (網站風格)
|  |
|  |- images
|  |  |
|  |  |- bg.png
|  |
|  |- main.css
|
|- config.php (網站設定)
|
|- index.php (進入點)

</code></pre><p>在使用了這個檔案架構後，相關路徑也需要做調整，例如樣版 CSS 的位置要改到 theme 底下。不過框架的檔案架構並不是一定要依照上面的規劃方式，我看過有些框架把 controllers 、 models 和 views 三個資料夾，放在一個 application 目錄裡，這樣就可以在同一專案下，建立不同的應用項目。</p>
<p>假設現在我要建立一個新專案，那麼我只需要把 core 、 config.php 和 index.php 複製到新專案的資料夾，然後再依照上面的結果把相關的目錄建立出來就可以撰寫新的程式了。當然 HtmlView 在新專案裡應該也是會用得到，因此也可以一併複製過去；不過在新的專案裡，我也還是可以使用 Smarty 或其他的 Template 引擎來實作 View 的部份，這就是抽象化的好處。</p>
<!-- raw HTML omitted -->
<p>不過事情沒那麼簡單，換了新檔案結構後，原來的 index.php 在自動載入類別的功能就會失效了；這是因為我沒有把 include_path 設定好的關係。不過我不希望去修改 php.ini ，也不想從 .htaccess 設定檔下手 (因為 IIS 上不能用) ；所以我得改用 set_include_path 這個函式。</p>
<p>另外由於 Controller 的實作已經交棒給 IndexController ，因此我也必須把原來的 Actions 給換掉。</p>
<p>新的 index.php 如下：</p>
<pre><code>&lt;?php
// 載入設定
require_once 'config.php';
$include_path = array ();
// 系統的 include_path
$include_path[] = get_include_path();
// 目前專案所需要的 include_path
$include_path[] = APP_REAL_PATH . '/core';
$include_path[] = APP_REAL_PATH . '/controllers';
$include_path[] = APP_REAL_PATH . '/models';
$include_path[] = APP_REAL_PATH . '/views';
// 設定新的 include_path
set_include_path(join(PATH_SEPARATOR, $include_path));
function __autoload($class_name)
{
    require_once $class_name . '.php';
}
$controller = new IndexController;
$controller-&gt;setRouter(new Router);
$controller-&gt;run();

</code></pre><p>從上面的程式中可以看到 index.php 的結構還是很簡單，只差多了 include_path 的設定與分離出路由器的實作而已。現在我能確保留言板能正常運作，而且大部份共用的功能可以拿到新專案上使用，從這裡可以看到建構 MVC 框架是非常值得的一件事情。</p>
<h3 id="其他部份">其他部份</h3>
<p>上面的框架只是很簡單的概念實作，因為除了 MVC 以外，一個框架要考量到的部份其實還有很多。像是如何與資料庫做結合、自動化測試、框架產生器等等，這些都會是一個好的框架所需要的部份。因此多吸取目前網路上一些公開框架的經驗，才能對框架的應用有更深一層的瞭解。</p>
<p>不過倒也不用認為框架就一定非常龐大，像有些框架就只會定義一些基本的抽象類別 (像上面我實作的這個版本) ，而有些框架則會提供很多類別庫來讓開發人員使用 (像是 .Net Framework 或 <a href="http://framework.zend.com/">Zend Framework</a>) ；而有些框架則是有特定用途，它們有可能建構在另一個框架上面 (就像 <a href="http://www.cakephp.org/">CakePHP</a> 建構在 <a href="http://www.php.net">PHP</a> 這個大框架上) 。所以框架的組成不會只侷限在 MVC 之下，只是在這裡 MVC 是我所想要釐清的部份而已。</p>
<p>註： PHP 本來就是基於 Web 開發而出現的一套語言框架。</p>
<h2 id="結論">結論</h2>
<p>其實學過寫程式的人都知道，如何為一支程式起頭是最困難的一件事；不過如果有一個好用的框架產生器再配合一個不錯的入門文件的話，這個問題通常很容易被解決 (不過對大部份複雜的專案來說還是天方夜譚) 。因此像 <a href="http://www.rubyonrails.org/">Ruby on Rails</a> 或 <a href="http://www.cakephp.org/">CakePHP</a> 這類的程式框架，也就受到許多人的重視與推崇。</p>
<p>而使用框架最大的好處，就是它們隱藏了許多看起來很複雜的機制；換句話說，它提供了一個抽象化的設計架構，讓開發人員可以依照自己的想法去建構出所需要的應用項目。這些抽象化的部份，像是 Controller 、 View 等，都能夠減少開發人員在處理底層程式的時間。而且透過框架產生器，就連複製基本框架的動作也都省了，不可謂不方便。</p>
<p>但是一個好的 Web 開發者一定要瞭解這些框架背後運作的原理，不然如果拿掉這些所謂的框架後，我想要寫出一個基本的留言板就不是那麼容易的一件事了 (想想看以前寫 CGI 的時代) 。雖然框架隱藏了很多細節，但不表示 Web 開發者就可以完全忽略這些細節；很多進階的功能都會需要熟悉這些原理後才有辦法實作出來，所以千萬不能因為會使用某個框架而自得意滿。</p>
<h2 id="範例下載">範例下載</h2>
<p>照慣例，我把上面的範例碼放在底下：</p>
<ul>
<li>
<p><a href="/resources/webmvc/source/v1.zip">原始留言版</a></p>
</li>
<li>
<p><a href="/resources/webmvc/source/v2.zip">改良後的 MVC 留言板</a></p>
</li>
<li>
<p><a href="/resources/webmvc/source/v3.zip">架構在 MVC 框架上的留言板 </a></p>
</li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>我也來實作 PHP mix-in 的概念 - Part 2</title>
			<link>https://jaceju.net/php-mix-in-2/</link>
			<pubDate>Wed, 14 Mar 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-mix-in-2/</guid>
			<description>石頭成老大說他要為他的 mix-in 實作 part 2 ，我也想到了一些好玩的東西。記得很久之前我寫過一篇「 PHP 的 callback 虛擬型態」，這次就把它用在這裡。 首先我把原來的 Prototype 類</description>
			<content type="html"><![CDATA[<p><a href="http://blog.roodo.com/rocksaying">石頭成</a>老大說他要為他的 <a href="http://blog.roodo.com/rocksaying/archives/2817003.html">mix-in</a> 實作 part 2 ，我也想到了一些好玩的東西。記得很久之前我寫過一篇「 <a href="http://www.jaceju.net/blog/archives/32">PHP 的 callback 虛擬型態</a>」，這次就把它用在這裡。</p>
<!-- raw HTML omitted -->
<p>首先我把原來的 Prototype 類別加上了可以接受 callback 虛擬型態的參數：</p>
<pre><code>&lt;?php
/**
* 實作 mix-in 概念
*
* 參考
* http://personal.schmalls.com/2006/11/06/prototype-based-programming-in-php/
* http://blog.roodo.com/rocksaying/archives/2817003.html
* http://blog.roodo.com/jaceju/archives/2832709.html
* http://blog.roodo.com/jaceju/archives/409709.html
**/
// 可接受 mix-in 物件的抽象類別
abstract class Prototype
{
    private function __set($name, $func)
    {
        if (is_array($func)) {
            if (is_object($func[0]) || (is_string($func[0]) &amp;amp;&amp;amp; class_exists($func[0]))) {
                $this-&gt;$name = array ($func[0], $func[1]);
            }
        } else {
            $this-&gt;$name = $func;
        }
    }
    private function __call($method, $args)
    {
        if (is_array($this-&gt;$method)) {
            call_user_func_array($this-&gt;$method, $args);
        } elseif (is_string($this-&gt;$method)
                &amp;amp;&amp;amp; class_exists($this-&gt;$method)
                &amp;amp;&amp;amp; is_subclass_of($this-&gt;$method, 'MethodObject')) {
            $method_object = new $this-&gt;$method($this);
            call_user_func_array(array ($method_object, 'run'), $args);
        }
    }
}

</code></pre><p>然後把 MethodObject 的 doWork 抽象方法改命名為 run ，這樣感覺比較對味。</p>
<pre><code>&lt;?php
// mix-in 方法的抽象類別
abstract class MethodObject
{
    protected $object;
    public function __construct($object = NULL)
    {
        $this-&gt;object = $object;
    }
    abstract function run();
}

</code></pre><p>而 ClassA 是一個普通的類別，擁有 method1 和 method2 兩個方法。</p>
<pre><code>&lt;?php
class ClassA
{
    public function method1($param)
    {
        echo __CLASS__, &quot;\n&quot;;
        echo &quot;param is &quot;, $param, &quot;\n&quot;;
    }
    public function method2($param)
    {
        echo __METHOD__, &quot;\n&quot;;
        echo &quot;param is &quot;, $param, &quot;\n&quot;;
    }
}

</code></pre><p>至於 ClassB 是繼承自 Prototype 類別，可以為它添油加醋；這裡我讓它保持清白之身就好。</p>
<pre><code>&lt;?php
class ClassB extends Prototype
{
}

</code></pre><p>ClassC 則繼承自 MethodObject ，當做是功能較複雜的函式物件。也就是說 ClassC 可以有自己的私有屬性或是私有方法，讓 run 方法過於龐大時，有重構的空間。</p>
<pre><code>&lt;?php
class ClassC extends MethodObject
{
    public function run()
    {
        $n = func_num_args();
        echo (1 == $n) ? func_get_arg(0) : '', &quot;\n&quot;;
    }
}

</code></pre><p>最後就是測試程式碼了，可以看到 ClassB 生成的 $b 物件可以接受 callback 型態的陣列變數做為執行函式。</p>
<pre><code>&lt;?php
// 測試用的程式碼
$a = new ClassA;
$b = new ClassB;
$b-&gt;method1 = array ('ClassA', 'method1');
$b-&gt;method2 = array ($a, 'method2');
$b-&gt;method3 = 'ClassC';
$b-&gt;method1('param1');
$b-&gt;method2('param2');
$b-&gt;method3('param3');

</code></pre><p>這樣一來，當 ClassB 需要某個方法，而 ClassA 已經有實作了，那麼我們就可以直接利用 ClassA 的實作。只是這樣程式交互的結果，也許會使得設計上出現很大的漏洞，使用上要特別注意。</p>
<p>當然這只是概念性的實作而已，歡迎大家一起來討論看看。</p>
]]></content>
		</item>
		
		<item>
			<title>我也來實作 PHP mix-in 的概念</title>
			<link>https://jaceju.net/php-mix-in/</link>
			<pubDate>Sat, 10 Mar 2007 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-mix-in/</guid>
			<description>之前提過一篇 Prototype-based programming in PHP ，後來在石頭閒語那裡也看到 PHP 實踐 mix-in 概念之可行性，我自己也手癢寫了一個。不過我是把 function 當成是一個 MethodObject ，有點 delegate 味道。然而大部份限</description>
			<content type="html"><![CDATA[<p>之前提過一篇 <a href="http://personal.schmalls.com/2006/11/06/prototype-based-programming-in-php/">Prototype-based programming in PHP</a> ，後來在<a href="http://blog.roodo.com/rocksaying">石頭閒語</a>那裡也看到 <a href="http://blog.roodo.com/rocksaying/archives/2817003.html">PHP 實踐 mix-in 概念之可行性</a>，我自己也手癢寫了一個。不過我是把 function 當成是一個 MethodObject ，有點 delegate 味道。然而大部份限制就像石頭成所說的，所以我也不多提了。還是等 PHP7 的規格出來，看它會不會<a href="http://javaworld.com.tw/roller/page/ingramchen?entry=2007_1_1_WhyAddClosureInJava7">支援 closure</a> 好了  (要跟隨 Java 7 嗎？ XD) 。</p>
<p>註： PHP 6 可能會實現的東西請參考 <a href="http://www.corephp.co.uk/archives/19-Prepare-for-PHP-6.html">Prepare for PHP 6</a> 。</p>
<!-- raw HTML omitted -->
<p>程式如下：</p>
<pre><code>&lt;?php
/**
 * 實作 mix-in 概念
 *
 * 參考
 * http://personal.schmalls.com/2006/11/06/prototype-based-programming-in-php/
 * http://blog.roodo.com/rocksaying/archives/2817003.html
 **/
// 可接受 mix-in 物件的抽象類別
abstract class Prototype
{
    private function __set($name, $value)
    {
        $this-&gt;$name = $value;
    }
    private function __call($method, $args)
    {
        if (class_exists($this-&gt;$method)) {
            $method_object = new $this-&gt;$method($this);
            call_user_func_array(array ($method_object, 'doWork'), $args);
        }
    }
}
// mix-in 方法的抽象類別
abstract class MethodObject
{
    protected $object;
    public function __construct($object = NULL)
    {
        $this-&gt;object = $object;
    }
    abstract function doWork();
}
// 測試用的 Person 類別
class Person extends Prototype
{
    private $name;
    public function __construct($name)
    {
        $this-&gt;name = $name;
    }
    public function getName()
    {
        return $this-&gt;name;
    }
    public function __toString()
    {
        return $this-&gt;name;
    }
}
// 測試用的 Car 類別
class Car extends Prototype
{
    private $owner;
    public function setOwner(Person $person)
    {
        $this-&gt;owner = $person;
    }
    public function __toString()
    {
        return 'This is ' . $this-&gt;owner-&gt;getName() . '\'s car.';
    }
}
// 測試用的 PrintString 類別
class PrintString extends MethodObject
{
    public function doWork()
    {
        $n = func_num_args();
        echo $this-&gt;object, (1 == $n) ? func_get_arg(0) : '';
    }
}
// 測試用的程式碼
$me = new Person('Jace');
$me-&gt;printName = 'PrintString';
$me-&gt;printName(' Say: ');
$my_car = new Car;
$my_car-&gt;setOwner($me);
$my_car-&gt;printCarName = 'PrintString';
$my_car-&gt;printCarName();

</code></pre><p>有沒有用我也不清楚，我的實務經驗太少，沒辦法想到它的用途。不過反正只是寫好玩的，也許能激發別人的靈感也說不一定。</p>
]]></content>
		</item>
		
		<item>
			<title>PHP 裡的 Prototype-based 開發手法</title>
			<link>https://jaceju.net/prototype-based-php/</link>
			<pubDate>Fri, 10 Nov 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/prototype-based-php/</guid>
			<description>上次介紹了一篇 Bring some Ruby/Prototype flavour in your PHP array ，這次有個類似的。 文章網址： Prototype-based programming in PHP 原文轉載如下： For those who have been doing a lot of Javascript programming, you know what prototype-based programming is all about. The basic idea is that functions can be added to classes</description>
			<content type="html"><![CDATA[<p>上次介紹了一篇 <a href="http://hasin.wordpress.com/2006/10/17/bring-some-rubyprototype-flavour-in-your-php-array/">Bring some Ruby/Prototype flavour in your PHP array</a>  ，這次有個類似的。</p>
<p>文章網址： <a href="http://personal.schmalls.com/2006/11/06/prototype-based-programming-in-php/">Prototype-based programming in PHP</a></p>
<!-- raw HTML omitted -->
<p>原文轉載如下：</p>
<!-- raw HTML omitted -->
<p>For those who have been doing a lot of Javascript programming, you know what prototype-based programming is all about. The basic idea is that functions can be added to classes dynamically. In Javascript functions can be added to a static class (using prototype) and it will be added to all instances of the class, or they can be added to a specific instance and only be added to that instance.</p>
<p>Anyway, lets get to the point. I decided to try adding this functionality to PHP. I&rsquo;m not sure why its a good idea, or if it even is, but I&rsquo;ll let you be the judge of that. So here is the class I came up with:</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">/**
 * @copyright   <span class="ni">&amp;amp;</span>#169; 2006, Schmalls / Joshua Thompson, All Rights Reserved
 * @license	 http://www.opensource.org/licenses/bsd-license.php New BSD
 * @author	  Joshua Thompson <span class="p">&lt;</span><span class="nt">spam.goes.in.here</span><span class="err">@</span><span class="na">gmail</span><span class="err">.</span><span class="na">com</span><span class="p">&gt;</span>
 * @version	 1.0.0
 * @link		http://www.countercubed.com
 */
/**
 * This class holds the prototype capabilities
 *
 * Extending this class makes it prototype capable
 */
class Prototype
{
    /**
     * Holds prototype functions
     *
     * @var  array
     */
    private $_functions = array();
    /**
     * Default constructor
     *
     * This is here so that php doesn&#39;t complain about the prototype function
     */
    public function __construct()
    {
    }
    /**
     * Sets the prototype functions or variables
     *
     * @param   string $name
     * @param   mixed $value
     */
    public function __set( $name, $value )
    {
        if ( function_exists( $value ) ) :
            $this-&gt;_functions[$name] = $value;
        else :
            $this-&gt;$name = $value;
        endif;
    }
    /**
     * Gets static prototype variables if they exist
     *
     * @param   string $name
     * @return  mixed
     */
    public function __get( $name )
    {
        if ( isset( $this-&gt;prototype()-&gt;$name ) ) :
            return $this-&gt;prototype()-&gt;$name;
        else :
            trigger_error( &#39;Undefined property: &#39; . __CLASS__ . &#39;::&#39; . $name, E_USER_NOTICE );
        endif;
    }
    /**
     * Calls a static prototype function
     *
     * @param   string $name
     * @param   array $arguments
     * @return  mixed
     */
    public function __call( $name, $arguments )
    {
        if ( isset( $this-&gt;_functions[$name] ) ) :
            return call_user_func_array( $this-&gt;_functions[$name], $arguments );
        elseif ( $this-&gt;prototype()-&gt;isCallable( $name ) ) :
            return call_user_func_array( array( $this-&gt;prototype(), $name ), $arguments );
        else :
            trigger_error( &#39;Call to undefined method &#39; . __CLASS__ . &#39;::&#39; . $name . &#39;()&#39;, E_USER_ERROR );
        endif;
    }
    /**
     * Returns the static prototype holder
     *
     * @return  Prototype
     */
    public static function prototype()
    {
        static $prototype = null;
        if ( $prototype === null ) :
            $prototype = new Prototype();
        endif;
        return $prototype;
    }
    /**
     * Needed for the static calling functionality
     *
     * @return  boolean
     */
    public function isCallable( $name )
    {
        return ( isset( $this-&gt;_functions[$name] ) );
    }
}

</code></pre></div><p>Now all a class needs to do is extend the Prototype class. A sample of its use follows:</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">// make some prototype classes
class Test1 extends Prototype
{
}
class Test2 extends Prototype
{
}
class Test3 extends Test1
{
}
// lets create some test static functions
Test1::prototype()-&gt;fun1 = create_function( &#39;$arg1&#39;, &#39;
    echo \&#39;Static Test1::fun1 \&#39; . $arg1 . &#34;\n&#34;;
&#39;);
Test2::prototype()-&gt;fun2 = create_function( &#39;$arg1&#39;, &#39;
    echo \&#39;Static Test2::fun2 \&#39; . $arg1 . &#34;\n&#34;;
&#39;);
// now instantiate the objects
$test1 = new Test1();
$test2 = new Test2();
// and make some more functions
$test1-&gt;fun3 = create_function( &#39;$arg2&#39;, &#39;
echo \&#39;Test1::fun3 \&#39; . $arg2 . &#34;\n&#34;;
&#39;);
$test2-&gt;fun4 = create_function( &#39;$arg2&#39;, &#39;
echo \&#39;Test2::fun4 \&#39; . $arg2 . &#34;\n&#34;;
&#39;);
// output: Static Test1::fun1 bob
$test1-&gt;fun1( &#39;bob&#39; );
// create another function
Test1::prototype()-&gt;fun2 = create_function( &#39;$arg1&#39;, &#39;
echo \&#39;Static Test1::fun2 \&#39; . $arg1 . &#34;\n&#34;;
&#39;);
// output: Static Test1::fun2 bobby
$test1-&gt;fun2( &#39;bobby&#39; );
// output: Static Test2::fun2 robert
$test2-&gt;fun2( &#39;robert&#39; );
// output: Test1::fun3 robby
$test1-&gt;fun3( &#39;robby&#39; );
// output: Test2::fun4 rob
$test2-&gt;fun4( &#39;rob&#39; );
// another instance still has the static functions
$test1_2 = new Test1();
// output: Static Test1::fun1 bob
$test1_2-&gt;fun1( &#39;bob&#39; );
$test3 = new Test3();
// this will give an error because prototype functions do not extend down to a child class
$test3-&gt;fun1( &#39;roberto&#39; );
</code></pre></div><p>Once again, I don&rsquo;t know how useful it is, but let me know what you think.</p>
<!-- raw HTML omitted -->
<p>主要的概念是用 PHP5 的 magic methods ： <code>__set</code> 、 <code>__get</code> 、 <code>__call</code> 。</p>
<p>利用 <code>__set</code> 及物件內部的 _functions 變數記住 <code>create_function</code> 所產生出來的 callback 匿名函式，然後再用 <code>__call</code> 呼叫這些動態建立的 methods 。</p>
<p>當然 magic methods 必須在物件實體產生後才能使用，詳細的說明可以參考一下該文回應的部份。</p>
]]></content>
		</item>
		
		<item>
			<title>[好文] Check your PHP code at every level with unit tests</title>
			<link>https://jaceju.net/os-php-unit/</link>
			<pubDate>Mon, 21 Aug 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/os-php-unit/</guid>
			<description>解釋如何利用 Unit Test 來測試各層級的 PHP 程式。 文章網址：Check your PHP code at every level with unit tests 重點提示： It&amp;rsquo;s 3 a.m. Do you know if your code is still working?</description>
			<content type="html"><![CDATA[<p>解釋如何利用 Unit Test 來測試各層級的 PHP 程式。</p>
<p>文章網址：<a href="http://www.ibm.com/developerworks/library/os-php-unit/">Check your PHP code at every level with unit tests</a></p>
<p>重點提示：</p>
<!-- raw HTML omitted -->
<p>It&rsquo;s 3 a.m. Do you know if your code is still working?</p>
<!-- raw HTML omitted -->
]]></content>
		</item>
		
		<item>
			<title>ASP 購物車三部曲(2)</title>
			<link>https://jaceju.net/classic-asp-shopping-cart-2/</link>
			<pubDate>Tue, 30 May 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-shopping-cart-2/</guid>
			<description>簡介 是到了該結帳的時候了，就像你在大賣場買完東西後，要到櫃台付錢是一樣的。但是在購物網站買完東西後，總是要填寫一些個人資料，方便商家能夠把東</description>
			<content type="html"><![CDATA[<h2 id="簡介">簡介</h2>
<p>是到了該結帳的時候了，就像你在大賣場買完東西後，要到櫃台付錢是一樣的。但是在購物網站買完東西後，總是要填寫一些個人資料，方便商家能夠把東西寄給你，這是虛擬商店比較不同的地方。</p>
<p>而要填寫的資料不外乎是購買人資訊、收貨人資訊等等，這些資訊大部份都能在使用者登入成為會員取得，這通常也是一般會員制購物網站的作法。</p>
<p>不過難題來了，雖然我們建立的是一個會員制購物網站，但是客戶卻希望能讓未註冊的訪客也能夠在這裡先買東西，而到結帳時才選擇是不是要加入會員。當然客戶永遠不會瞭解採取這種方式的難度，他認為你是網站開發人員，你一定會有辦法的。</p>
<p>先不要翻桌子，解決問題是我們的責任。仔細想想，訪客對購物網站的經營者而言，也可能是潛在的消費者；所以如果能提供便利的方式讓這些人轉變為會員，其實還滿重要的。</p>
<p>當然我們得提供畫面讓使用者填寫資料，並且把這些資料做適當的處理。不過後續的處理方式我就不多提了，現在我們把重點先放在結帳表單顯示的頁面流程，來看看物件導向思維如何應用在這上面。</p>
<p>註：這裡我也會略過金流及物流的部份，因為各家金流及物流的實作方式都不太一樣，要寫的話可能又會是落落長。</p>
<!-- raw HTML omitted -->
<h2 id="定義頁面顯示的流程">定義頁面顯示的流程</h2>
<p>暫時別想太多，我們先將填寫個人資料表單頁面的產生流程定義下來：</p>
<ul>
<li>判斷是不是已登入，如果是就取得該會員的資料 (一般是從資料庫取得) 。</li>
<li>如果不是會員 (即訪客) ，顯示空值表單，並顯示一個選項讓訪客決定要不要成為會員；否則表單就預先帶出會員的資料，但不必再填寫購買人的資訊 (收貨人則是一定要填寫的，這是一般結帳原則) 。</li>
</ul>
<p>而不管使用者是不是會員，我們都先把資料產生出來，放到一個 Dictionary 物件中，以便 HTML 頁面可以用一致的方式顯示資料。</p>
<p>結帳時處理會員的程式如下：</p>
<p>/Checkout.asp</p>
<pre><code>&lt;%
Option Explicit
%&gt;
&lt;!-- #include virtual=&quot;/OOASP/functions/GetObject.asp&quot; --&gt;
&lt;%
Session(&quot;MemberID&quot;) = &quot;A123456789&quot;
' 會員物件
Dim oMember : Set oMember = GetObject(&quot;Member&quot;)
' 會員初始化
oMember.Init
' HTML 的部份利用上面的 oMember 來顯示表單頁面
%&gt;
&lt;!-- #include file=&quot;CheckOut.tpl.asp&quot; --&gt;

</code></pre><p>因為在 ASP 中我們沒辦法把自訂類別所產生的物件放到 Session 裡，所以這邊我只利用 Session 來記住某個關鍵屬性。假設登入時， Session(&ldquo;MemberID&rdquo;) 會記住會員的身份證號碼；而這個 Session(&ldquo;MemberID&rdquo;) 是在使用者為登入時，則會是空值。</p>
<p>然後 HTML 頁面的部份我是用 SSI 的 Include 方式，這在小程序上非常好用，以下就是部份的 HTML 碼：</p>
<p>/Checkout.tpl.asp</p>
<pre><code>...
&lt;%
' 如果是訪客就要填寫購買人姓名及身份證號碼
If Not oMember.IsLogin Then
%&gt;
&lt;p&gt;
&lt;label for=&quot;shopper_name&quot; class=&quot;text&quot;&gt;購買人姓名&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;shopper_name&quot; id=&quot;shopper_name&quot;
value=&quot;&quot; /&gt;
&lt;/p&gt;
&lt;p&gt;
&lt;label for=&quot;shopper_pid&quot; class=&quot;text&quot;&gt;身份證號碼&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;shopper_pid&quot; id=&quot;shopper_pid&quot;
value=&quot;&quot; /&gt;
&lt;/p&gt;
&lt;%
' 如果是會員就不用填寫了
Else
%&gt;
&lt;p&gt;
&lt;label for=&quot;shopper_name&quot; class=&quot;text&quot;&gt;購買人姓名&lt;/label&gt;
&lt;span&gt;&lt;% = oMember.GetField(&quot;Name&quot;) %&gt;&lt;/span&gt;
&lt;input type=&quot;hidden&quot; name=&quot;shopper_name&quot; id=&quot;shopper_name&quot;
value=&quot;&lt;% = oMember.GetField(&quot;Name&quot;) %&gt;&quot; /&gt;
&lt;/p&gt;
&lt;p&gt;
&lt;label for=&quot;shopper_pid&quot; class=&quot;text&quot;&gt;身份證號碼&lt;/label&gt;
&lt;span&gt;&lt;% = oMember.GetField(&quot;PersonalNumer&quot;) %&gt;&lt;/span&gt;
&lt;input type=&quot;hidden&quot; name=&quot;shopper_pid&quot; id=&quot;shopper_pid&quot;
value=&quot;&lt;% = oMember.GetField(&quot;PersonalNumer&quot;) %&gt;&quot; /&gt;
&lt;/p&gt;
&lt;%
End If
%&gt;
...
&lt;label for=&quot;receiver_name&quot; class=&quot;text&quot;&gt;收件人姓名&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;receiver_name&quot; id=&quot;receiver_name&quot;
value=&quot;&lt;% = oMember.GetField(&quot;Name&quot;) %&gt;&quot; /&gt;
...
&lt;label for=&quot;receiver_address&quot; class=&quot;text&quot;&gt;收件人地址&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;receiver_address&quot; id=&quot;receiver_address&quot; /&gt;
...
&lt;%
' 在表單中加上一個 checkbox
' 讓使用者決定要不要用現在的資料成為會員
If Not oMember.IsLogin Then
%&gt;
&lt;fieldset&gt;&lt;legend&gt;選項 (非必填) &lt;/legend&gt;
&lt;input type=&quot;checkbox&quot; name=&quot;change_level&quot; id=&quot;change_level&quot; /&gt;
&lt;label for=&quot;change_level&quot;&gt;是否成為會員？&lt;/label&gt;
&lt;/fieldset&gt;
&lt;%
End If
%&gt;
&lt;input type=&quot;submit&quot; value=&quot;確定&quot; /&gt;
...

</code></pre><p>註：為了不讓大家頭痛，在 HTML 的部份我省略掉很多東西，像是 JavaScript 的表單驗證等等，請先專注在 ASP 程式上就好。</p>
<p>其實看起來也不會太難嗎？而且不管怎樣，使用者都一定要填寫個人資料；因為如果使用者的身分是會員時，購買人的輸入欄就會變成隱藏欄位，這樣處理資料時就能夠一致了 (除了訪客可能會成為會員時) 。</p>
<p>先來看看顯示的結果好了，如果使用者尚未登入成為會員時，就會顯示以下畫面：</p>
<p><img src="/resources/ooasp_cart/images/01_checkout_guest.gif" alt=""></p>
<p>而如果已經登入成會員身份時，就會顯示以下畫面：</p>
<p><img src="/resources/ooasp_cart/images/01_checkout_member.gif" alt=""></p>
<p>這時候會員就不必填寫購買人姓名和身份證了。</p>
<p>註：當然實際應用上的表單會比較複雜，這裡我特意簡化了。</p>
<p>大致瞭解程式與畫面後，我們接下來就可以完成會員類別的程式碼：</p>
<p>/class/Member.asp</p>
<pre><code>&lt;%
' 定義常數
Const LEVEL_GUEST  = 1   ' 訪客
Const LEVEL_MEMBER = 2   ' 一般會員
' 會員類別
Class Member
Private Fields
Public IsLogin
' 物件初始化
Private Sub Class_Initialize()
IsLogin = False
' 為了方便說明，我只用了幾個簡單的會員屬性
Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
With Fields
.Add &quot;Level&quot;, LEVEL_GUEST ' 預設等級為訪客
.Add &quot;Name&quot;, &quot;&quot;           ' 預設姓名為空字串
.Add &quot;PersonalNumer&quot;, &quot;&quot;  ' 預設身份證號碼為空字串
End With
End Sub
' 會員初始化
Public Sub Init
Dim sName : sName = &quot;&quot;
Dim iLevel : iLevel = LEVEL_GUEST
Dim sPersonalNumer
sPersonalNumer = UCase(Trim(Session(&quot;MemberID&quot;)))
' 從 Session 值判斷是否已登入
If sPersonalNumer = &quot;A123456789&quot; Then
sName = &quot;我是一般會員&quot;
iLevel = LEVEL_MEMBER
IsLogin = True
End If
SetField &quot;Level&quot;, iLevel
SetField &quot;Name&quot;, sName
SetField &quot;PersonalNumer&quot;, sPersonalNumer
End Sub
' 設定會員屬性
Public Sub SetField(sName, vValue)
If Fields.Exists(sName) Then
Fields(sName) = vValue
End If
End Sub
' 取得會員屬性
Public Function GetField(sName)
GetField = Null
If Fields.Exists(sName) Then
GetField = Fields(sName)
End If
End Function
' 物件結束
Private Sub Class_Terminate()
Set Fields = Nothing
End Sub
End Class
%&gt;

</code></pre><p>註：會員類別做的事當然不僅這些，這裡我簡化掉很多功能，像是 Login 方法等；目前我只需要讓範例能夠成功執行，讓大家能夠專注在頁面的流程上。</p>
<h2 id="流程的改變">流程的改變</h2>
<p>不過上面的做法讓我覺得心神不寧，因為我總是認為客戶絕對不會就這樣滿足。果不其然，沒多久客戶就要我們加上 VIP 會員的流程。一樣為了簡化說明，這裡我先把新的流程定義如下：</p>
<ul>
<li>判斷是不是已登入，如果是就取得該會員的資料 (一般是從資料庫取得) 。</li>
<li>如果不是會員 (即訪客) ，顯示空值表單，並顯示一個選項讓訪客決定要不要成為會員；否則表單就預先帶出會員的資料，但不必再填寫購買人的資訊 (至此都和前面的規則一樣) 。</li>
<li>如果是一般會員，就要顯示升級為 VIP 會員的選項；如果是 VIP 會員，要額外顯示可用折扣額度及折扣輸入的欄位。</li>
</ul>
<p>註：事實上，我在專案裡所加入的功能遠比單純加上 VIP 會員流程複雜；這裡只是為了讓大家容易進入狀況，我特地以這種比較簡單的方式來說明。</p>
<p>我們先來看看畫面有什麼差別，首先是訪客：</p>
<p><img src="/resources/ooasp_cart/images/02_checkout_guest.gif" alt=""></p>
<p>我們可以看到訪客的表單跟前面是一樣的。</p>
<p>接下來是一般會員的表單畫面：</p>
<p><img src="/resources/ooasp_cart/images/02_checkout_member.gif" alt=""></p>
<p>如上圖所示，一般會員多了一個是否升級為 VIP 會員的選項。</p>
<p>最後是新增的 VIP 會員表單畫面：</p>
<p><img src="/resources/ooasp_cart/images/02_checkout_vip.gif" alt=""></p>
<p>很清楚地，三種不同等級的使用者都有不同選項，所以我們的程式也要有所因應。</p>
<p>不過會員類別的部份其實可以不用變動，因為實際上會員資料是從資料庫取得的；但為了模擬程式效果，我還是加入了 VIP 會員的常數定義及程式判斷：</p>
<p>/class/Member.asp</p>
<pre><code>' 定義常數
Const LEVEL_GUEST  = 1   ' 訪客
Const LEVEL_MEMBER = 2   ' 一般會員
Const LEVEL_VIP    = 3   ' VIP 會員
' 會員類別
Class Member
...
' 會員初始化
Public Sub Init
...
' 從 Session 值判斷是否已登入
Select Case sPersonalNumer
Case &quot;A123456789&quot;
sName = &quot;一般會員&quot;
iLevel = LEVEL_MEMBER
IsLogin = True
      Case &quot;V123456789&quot;
        sName = &quot; VIP 會員&quot;
        iLevel = LEVEL_VIP
        IsLogin = True
End Select
...
End Sub
...
End Class

</code></pre><p>至於 Checkout.asp 就暫時不用修改了，主要的變化是在 HTML 頁面上，因此我們可以將 HTML 頁面的選項區修改如下：</p>
<p>/Checkout.tpl.asp</p>
<pre><code>...
&lt;fieldset&gt;&lt;legend&gt;選項 (非必填) &lt;/legend&gt;
&lt;%
' 在表單中加上一個 checkbox
' 讓使用者決定要不要用現在的資料成為會員
If Not oMember.IsLogin Then
%&gt;
&lt;input type=&quot;checkbox&quot; name=&quot;change_level&quot; id=&quot;change_level&quot; /&gt;
&lt;label for=&quot;change_level&quot;&gt;是否成為會員？&lt;/label&gt;
&lt;%
' 在表單中加上一個 checkbox
' 讓一般會員決定要不要升級為 VIP 會員
ElseIf LEVEL_MEMBER = oMember.GetField(&quot;Level&quot;) Then
%&gt;
&lt;input type=&quot;checkbox&quot; name=&quot;change_level&quot; id=&quot;change_level&quot; /&gt;
&lt;label for=&quot;change_level&quot;&gt;是否升級為 VIP 會員？&lt;/label&gt;
&lt;%
' 在表單中加上一個文字欄位
' 讓 VIP 會員使用點數
ElseIf LEVEL_VIP = oMember.GetField(&quot;Level&quot;) Then
%&gt;
&lt;p&gt;您可用的 ECoupon 點數：100&lt;/p&gt;
&lt;label for=&quot;ecoupon_number&quot;&gt;輸入欲使用的 ECoupon 點數：&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;ecoupon_number&quot; id=&quot;ecoupon_number&quot; size=&quot;10&quot; /&gt;
&lt;%
End If
%&gt;
&lt;/fieldset&gt;

</code></pre><p>但是我仔細思考了一下，如果我們再用 If Else 結構，往後維護肯定很麻煩，而且 Checkout.tpl.asp 的架構會變得非常之龐大，這可不是我樂意見到的。</p>
<p>註：上面程式之所以看起來很簡單的原因，是因為我簡略掉很多細節。</p>
<h2 id="流程抽象化">流程抽象化</h2>
<p>我重新歸納並且分開三種角色的流程，看看它們到底有什麼不同。</p>
<p>訪客：</p>
<ul>
<li>建立空的會員資料，購買人欄位為可填。</li>
<li>顯示收貨人的填寫欄位。</li>
<li>加上一個詢問是不是要成為會員的選項。</li>
</ul>
<p>一般會員：</p>
<ul>
<li>預先帶出會員的資料，但購買人欄位變為不可填。</li>
<li>顯示收貨人的填寫欄位。</li>
<li>加上升級為 VIP 會員的選項。</li>
</ul>
<p>VIP 會員：</p>
<ul>
<li>預先帶出 VIP 會員的資料，但購買人欄位變為不可填。</li>
<li>顯示收貨人的填寫欄位。</li>
<li>顯示可用折扣額度。</li>
</ul>
<p>看起來只有第二個步驟是一樣的，第一步和第三步好像都不太一樣，但是真的是不一樣嗎？</p>
<p>在三個角色的第一個步驟裡，不管是建立空的會員資料或是從資料庫讀取會員資料，最後不是都會在 HTML 頁面上顯示購買人欄位嗎？差別只是要不要填寫而已。從這個方向去想，其實它們做的事是滿相像的。</p>
<p>同樣地對第三個步驟來說，每種層級都會有不同的附屬畫面，例如訪客有詢問是否要成為會員的選項，而 VIP 會員會有一個額外顯示可折扣額度的功能；其實我們也可以將它視為附屬在顯示表單流程裡。</p>
<p>因此不管是哪一個角色，我們都能得到一個基本的概念流程：</p>
<ul>
<li>取得角色資料，決定是否讓使用者填寫購買人資訊。</li>
<li>顯示收貨人的填寫欄位。</li>
<li>依照使用者等級決定表單所以顯示的額外資訊。</li>
</ul>
<p>這就是流程抽象化。</p>
<h2 id="template-method-模式">Template Method 模式</h2>
<h3 id="什麼是-template-method">什麼是 Template Method</h3>
<p>抽象化有什麼好處還記得嗎？沒錯！就是可以用相同的方式來處理不同的邏輯。我們已經將上面三種會員的結帳表單顯示流程歸納出了相同的抽象結構，所以我們是不是能用相同的方式來處理它們呢？這時候就是 Template Method 模式大展身手的時機了。</p>
<p>Template Method 的基本概念就是在父類別 (Base) 的某個方法 (Run) 中定義好一個流程骨架，然後讓子類別 (ConcreteClass) 繼承，再由子類別去實作流程中的每個步驟。我們可以用以下的 UML 圖來表示：</p>
<p><img src="/resources/ooasp_cart/images/template_method.png" alt="Template Method UML"></p>
<p>如你所見， Base 類別的 Run 函式定義好了整個流程骨架，而它就不能被子類別所覆寫，以免打亂了整個流程。這在良好物件導向特性語言裡，可以用 final 關鍵字來指定。</p>
<p>所以子類別 (ConcreteClass) 只需要關心步驟的實作，而執行的順序早已由 Base 類別的 Run 函式決定好了。也因此每個角色的流程雖然都一樣，但透過子類別上各步驟實作上的不同，我們就能夠清楚地分離每個角色該做的事。</p>
<h3 id="如何實作-template-method">如何實作 Template Method</h3>
<p>先前提過 ASPUnit 用到了一個 Template Method 模式，但是沒有繼承機制的 ASP 如何實作 Template Method 呢？</p>
<p>Template Method 裡有句名言： Don&rsquo;t Call Me, I Call You!</p>
<p>意思就是父類別會去呼叫子類別的方法；雖然在似類 Java 的語言裡，實際執行動作的還是子類別的實體 (因為父類別是抽象的) ，但是在 ASP 中卻真的是由父類別的實體去呼叫子類別的實體！</p>
<p>還是實際用前面的例子來示範好了，首先我先把 Checkout.asp 改一下，讓大家清楚程式會怎麼跑：</p>
<p>/Checkout.asp</p>
<pre><code>&lt;%
Option Explicit
%&gt;
&lt;!-- #include virtual=&quot;/OOASP/functions/GetObject.asp&quot; --&gt;
&lt;!-- #include file=&quot;class/CheckoutForm.asp&quot; --&gt;
&lt;%
Session(&quot;MemberID&quot;) = &quot;A123456789&quot;
' 會員物件
Dim oMember : Set oMember = GetObject(&quot;Member&quot;)
' 會員初始化
oMember.Init
' 建立表單顯示物件
' 因為用到了 SSI 指令，所以不能使用 GetObject 來建立物件
Dim oCheckoutForm : Set oCheckoutForm = New CheckoutForm
oCheckoutForm.SetMember oMember
oCheckoutForm.Display
%&gt;

</code></pre><p>你應該會發現原來的 SSI Include 指令被我換成一個 CheckoutForm 類別物件了，它扮演的角色就是 Template Method 中的父類別，也就是負責定義流程骨架的角色。</p>
<p>接下來是 CheckoutForm 類別，它是用來顯示表單的：</p>
<p>/class/CheckoutForm.asp</p>
<pre><code>&lt;!-- #include file=&quot;GuestCheckoutForm.asp&quot; --&gt;
&lt;!-- #include file=&quot;MemberCheckoutForm.asp&quot; --&gt;
&lt;!-- #include file=&quot;VIPCheckoutForm.asp&quot; --&gt;
&lt;%
Class CheckoutForm
' 實體物件
Private ConcreteObject
' 被繼承的物件
Public Prototype
' 會員
Public Member
' 會員層級的對應表
Private LevelList
' 物件初始化
Private Sub Class_Initialize()
Set ConcreteObject = Me
Set Prototype = Me
Set LevelList = _
Server.CreateObject(&quot;Scripting.Dictionary&quot;)
With LevelList
.Add LEVEL_GUEST, &quot;Guest&quot;
.Add LEVEL_MEMBER, &quot;Member&quot;
.Add LEVEL_VIP, &quot;VIP&quot;
End With
End Sub
' 設定會員
Public Sub SetMember(oMember)
Set Member = oMember
Set ConcreteObject = Me
On Error Resume Next
Execute &quot;Set ConcreteObject = New &quot; &amp;amp; _
LevelList(oMember.GetField(&quot;Level&quot;)) &amp;amp; _
&quot;CheckoutForm&quot;
On Error Goto 0
Set ConcreteObject.Prototype = Me
End Sub
' 顯示表單
Public Sub Display()
%&gt;&lt;!-- #include file=&quot;../CheckOut.tpl.asp&quot; --&gt;&lt;%
End Sub
' 顯示購買人欄位
Public Sub DisplayShopperFields()
%&gt;&lt;p&gt;
&lt;label for=&quot;shopper_name&quot; class=&quot;text&quot;&gt;購買人姓名&lt;/label&gt;
&lt;span&gt;&lt;% = Prototype.Member.GetField(&quot;Name&quot;) %&gt;&lt;/span&gt;
&lt;input type=&quot;hidden&quot; name=&quot;shopper_name&quot; id=&quot;shopper_name&quot;
value=&quot;&lt;% = Prototype.Member.GetField(&quot;Name&quot;) %&gt;&quot; /&gt;
&lt;/p&gt;
&lt;p&gt;
&lt;label for=&quot;shopper_pid&quot; class=&quot;text&quot;&gt;身份證號碼&lt;/label&gt;
&lt;span&gt;&lt;% = Prototype.Member.GetField(&quot;PersonalNumer&quot;) %&gt;&lt;/span&gt;
&lt;input type=&quot;hidden&quot; name=&quot;shopper_pid&quot; id=&quot;shopper_pid&quot;
value=&quot;&lt;% = Prototype.Member.GetField(&quot;PersonalNumer&quot;) %&gt;&quot; /&gt;
&lt;/p&gt;&lt;%
End Sub
' 顯示收貨人欄位
Public Sub DisplayReceiverFields()
%&gt;&lt;p&gt;
&lt;label for=&quot;receiver_name&quot; class=&quot;text&quot;&gt;收件人姓名&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;receiver_name&quot; id=&quot;receiver_name&quot;
value=&quot;&lt;% = Prototype.Member.GetField(&quot;Name&quot;) %&gt;&quot; /&gt;
&lt;/p&gt;
&lt;p&gt;
&lt;label for=&quot;receiver_address&quot; class=&quot;text&quot;&gt;收件人地址&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;receiver_address&quot; id=&quot;receiver_address&quot; /&gt;
&lt;/p&gt;&lt;%
End Sub
' 顯示選擇欄位
Public Sub DisplayOptionFields()
End Sub
' 物件結束
Private Sub Class_Terminate()
Set ConcreteObject = Nothing
Set Prototype = Nothing
Set LevelList = Nothing
Set Member = Nothing
End Sub
End Class
%&gt;

</code></pre><p>注意，我們在 SetMember 函式裡用了 Factory Method 模式，它會幫助我們自動產生對應的 CheckoutForm 衍生物件。</p>
<p>在 CheckoutForm 類別中， Display 函式就是一個 Template Method ；看起來它只是將原來引入 Checkout.tpl.asp 的動作搬過來而已，但這其實是告訴我們流程已經定義在 Checkout.tpl.asp 裡：</p>
<p>/Checkout.tpl.asp</p>
<pre><code>...
&lt;fieldset&gt;&lt;legend&gt;購買人資訊&lt;/legend&gt;
&lt;% ConcreteObject.DisplayShopperFields %&gt;
&lt;/fieldset&gt;
&lt;fieldset&gt;&lt;legend&gt;收件人資訊&lt;/legend&gt;
&lt;% ConcreteObject.DisplayReceiverFields %&gt;
&lt;/fieldset&gt;
&lt;fieldset&gt;&lt;legend&gt;選項 (非必填) &lt;/legend&gt;
&lt;% ConcreteObject.DisplayOptionFields %&gt;
&lt;/fieldset&gt;
...

</code></pre><p>而 DisplayShopperFields 、 DisplayReceiverFields 、 DisplayOptionFields 就是我們的步驟函式，也就是說子類別只需要實作這三個函式即可。當然在 CheckoutForm 類別裡，我們也可以把預設的動作寫上去，這樣子類別如果有不想覆寫的步驟，就可以轉交給父類別執行。</p>
<p>可是到底要怎麼模擬繼承呢？ ASPUnit 採用了一個很聰明的作法，也就是反過來將子類別的方法或屬性，移交給父類別來執行。</p>
<p>什麼意思呢？首先我們在父類別中定義了兩個屬性： Prototype 及 ConcreteObject ，而在子類別中定義了一個 Prototype 屬性。在 CheckForm 類別實體化時，把 Prototype 及 ConcreteObject 兩個屬性都指向自己 (Me) 。</p>
<p>而在 Factory Method (也就是 SetMember) 執行後，就會把 ConcreteObject 指向真正的子類別 (GuestCheckoutForm 、 MemberCheckoutForm 、 VIPCheckoutForm 其中之一) 的實體；並且同時將子類別的 Prototype 屬性指向 CheckoutForm 的實體 (Me) ，讓雙方都能同時相互參考。</p>
<p>如此一來，父類別就會透過 ConcreteObject 來呼叫子類別；如果子類別有不想實作的函式，就透過 Prototype 屬性把要求轉交給父類別；整個執行的角色就是父類別，而這其中的關鍵就是 SetMember 函式。</p>
<p>換句話說，我們在執行時期只會操作到父類別的實體。至於子類別的實體，則是由父類別實體自行產生並控制。</p>
<p>我想你大概已經昏了吧？直接看看 CheckoutForm 的子類別好了：</p>
<p>/class/GuestCheckoutForm.asp</p>
<pre><code>&lt;%
Class GuestCheckoutForm ' Extends CheckoutForm
' 被繼承的物件
Public Prototype
' 顯示購買人欄位
Public Sub DisplayShopperFields()
%&gt;&lt;p&gt;
&lt;label for=&quot;shopper_name&quot; class=&quot;text&quot;&gt;購買人姓名&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;shopper_name&quot; id=&quot;shopper_name&quot;
value=&quot;&quot; /&gt;
&lt;/p&gt;
&lt;p&gt;
&lt;label for=&quot;shopper_pid&quot; class=&quot;text&quot;&gt;身份證號碼&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;shopper_pid&quot; id=&quot;shopper_pid&quot;
value=&quot;&quot; /&gt;
&lt;/p&gt;&lt;%
End Sub
' 顯示收貨人欄位
Public Sub DisplayReceiverFields()
Prototype.DisplayReceiverFields
End Sub
' 顯示選擇欄位
Public Sub DisplayOptionFields()
%&gt;&lt;input type=&quot;checkbox&quot; name=&quot;change_level&quot; id=&quot;change_level&quot; /&gt;
&lt;label for=&quot;change_level&quot;&gt;是否成為會員？&lt;/label&gt;&lt;%
End Sub
End Class
%&gt;

</code></pre><p>由於 GuestCheckoutForm 的購買人欄位和其他兩個類別不同，所以我覆寫了 CheckoutForm 的 DisplayShopperFields 函式，而 DisplayOptionFields 也是一樣。但是因為 DisplayReceiverFields 用預設的即可，因此我便透過 Prototype 來呼叫父類別實體裡的 DisplayReceiverFields 函式。</p>
<p>另外我把 HTML 頁面中屬於訪客條件區塊的部份集中到 GuestCheckoutForm 裡，這樣我們就能很明確知道訪客應該執行的步驟。</p>
<p>註：這裡用 ASP 模擬繼承時有個缺點，那就是方法必須再宣告一次，然後呼叫父類別的實作來執行。</p>
<p>以此類推， MemberCheckoutForm 及 VIPCheckoutForm 也是一樣的道理。</p>
<p>/class/MemberCheckoutForm.asp</p>
<pre><code>&lt;%
Class MemberCheckoutForm ' Extends CheckoutForm
' 被繼承的物件
Public Prototype
' 顯示購買人欄位
Public Sub DisplayShopperFields()
Prototype.DisplayShopperFields
End Sub
' 顯示收貨人欄位
Public Sub DisplayReceiverFields()
Prototype.DisplayReceiverFields
End Sub
' 顯示選擇欄位
Public Sub DisplayOptionFields()
%&gt;&lt;input type=&quot;checkbox&quot; name=&quot;change_level&quot; id=&quot;change_level&quot; /&gt;
&lt;label for=&quot;change_level&quot;&gt;是否升級為 VIP 會員？&lt;/label&gt;&lt;%
End Sub
End Class
%&gt;

</code></pre><p>/class/VIPCheckoutForm.asp</p>
<pre><code>&lt;%
Class VIPCheckoutForm ' Extends CheckoutForm
' 被繼承的物件
Public Prototype
' 顯示購買人欄位
Public Sub DisplayShopperFields()
Prototype.DisplayShopperFields
End Sub
' 顯示收貨人欄位
Public Sub DisplayReceiverFields()
Prototype.DisplayReceiverFields
End Sub
' 顯示選擇欄位
Public Sub DisplayOptionFields()
%&gt;&lt;p&gt;您可用的 ECoupon 點數：100&lt;/p&gt;
&lt;label for=&quot;ecoupon_number&quot;&gt;輸入欲使用的 ECoupon 點數：&lt;/label&gt;
&lt;input type=&quot;text&quot; name=&quot;ecoupon_number&quot; id=&quot;ecoupon_number&quot; size=&quot;10&quot; /&gt;&lt;%
End Sub
End Class
%&gt;

</code></pre><p>由上面的程式碼可以看出，我們在子類別只要實作父類別所定義的步驟函式，就能輕鬆地更換流程執行的過程。整個系統架構如下所示：</p>
<p><img src="/resources/ooasp_cart/images/shopping_cart_checkout.png" alt=""></p>
<h2 id="結論">結論</h2>
<p>看完這篇的人，大概十個裡面會有八個說：為什麼要弄得這麼複雜呀！其實這是因為 ASP (VBScript) 不支援繼承的關係；實際上使用支援 OO 特性的語言，要實現 Template Method 就會變得很簡單而且也比較容易懂。當然以上的例子並不能說是非常好，不過我想也許能讓大家瞭解 Template Method 的精神。</p>
<p>Template Method 可以說是把物件導向特性發揮的淋漓盡致：流程的抽象化、步驟的繼承、角色的多型；換句話說，因為 Template Method 已經幫我們定義好了流程骨架，所以如果未來我們有新的相似流程，就不必再跑到 HTML 頁面裡加上一大堆的條件判斷式。而且我們也要清楚地分離流程中的每個步驟，把它們交由子類別來實作；這樣我們就能區隔每個角色應該做的事，而不會被大量的 If Else 給淹沒。</p>
<p>不過應用 Template Method 模式時，還有許多要注意的地方，像是如何切割流程、設計掛勾函式 (Hook) 等，都是很有趣的課題；這些我想很多書上都有寫了，所以我就不再多提了。</p>
<p>還有我其實不應該把 HTML 寫在類別裡面，應該使用樣版引擎將它們分離。不過為了不讓大家搞混，所以我就省略掉了這些枝節。但是我強烈建議實際應用時，不要把 HTML 寫在程式裡面。</p>
<p>註：不要把樣版和 Template Method 搞混了。樣版一般是指 HTML 畫面，而 Template Method 的 Template 有流程骨架的意思。</p>
<h2 id="範例程式下載">範例程式下載</h2>
<p>範例程式裡面包含了上面每個步驟所介紹的結帳表單原始程式，你可以參考壓縮檔案的 README.txt 來得知如何執行它們。</p>
<p><a href="/resources/ooasp_cart/source/ooasp_02_src.rar">下載</a></p>
]]></content>
		</item>
		
		<item>
			<title>[心得] 網頁程式開發建議</title>
			<link>https://jaceju.net/suggestions-for-web-development/</link>
			<pubDate>Mon, 08 May 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/suggestions-for-web-development/</guid>
			<description>這篇是寫給新同事以及我自己備忘用的，也歡迎大家提出指正。 在撰寫網頁程式時，有些基本的東西要非常注意，這樣寫出來的程式才不容易出現問題，也才能</description>
			<content type="html"><![CDATA[<p>這篇是寫給新同事以及我自己備忘用的，也歡迎大家提出指正。</p>
<p>在撰寫網頁程式時，有些基本的東西要非常注意，這樣寫出來的程式才不容易出現問題，也才能夠交給客戶。以下就是我自己的開發心得，供大家參考。</p>
<h2 id="利用明確的指定方式來取得頁面傳來的變數">利用明確的指定方式來取得頁面傳來的變數</h2>
<p>不管是 GET 或 POST 的參數，我會希望明確地指定它的型態，例如 id 應該是整數， content 是文字等。而且我也常常發現有人把接到這些變數後，沒有事先做處理就直接串到程式中，這是非常危險的一件事情！</p>
<p>最明顯的例子就是 SQL Injection 了，我想這應該不用多說了。總而言之，不要過份相信用戶端傳來的資訊，把它轉換成你能掌握的型態再用吧。</p>
<h2 id="資料庫的連結每個頁面只作一次">資料庫的連結每個頁面只作一次</h2>
<p>資料庫連結是網站頁面常做的事情，但是儘可能不要讓一個頁面產生太多的資料庫連結。我們可以利用 Singleton 模式來取得資料庫連結物件，這樣可以確保程式裡只會用到一個資料庫連結；當然需要連結不同資料庫的話就另當別論。</p>
<p>所以我們要確定資料庫會在頁面初始化時連結，頁面結束時關閉。這樣有單一的入口與出口，程式也就不容易出現奇怪的問題。</p>
<h2 id="相同的邏輯不要寫兩遍以上">相同的邏輯不要寫兩遍以上</h2>
<p>如果發現有兩支程式會用到相同的程式邏輯時，不要猶豫，把它抽出來變成類別或函式 (最好是類別，原因在下一則) 。因為如果哪一天需要更新程式的邏輯時，你只需要更改一支程式即可。對健忘的人來說，這點尤其重要。</p>
<p>我就遇過有人把產生選單的邏輯重複寫在十多支程式裡，結果有次客戶要求要修正其中一個地方，可憐的維護人員 (就是我啦) 就得一支一支地去翻出來改。</p>
<h2 id="利用物件來管理錯誤">利用物件來管理錯誤</h2>
<p>我這裡指的錯誤是任何預期中的狀況，也就是你不希望使用者操作的方式，例如編號不存在或是檔案大小超過限制等。</p>
<p>當錯誤發生時，不要立刻結束，應該利用錯誤上升機制來讓通知上一層的程式，最後再由頁面控制程式來決定要如何處理錯誤。我常常會遇到有人在函式裡利用 exit 離開程式，但是這時候頁面的資料庫或其他物件等等都還沒釋放掉；雖然程式平台可能會幫你做，但那總是很難預期。</p>
<p>所以我建議不要使用函式，而改為使用物件的方法，然後利用類似 PEAR::isError() 來判斷是否執行成功；如果失敗的話就把錯誤往上丟，直到頁面能夠控制為止。</p>
<h2 id="該釋放的要記得釋放">該釋放的要記得釋放</h2>
<p>不管是物件還是資料庫，都應該在頁面結束前將它們銷毀或關閉，而不要過於依賴程式執行平台。網頁程式是很多人會同時存取的，如果沒有正確地將資源即時釋放掉的話，久而久之就會造成系統效能上的不穩定。</p>
<p>而釋放的動作要什麼時候做呢？記住一句話：誰開的就誰負責關。例如上面頁面控制程式開啟資料庫連結，那就在頁面控制程式的最後把資料庫連結關閉。類別建構函式產生的物件，就在類別解構程式裡銷毀。函式開的頭，當然就在函式尾收掉；不過有個例外，那就是這個函式如果本身就是要回傳產生的物件時，那就不能把它給釋放囉，而是要改為呼叫這個函式的程式來釋放。</p>
<h2 id="利用樣版技術">利用樣版技術</h2>
<p>樣版是用來分離程式邏輯與視覺頁面的，也常常有人用 MVC 這個模式來稱呼它。然而兩者分離除了不相互干擾外，其實還有一個好處：那就是程式可以在錯誤發生後，決定要顯示的結果。就像上面提到的錯誤管理，當我們在頁面控制程式取得錯誤訊息時，我們就可以而用置換樣版來避免掉頁面的錯誤，或者是導向別的處理程式。</p>
<p>這點我覺得 PHP 的 Smarty 就考慮得很好，因為它是後期頁面綁定 (Binding) ，而不會像傳統樣版引擎在前期就把頁面拉進程式處理，導致錯誤發生時徒然浪費處理時間 (當然要看怎麼設計的) 。</p>
<h2 id="徹底瞭解開發環境的性質">徹底瞭解開發環境的性質</h2>
<p>網頁程式和一般應用程式 (例如視窗應用程式) 在本質上是有差異的，這些差異不僅是在操作上，就連執行的過程都非常的不一樣。雖然現在有 AJAX 或其他技術可以縮小彼此的差距，但是它還是建構在 HTTP 這個無狀態協定之上。身為網站程式開發人員，其實應該要瞭解這些基礎，而不要只熟稔某些已經被包裝過的技術就顯得自得意滿。</p>
<p>最簡單的就是伺服端程式與用戶端程式之間的溝通，例如 PHP 和 JavaScript 。我常常在網路上看見有人問道：要如何讓 PHP 和 JavaScript 之間的變數互通？如果瞭解 HTTP 執行的過程，那麼你就會自己發現這些問題的答案。</p>
]]></content>
		</item>
		
		<item>
			<title>ASP 購物車三部曲(1)</title>
			<link>https://jaceju.net/classic-asp-shopping-cart-1/</link>
			<pubDate>Sun, 23 Apr 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-shopping-cart-1/</guid>
			<description>簡介 物件導向是一種思維，這點我深信不疑。但是在我寫了 ASP 物件設計手法系列文章後，我才發現自己其實深陷在語言的泥淖裡。 我想這裡也許該用一些真正的</description>
			<content type="html"><![CDATA[<h2 id="簡介">簡介</h2>
<p>物件導向是一種思維，這點我深信不疑。但是在我寫了 ASP 物件設計手法系列文章後，我才發現自己其實深陷在語言的泥淖裡。</p>
<p>我想這裡也許該用一些真正的實例來表達我的想法了，也就是我想告訴大家，我心目中的物件導向思維到底是什麼？</p>
<p>前面幾篇的 ASP 物件設計手法或許看起來很神妙，但那只不過是 ASP 原本就有的一些東西。在別的物件導向語言裡，這些手法可能就像呼吸一樣稀鬆平常。所以如果你懂的是別種開發平台 (例如 PHP 、 ASP.NET 或 JSP ) 也沒關係，瞭解物件導向思維的意義後，你大可去發揮那個平台的長處。</p>
<p>當然不一定非得 ASP 不可，我已經不會再去證明 ASP 能不能辦到什麼。只不過我想會寫 Web 網站程式的人大部份應該都懂 ASP ，而且也為了延續之前的主題，所以這裡就繼續用 ASP 了。</p>
<p>註：這裡的 ASP 採用的當然是 VBScript ，你想用 JScript 來做我也不反對。</p>
<p>我將利用一個簡化的購物車程式，來介紹一些我設計購物車程式時的概念，其中會包括先前介紹的 ASP 物件設計手法以及設計模式的應用。</p>
<p>廢話不多說，往下看吧。</p>
<!-- raw HTML omitted -->
<h2 id="購物車">購物車</h2>
<p>開發過稍具規模專案的程式設計師們，我想大部份都寫過購物車程式吧。購物車創造了多少台灣經濟奇蹟我是不清楚，但是至少它一直是網站開發的重要課題之一。</p>
<p>其實購物車的本意很簡單，也就是在使用者瀏覽網站的過程裡，將他所選擇的商品資訊放到記憶體或資料庫中，最後再提供一個畫面顯示他買過哪些東西。</p>
<p>註：也許有人會說購物車應該還有金流物流等機制，不過這裡我想就先略過不提了。我們先把焦點放在最單純的購物車機制上，將基礎打好後再往下繼續。</p>
<h3 id="加入購物車">加入購物車</h3>
<p>在物件導向的思維裡，我們習慣把購物車視成一個物件，而商品當然也是一個一個的物件，這其實和現實生活沒什麼兩樣。把喜歡的商品放到購物車裡，這種動作再自然也不過。當然在程式的世界裡，我們很少會去完整模擬出購物車和商品的樣子 (是誰說網站購物車要有輪子的？) 我們只會把購物車所要具備的功能實作出來而已。</p>
<p>想像一下，你可能會把三包科學麵、兩瓶可樂還有一盒巧克力放到你的購物車裡。</p>
<pre><code>Set oCart = New Shopping_Cart
oCart.AddCartItem &quot;科學麵&quot;, 3
oCart.AddCartItem &quot;瓶裝可樂&quot;, 2
oCart.AddCartItem &quot;盒裝巧克力&quot;, 1

</code></pre><p>在 AddCartItem 中，第一個參數表示商品代碼 (只是這裡我用商品名稱) ，後面則是購買數量。</p>
<p>但是，在較大規模的網站上通常不會那麼簡單地採用這種設計。怎麼說呢？想想看，網站可能會有促銷活動，有些商品可能會有打折。總之你的客戶絕對不會這麼輕易把你晾在那邊，這時我們也許得先「預構」一下。</p>
<p>註：如果你不瞭「預構」那也沒關係，這無損客戶對你極盡壓榨之能事。</p>
<p>雖然我們不想過度設計，預先放入過多未來不確定的功能，但是基本應付變化的能力總是要有的。我在加入購物車時，多了一個商品種類的參數：</p>
<pre><code>Set oCart = New Shopping_Cart
oCart.AddCartItem &quot;科學麵&quot;, 3, &quot;Normal&quot;
oCart.AddCartItem &quot;瓶裝可樂&quot;, 2, &quot;Normal&quot;
oCart.AddCartItem &quot;盒裝巧克力&quot;, 1, &quot;Normal&quot;

</code></pre><p>我為什麼要多出這個參數呢？其用意是什麼？商品種類是很有用的參數，因為有時候他可以告訴購物車，這是一個活動商品還是一般商品。</p>
<p>我知道你想問為什麼我知道會有這些變化，老實說，我一開始的時候也不知道。其實這些都是經由客戶的需求以及過往的經驗去整理出來的；如果客戶擺明未來有一陣子不可能會有這些東西，那就不值得你現在就把它們加上去。</p>
<p>現在看起來是能夠應付變化了，可是這樣好嗎？ AddCartItem 看起來參數太多了！</p>
<p>註：其實我也說不上來這種設計好不好，在我某個專案的第一版 (還存活著) 就是這麼做的。事實上我也許可以重構它，不過那時候我並沒有足夠的單元測試知識支援我做這種事情，所以我放棄了 (但至少維護起來沒那麼困難就是了) 。</p>
<p>這裡我們換個方式好了，我不打算讓 AddCartItem 想太多，畢竟如果以後有新的參數時， AddCartItem 的介面就很難改動，造成往後需要更多動作來處理。我乾脆直接讓 AddCartItem 接受一個購買項目物件好了，雖然在 ASP (VBScript) 裡實現起來有點難看，但在其他環境中我想可以很漂亮地完成。</p>
<pre><code>Set oCart = New Shopping_Cart
Set oP1 = New Shopping_CartItem : oP1.Init &quot;科學麵&quot;, 3, &quot;Normal&quot;
Set oP2 = New Shopping_CartItem : oP2.Init &quot;瓶裝可樂&quot;, 2, &quot;Normal&quot;
Set oP3 = New Shopping_CartItem : oP3.Init &quot;盒裝巧克力&quot;, 1, &quot;Normal&quot;
oCart.AddCartItem oP1
oCart.AddCartItem oP2
oCart.AddCartItem oP3
Set oP1 = Nothing
Set oP2 = Nothing
Set oP3 = Nothing

</code></pre><p>其實我的想法應該是如此：</p>
<pre><code>oCart.AddCartItem(New Shopping_CartItem(&quot;科學麵&quot;, 3, &quot;Normal&quot;))
oCart.AddCartItem(New Shopping_CartItem(&quot;瓶裝可樂&quot;, 2, &quot;Normal&quot;))
oCart.AddCartItem(New Shopping_CartItem(&quot;盒裝巧克力&quot;, 1, &quot;Normal&quot;))

</code></pre><p>當然這個程式是不能用的，因為 ASP (VBScript) 的建構式沒辦法傳參數進去，而且也不公開，所以就忘了它吧。</p>
<h3 id="更新購物車與取得總金額">更新購物車與取得總金額</h3>
<p>在將商品加入購物車時，我們並不會立刻去計算購物車的總金額，而是等到需要時再重新計算整個購物車內的購買項目所包含的價錢。</p>
<p>所以我在上面的測試程式的最後加上更新購物車的流程。</p>
<pre><code>Set oCart = New Shopping_Cart
Set oP1 = New Shopping_CartItem : oP1.Init &quot;科學麵&quot;, 3, &quot;Normal&quot;
Set oP2 = New Shopping_CartItem : oP2.Init &quot;瓶裝可樂&quot;, 2, &quot;Normal&quot;
Set oP3 = New Shopping_CartItem : oP3.Init &quot;盒裝巧克力&quot;, 1, &quot;Normal&quot;
oCart.AddCartItem oP1
oCart.AddCartItem oP2
oCart.AddCartItem oP3
Set oP1 = Nothing
Set oP2 = Nothing
Set oP3 = Nothing
oCart.Update
Response.Write oCart.GetAmount

</code></pre><p>註：其實除了更新整個購物車以外，應該還要能單獨更新某個指定的商品或是清空購物車，不過我想這個就留給你自己去思考吧。</p>
<h3 id="寫出程式">寫出程式</h3>
<p>現在，我們就可以整理出 Shopping_Cart 類別應該要有什麼主要的功能：</p>
<ul>
<li>加入商品 (AddCartItem)</li>
<li>更新購物車 (Update)</li>
<li>取得總金額 (GetAmount)</li>
</ul>
<p>當然，在購物車內部我們應要會需要一個集合物件，用來存放購買項目，而 Dictionary 物件就是最適合的了。至於其他的功能是為了能讓測試能夠正常執行，我們暫時略過不提。</p>
<p>整個購物車的程式如下：</p>
<pre><code>&lt;%
' /class/Shopping/Cart.asp
' 購物車
Class Shopping_Cart
Private CartItemList
Private Fields
' 物件初始化
Private Sub Class_Initialize()
' 我們建立了一個存放購買項目的 Dictionary 物件
' 以及一個存放購物車資訊的 Dictionary 物件
Set CartItemList = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
Fields.Add &quot;Amount&quot;, 0
End Sub
' 加入商品
Public Sub AddCartItem(oCartItem)
' 我們把商品名稱當做索引鍵，實際上則會比較複雜
Dim sKey : sKey = oCartItem.GetField(&quot;Name&quot;)
' 先判斷有沒有相同的商品存在
If Not CartItemList.Exists(sKey) Then
CartItemList.Add sKey, oCartItem
' 已經存在就更改數量
Else
Dim oExistedCartItem
Set oExistedCartItem = CartItemList(sKey)
oExistedCartItem.SetField &quot;Quantity&quot;, _
oExistedCartItem.GetField(&quot;Quantity&quot;) + _
oCartItem.GetField(&quot;Quantity&quot;)
End If
End Sub
' 取得所有商品
Public Function Items()
Items = CartItemList.Items
End Function
' 判斷是否存在某個商品
Public Function Exists(sKey)
Exists = CartItemList.Exists(sKey)
End Function
' 更新購物車
Public Sub Update()
Dim oCartItem
Fields(&quot;Amount&quot;) = 0
For Each oCartItem In CartItemList.Items
oCartItem.Refresh
Fields(&quot;Amount&quot;) = Fields(&quot;Amount&quot;) + oCartItem.GetField(&quot;SubTotal&quot;)
Next
End Sub
' 取得總金額
Public Function GetAmount()
GetAmount = Fields(&quot;Amount&quot;)
End Function
' 物件結束
Private Sub Class_Terminate()
Set CartItemList = Nothing
Set Fields = Nothing
End Sub
End Class
%&gt;

</code></pre><p>如你所見，目前它只是個簡單的購物車，後面我們再來談談它會如何因應變化。</p>
<h2 id="購買項目">購買項目</h2>
<p>購買項目 (CartItem) 和商品 (Product) 是不一樣的概念，商品不會知道自己會屬於什麼活動，也不會知道購買數量的資訊。而購買項目主要是記錄消費者買了些什麼，並買了多少。</p>
<p>由於我們已經寫好了購物車類別，所以我們也能夠很清楚購買項目應該要提供什麼資訊給購物車。包括：</p>
<ul>
<li>初始化商品資料 (Init)</li>
<li>取得指定欄位 (GetField)</li>
<li>更新購買項目的小計金額 (Refresh)</li>
</ul>
<p>購買項目的程式如下：</p>
<pre><code>&lt;%
' /class/Shopping/CartItem.asp
Class Shopping_CartItem
Private Fields
Private Sub Class_Initialize()
Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
Fields.Add &quot;Price&quot;, 0
Fields.Add &quot;Quantity&quot;, 0
Fields.Add &quot;SubTotal&quot;, 0
Fields.Add &quot;EventType&quot;, &quot;&quot;
Fields.Add &quot;EventID&quot;, 0
End Sub
Private Sub Class_Terminate()
Set Fields = Nothing
End Sub
Private Sub SetField(sName, vValue)
If Fields.Exists(sName) Then
Fields(sName) = vValue
End If
End Sub
' 初始化商品資料
Public Sub Init(sName, iQuantity, sEventType)
Dim iPrice
Dim oProduct
Set oProduct = GetObject(&quot;Shopping_MockProduct&quot;)
oProduct.Init sName
' 從資料庫取得資料
If oProduct.Exists Then
iPrice = oProduct.GetPrice
SetField &quot;Price&quot;, iPrice
SetField &quot;Quantity&quot;, iQuantity
SetField &quot;SubTotal&quot;, iPrice * iQuantity
SetField &quot;EventType&quot;, sEventType
SetField &quot;EventID&quot;, iEventID
End If
End Sub
' 取得指定欄位資料
Public Function GetField(sName)
GetField = Null
If Fields.Exists(sName) Then
GetField = Fields(sName)
End If
End Function
' 更新購買項目內容
Public Sub Refresh()
Fields(&quot;SubTotal&quot;) = Fields(&quot;Price&quot;) * Fields(&quot;Quantity&quot;)
End Sub
End Class
%&gt;

</code></pre><p>一般在購買項目初始化時，會從資料庫去取得商品的資料，不過這裡我為了簡化程式，就沒有實際去連結資料庫，而是把資料寫死 (<a href="http://en.wikipedia.org/wiki/Hard_code">hard-code</a>) 在模擬用的商品類別裡。如果到時候真的要連結資料庫時，我只要抽換掉這個假的商品類別，換上會真正抓取資料庫的商品類別即可。</p>
<p>至於模擬用的商品類別 (MockProduct) 程式碼如下：</p>
<pre><code>&lt;%
' /class/Shopping/MockProduct.asp
Class Shopping_MockProduct
Private ProductData
Private Name
' 物件啟動
Private Sub Class_Initialize()
Set ProductData = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
ProductData.Add &quot;科學麵&quot;, 6
ProductData.Add &quot;瓶裝可樂&quot;, 20
ProductData.Add &quot;盒裝巧克力&quot;, 100
Name = &quot;&quot;
End Sub
' 物件結束
Private Sub Class_Terminate()
Set ProductData = Nothing
End Sub
' 商品初始化
Public Sub Init(sName)
Name = sName
End Sub
' 商品是否存在
Public Function Exists()
Exists = ProductData.Exists(Name)
End Function
' 取得價錢
Public Function GetPrice()
GetPrice = ProductData(Name)
End Function
End Class
%&gt;

</code></pre><p>註：為了簡化程式篇幅，我拿掉了許多防呆程式碼。</p>
<p>現在整個程式的架構如下 (我簡略掉一些通用的函式及屬性) ：</p>
<p><img src="/resources/ooasp_cart/images/ooasp_cart_1_1.png" alt="原始購物車架構"></p>
<h2 id="新的購物模式">新的購物模式</h2>
<p>該來的還是會來，客戶總是不會讓我們過好日子的。我們接到了兩個新的需求，折扣活動與贈品活動。規格簡述如下：</p>
<ul>
<li>折扣活動：有部份商品可以有折扣，但是不能修改現有的商品資料，而是要在加入購物車時將金額自動抵扣掉。</li>
<li>贈品活動：如果消費者買了某個指定商品，那麼購物車要自動幫他加入附屬在這個商品下的贈品。</li>
</ul>
<p>註：別懷疑，這兩個行銷手法在實際的網站上是常見的，而且還算簡單，真正難搞的活動我還沒寫出來呢。</p>
<p>為了簡化說明，我把實作規格定義如下：</p>
<ul>
<li>折扣活動：有折扣的商品被歸類於 Discount 分類，而商品價格一律為 9 折。</li>
<li>贈品活動：當我們買了一瓶可樂後，購物車會自動幫我們加上一包科學麵。</li>
</ul>
<p>在贈品活動的部份，我希望贈品的價格是 0 ，所以在我 Shopping_MockProduct 類別初始化時加入了一個價格為 0 的「科學麵(贈品)」：</p>
<pre><code>  ' 物件啟動
Private Sub Class_Initialize()
Set ProductData = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
ProductData.Add &quot;科學麵&quot;, 6
ProductData.Add &quot;科學麵(贈品)&quot;, 0
ProductData.Add &quot;瓶裝可樂&quot;, 20
ProductData.Add &quot;盒裝巧克力&quot;, 100
Name = &quot;&quot;
End Sub

</code></pre><p>其實我希望除了這兩個活動以外，我的購物車能夠應付其他類型的活動，因此我在 AddCartItem 呼叫了一個函式，用來檢查商品所屬的活動，也就是說我把檢查的動作從 AddCartItem 中獨立出來：</p>
<pre><code>  ' 加入商品
Public Sub AddCartItem(oCartItem)
' 我們把商品名稱當做索引鍵，實際上則會比較複雜
Dim sKey : sKey = oCartItem.GetField(&quot;Name&quot;)
' 先判斷有沒有相同的商品存在
If Not CartItemList.Exists(sKey) Then
CartItemList.Add sKey, oCartItem
' 檢查商品是否屬於某個活動
CheckCartItemForEvent oCartItem
' 已經存在就更改數量
Else
Dim oExistedCartItem
Set oExistedCartItem = CartItemList(sKey)
oExistedCartItem.SetField &quot;Quantity&quot;, _
oExistedCartItem.GetField(&quot;Quantity&quot;) + _
oCartItem.GetField(&quot;Quantity&quot;)
End If
End Sub

</code></pre><p>CheckCartItemForEvent 函式主要會判斷商品的活動型態，以處理對應的動作，其程式如下：</p>
<pre><code>  ' 檢查是否為某個活動的商品
Private Sub CheckCartItemForEvent(oCartItem)
Select Case oCartItem.GetField(&quot;EventType&quot;)
' 折扣活動
Case &quot;Discount&quot;
oCartItem.SetField &quot;Price&quot;, oCartItem.GetField(&quot;Price&quot;) * 0.9
oCartItem.Refresh
' 贈品活動
Case &quot;Gift&quot;
If &quot;瓶裝可樂&quot; = oCartItem.GetField(&quot;Name&quot;) Then
Dim oGiftCartItem
Set oGiftCartItem = GetObject(&quot;Shopping_CartItem&quot;)
oGiftCartItem.Init &quot;科學麵(贈品)&quot;, 1, &quot;Normal&quot;
AddCartItem oGiftCartItem
End If
' 一般商品，什麼也不用作
Case Else
End Select
End Sub

</code></pre><p>因為 CheckCartItemForEvent 不會被外部呼叫，所以我用 Private 來指定它的存取權限。</p>
<h2 id="strategy-模式">Strategy 模式</h2>
<p>雖然現在 CheckCartItemForEvent 函式看起來還不是很龐大，不過既然已經有三個不同的活動了 (一般商品也可歸屬為一種活動)，當然還會可能有第四個或第五個。可想而知未來 CheckCartItemForEvent 會逐漸變得落落長，超出我們所能理解的範圍。</p>
<p>如果我們能把活動機制獨立在購物車外是不是比較好呢？這樣也能使得購物車不再背負過多的商業邏輯，在開發上也能靈活許多。</p>
<p>首先我定義一個虛假的活動類別好了，我們要在概念上繼承它 (就是 Duck Typing 啦) ，這個類別的定義如下：</p>
<pre><code>&lt;%
Class Event
' 處理購買項目
Public Sub Check(oCartItem, oCart)
End Sub
End Class
%&gt;

</code></pre><p>在 Event 虛擬類別中，我們提供了一個 Check 函式，用來檢查即將要加入的商品 oCartItem 。不過為什麼還需要一個 oCart 參數呢？這是因為某些活動可能會需要購物車內的其他商品，所以我們就把購物車物件傳進來讓 Event 自行應用，稍後我們就能看到實際的例子。</p>
<p>接著我建立一個處理一般商品的活動類別 Event_Normal，然而這個物件其實什麼也不做：</p>
<pre><code>&lt;%
' /class/Event/Normal.asp
Class Event_Normal ' Extends Event
' 處理購買項目
Public Sub Check(oCartItem, oCart)
End Sub
End Class
%&gt;

</code></pre><p>當然我們還有兩種活動類別，先看看 Discount 好了，它比較簡單：</p>
<pre><code>&lt;%
' /class/Event/Discount.asp
Class Event_Discount ' Extends Event
' 處理購買項目
Public Sub Check(oCartItem, oCart)
oCartItem.SetField &quot;Price&quot;, oCartItem.GetField(&quot;Price&quot;) * 0.9
oCartItem.Refresh
End Sub
End Class
%&gt;

</code></pre><p>你可以看到我用了一個很簡單的做法：把原來的值乘以 0.9 後，再將它設回 oCartItem 的單價裡。</p>
<p>註：注意！ ASP 的物件是以傳參考的方式來傳遞的，所以這邊只要一更新，就會直接反應回購物車內所對應的 CartItem 物件。</p>
<p>剩下的 Gift 類別就比較複雜了，剛剛定義的 oCart 現在就派上用場了。</p>
<pre><code>&lt;%
' /class/Event/Gift.asp
Class Event_Gift ' Extends Event
' 處理購買項目
Public Sub Check(oCartItem, oCart)
If &quot;瓶裝可樂&quot; = oCartItem.GetField(&quot;Name&quot;) Then
Dim oGiftCartItem
Set oGiftCartItem = GetObject(&quot;Shopping_CartItem&quot;) : oGiftCartItem.Init &quot;科學麵(贈品)&quot;, 1, &quot;Normal&quot;
oCart.AddCartItem oGiftCartItem
End If
End Sub
End Class
%&gt;

</code></pre><p>當 Event_Gift 發現 oCartItem 的 Name 是「瓶裝可樂」時，就會建立一個「科學麵(贈品)」的 CartItem 物件， 並利用 oCart 的 AddCartItem 把這個贈品再加上去。</p>
<p>好，現在三種活動類別都已經定義好了，我們就來看看怎麼用吧。</p>
<p>我把原來的 CheckCartItemForEvent 改成以下的形式：</p>
<pre><code>  ' 檢查是否為某個活動的商品
Private Sub CheckCartItemForEvent(oCartItem)
Dim oEvent
Select Case oCartItem.GetField(&quot;EventType&quot;)
' 折扣活動
Case &quot;Discount&quot;
        Set oEvent = GetObject(&quot;Event_Discount&quot;)
' 贈品活動
Case &quot;Gift&quot;
        Set oEvent = GetObject(&quot;Event_Gift&quot;)
' 一般商品
Case Else
        Set oEvent = GetObject(&quot;Event_Normal&quot;)
End Select
    oEvent.Check oCartItem, Me
End Sub

</code></pre><p>發現什麼沒有？我們竟然可以用同樣的 Check 函式來檢查商品！這就是所謂的 Strategy 模式！</p>
<p>Strategy 模式主要的概念就是可抽換的策略，也就是在程式執行的階段裡，我們能用同一種操作方式 (如上面的 Check 函式) 來對資料進行不同的運算條件，而這些運算邏輯則定義在不同的策略類別裡，就像上面的 Event_xxx 類別。</p>
<p>在完整支援物件導向技術的語言中，這些策略類別應該是繼承自一個抽象類別或介面 (就是我們上面提到的虛擬 Event 類別) ，而 Event 的 Check 函式就是需要被實作的了。就因為 Event 類別所衍生的類別在實作上是不同的，所以我們才能用同一個 Check 函式來對應到不同的活動條件。</p>
<p>註：話說回來， ASP (VBScript) 既然沒有繼承，所以只要是有實作相同函式介面的類別所產生的實體，都有可能被拿來當做是策略物件。這點是 ASP (VBScript) 自由卻也是不合物件導向語言規範的地方。</p>
<p>另外注意一點，就是 Me 關鍵字的應用。我利用 Me 關鍵字把 CheckCartItemForEvent 所在這個物件 (也就是 Shopping_Cart 的實體) ，傳遞給 Event_xxx 物件，這樣這些 Event_xxx 物件就是利用 oCart 參數來存取目前的購物車內容。這種方式稱為 delegate (委託) ， Shopping_Cart 物件拜託 Event 物件做一些事情，所以要把自己 (Me) 託付給它。這在委託者缺少某些功能，而需要外部物件協助時非常有用。</p>
<p>和前面一樣，我簡單地把加入活動類別的架構圖描繪如下：</p>
<p><img src="/resources/ooasp_cart/images/ooasp_cart_1_2.png" alt="新的購物車架構"></p>
<h2 id="factory-method-模式">Factory Method 模式</h2>
<p>在上面的程式裡，我們還是在 CheckCartItemForEvent 函式中參雜了 Select Case 敘述，這使得我們到時候要新增活動時，還是得回來加上新的活動類別建構程式。有沒有什麼方法可以避免這些動作呢？或是直接拿掉 Select Case 敘述呢？</p>
<p>我相信大家也看出一些端倪了。我所用的類別代號 Normal 、 Gift 及 Discount ，其實就對應到 Event 的實作類別名稱。這表示我們能夠利用類別代號來產生我們所需要的物件，但是怎麼做呢？</p>
<p>從上面的程式裡，我們可以看到 GetObject 需要一個類別名稱來當做引數。這個引數剛好是用 Event_ 加上類別代碼，所以我們就能夠利用這點來下手。</p>
<p>我把剛剛的 Select Case 換成以下粗體的部份：</p>
<pre><code>' 檢查是否為某個活動的商品
Private Sub CheckCartItemForEvent(oCartItem)
Dim oEvent
Set oEvent = CreateEvent(oCartItem.GetField(&quot;EventType&quot;))
oEvent.Check oCartItem, Me
End Sub

</code></pre><p>現在我們就要思考 CreateEvent 函式要丟出什麼東西，以下是我的實作：</p>
<pre><code>' 取得活動商品
Private Function CreateEvent(sEventType)
Set CreateEvent = Nothing
On Error Resume Next
Set CreateEvent = GetObject(&quot;Event_&quot; &amp;amp; sEventType)
If CreateEvent Is Nothing Then
  Set CreateEvent = GetObject(&quot;Event_Normal&quot;)
End If
On Error Goto 0
End Function

</code></pre><p>你可以看到我用之前介紹過的 Factory Method 手法來完成建構活動物件的過程，利用錯誤延遲機制讓不存在的活動類別一律視為一般商品來處理，避免掉一些不必要的麻煩。</p>
<p>Factory Method 最大的好處就是能夠隱藏物件建構的過程，讓呼叫它的程式能夠保持簡單及一致性。它可以把建構物件的複雜邏輯封裝在別的類別裡面，使得我們能夠重複使用這個類別來產生我們所要的東西，也就是能更簡單地把建構物件自動化。不過在這裡，你會發現到我並沒有再做出一個類別來放置 Factory Method ，為什麼呢？</p>
<p>這要歸功於能把字串當做程式敘述的 Execute (ExecuteGlobal) 指令；在其他不支援此特性的語言裡，類似 Select Case 的敘述在 Factory Method 中通常是不可避免的；然而在 ASP (VBScript 、 JavaScript) 或 PHP 等語言平台中， Factory Method 因為這些語言支援把字串變成程式指令的特性，所以在實作上就變得更有彈性。</p>
<p>註：把字串當做程式敘述這種方式通常會有安全性上的議題出現，所以寫程式時要特別小心。</p>
<h2 id="結論">結論</h2>
<p>回頭思考看看，現在如果又有一個新的活動，我們已經不必再更改 Shopping_Cart 類別裡面的程式；只要新增一個 Event_xxx 類別，實作該活動的規則，就能讓購物車處理這些活動商品，這樣的彈性就是物件導向思維所帶來的好處。</p>
<p>也許你會想，這我用函式也能做到呀！其實也沒錯。不過這裡我要強調一件事，不管你用什麼方式開發，只要這些程式在你交接後還可能存活時，最後一定要考慮到程式的延展性。我採用物件導向思維及開發手法是為了更容易表達我對這個系統的概念，而且也讓整個系統就在物件交互作用之下，完成了許多看似複雜的工作。</p>
<p>另外我建議把這些開發的概念撰寫成文件，讓後續維護的人員能夠進入狀況，就像這篇文章一樣。他們不一定能夠清楚為什麼要採用這樣的方式，所以透過類似這篇文章的說明，他們就能一步一步瞭解開發者所建立的系統輪廓。</p>
<p>我希望透過我的介紹，能夠讓我的伙伴瞭解物件導向是怎麼一回事。而它到底是不是真的必須依賴語言？還是協助我們把心目中的系統架構描繪出來的一種設計手法？我想這邊就留給大家去思考，畢竟我不是大師，這種問題我無法解答；我能夠做的就是把這些概念實際轉換成程式碼，讓系統能夠順利運作而已。</p>
<h2 id="範例程式下載">範例程式下載</h2>
<p>範例程式不是個可用的購物車，它只包含了部份的商業邏輯而已，所以想要直接拿它來改寫的朋友可能會失望。不過裡面我用到了 ASPUnit ，有興趣學習如何撰寫單元測試的朋友可以參考看看。另外裡面包含了上面每個步驟所介紹的購物車原始程式，你可以參考壓縮檔案的 README.txt 來得知如何執行它們。</p>
<p><a href="/resources/ooasp_cart/source/ooasp_01_src.rar">下載</a></p>
]]></content>
		</item>
		
		<item>
			<title>很有趣的 Fluent Interface</title>
			<link>https://jaceju.net/fluent-interface/</link>
			<pubDate>Tue, 07 Mar 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/fluent-interface/</guid>
			<description>在研究 Zend Framework Preview 的文件時，發現了一個很有趣的 PHP 寫法： $select-&amp;gt;from(&#39;round_table&#39;, &#39;*&#39;) -&amp;gt;where(&#39;noble_title = ?&#39;, &#39;Sir&#39;) -&amp;gt;order(&#39;first_name&#39;) -&amp;gt;limit(10, 20); 看出來沒？除了 from 函式以外，每一個函式都直接接續著上一個函式。怎麼辦到的呢？</description>
			<content type="html"><![CDATA[<p>在研究 <a href="http://framework.zend.com/">Zend Framework Preview</a> 的文件時，發現了一個很有趣的 PHP 寫法：</p>
<pre><code>$select-&gt;from('round_table', '*')
       -&gt;where('noble_title = ?', 'Sir')
       -&gt;order('first_name')
       -&gt;limit(10, 20);

</code></pre><p>看出來沒？除了 from 函式以外，每一個函式都直接接續著上一個函式。怎麼辦到的呢？</p>
<!-- raw HTML omitted -->
<p>我一開始以為這是 PHP 的特異功能，所以我先試了以下程式：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">class</span> <span class="nc">TestA</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">a</span><span class="p">()</span> <span class="p">{}</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">b</span><span class="p">()</span> <span class="p">{}</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">c</span><span class="p">()</span> <span class="p">{}</span>
<span class="p">}</span>
<span class="nv">$a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TestA</span><span class="p">();</span>
<span class="nv">$a</span><span class="o">-&gt;</span><span class="na">a</span><span class="p">()</span>
  <span class="o">-&gt;</span><span class="na">b</span><span class="p">()</span>
  <span class="o">-&gt;</span><span class="na">c</span><span class="p">();</span>

</code></pre></div><p>結果是不行的，然後我想了一下，如果把物件自己的參考 ($this) 丟出來不就行了？</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">class</span> <span class="nc">TestA</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">a</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="nv">$this</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">b</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="nv">$this</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="k">function</span> <span class="nf">c</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="nv">$this</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
<span class="nv">$a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TestA</span><span class="p">;</span>
<span class="nv">$a</span><span class="o">-&gt;</span><span class="na">a</span><span class="p">()</span>
  <span class="o">-&gt;</span><span class="na">b</span><span class="p">()</span>
  <span class="o">-&gt;</span><span class="na">c</span><span class="p">();</span>

</code></pre></div><p>果然成功了，再回頭去看 Zend Framework 的做法，的確也是這樣寫，真是一種有趣的寫法。</p>
<p>不過這種方式我想應該只適用於原本就不傳回值的函式，那種要傳回其他資訊或物件的函式就沒法這樣玩了。</p>
<p>註：這個方法應該只有 PHP 5 才能用，PHP 4 好像不能直接用函式來當做物件參考；有錯的話請指正我。</p>
<p>補充： ASP (VBScript) 和 JavaScript 也可以這樣寫喔。</p>
<p>ASP (VBScript) 版：</p>
<pre><code>&lt;%
Class TestA
    Public Function A()
        Response.Write &quot;A&quot;
        Set A = Me
    End Function
    Public Function B()
        Response.Write &quot;B&quot;
        Set B = Me
    End Function
    Public Function C()
        Response.Write &quot;C&quot;
        Set C = Me
    End Function
End Class
Dim oA : Set oA = New TestA
oA.A().B().C()
%&gt;

</code></pre><p>這裡是 JavaScript 的動態版：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">TestA</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">alert</span><span class="p">(</span><span class="s1">&#39;Aa&#39;</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">b</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">alert</span><span class="p">(</span><span class="s1">&#39;Ab&#39;</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">c</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
        <span class="nx">alert</span><span class="p">(</span><span class="s1">&#39;Ac&#39;</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="nx">a</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TestA</span><span class="p">();</span>
<span class="nx">a</span><span class="p">.</span><span class="nx">a</span><span class="p">().</span><span class="nx">b</span><span class="p">().</span><span class="nx">c</span><span class="p">();</span>

</code></pre></div><p>然後這是 JavaScript 的靜態版：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">TestB</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{}</span>
<span class="nx">TestB</span><span class="p">.</span><span class="nx">a</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">alert</span><span class="p">(</span><span class="s1">&#39;Ba&#39;</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">TestB</span><span class="p">.</span><span class="nx">b</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">alert</span><span class="p">(</span><span class="s1">&#39;Bb&#39;</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">TestB</span><span class="p">.</span><span class="nx">c</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">alert</span><span class="p">(</span><span class="s1">&#39;Bc&#39;</span><span class="p">);</span>
    <span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">d</span><span class="p">)</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">TestB</span><span class="p">.</span><span class="nx">d</span> <span class="o">=</span> <span class="s1">&#39;abc&#39;</span><span class="p">;</span>
<span class="nx">TestB</span><span class="p">.</span><span class="nx">a</span><span class="p">().</span><span class="nx">b</span><span class="p">().</span><span class="nx">c</span><span class="p">();</span>
<span class="c1">// b = new TestB();
</span><span class="c1">// b.a().b().c(); // 這是不能執行的，不能透過物件取用靜態方法。
</span></code></pre></div><p>其實 JavaScript 裡也已經有類似的用法，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">s1</span> <span class="o">=</span> <span class="s1">&#39;ABC&#39;</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">s2</span> <span class="o">=</span> <span class="s1">&#39;DEF&#39;</span><span class="p">;</span>
<span class="nx">alert</span><span class="p">(</span><span class="nx">s1</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="nx">s2</span><span class="p">).</span><span class="nx">toLowerCase</span><span class="p">());</span>
</code></pre></div><p>註：原來這些都已經是別的物件導向語言常用的寫法，可見我還有很多要學的呢。</p>
]]></content>
		</item>
		
		<item>
			<title>ASP 物件設計手法 (6) - 單元測試</title>
			<link>https://jaceju.net/classic-asp-unit-testing/</link>
			<pubDate>Mon, 20 Feb 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-unit-testing/</guid>
			<description>ASP 上的單元測試 沒錯，你沒看錯， ASP 也有單元測試。 什麼是單元測試呢？我想使用 Java 或 .NET 來開發程式的朋友們一定很熟悉。我這裡僅簡單說明一下它的原理，至</description>
			<content type="html"><![CDATA[<h2 id="asp-上的單元測試">ASP 上的單元測試</h2>
<p>沒錯，你沒看錯， ASP 也有單元測試。</p>
<p>什麼是單元測試呢？我想使用 Java 或 .NET 來開發程式的朋友們一定很熟悉。我這裡僅簡單說明一下它的原理，至於深入的介紹，請大家自行去找 XP (eXtreme Programming ，中文常譯為「極致編程」) 相關書籍吧。</p>
<p>註：建議你去看看<a href="http://www.dotspace.idv.tw/">點空間</a>裡有篇文章叫「<a href="http://140.109.17.94/xp/2002/JUnit_test.htm">測試的概念</a>」，寫得滿簡單易懂的。</p>
<p>在 ASP 上面要進行單元測試，首先就要有單元測試框架 (Unit Testing Framework) 。在網路上我找到了以下兩種框架：</p>
<ul>
<li><a href="http://aspunit.sourceforge.net/">ASPUnit</a></li>
<li><a href="http://sourceforge.jp/projects/aspunit/">ASPunit</a> (這是日本人開發的。)</li>
</ul>
<p>它們的名字差在 U 的大小寫。</p>
<p>日本人開發的 <!-- raw HTML omitted -->ASPunit<!-- raw HTML omitted --> 架構比較複雜，而 <!-- raw HTML omitted -->ASPUnit<!-- raw HTML omitted --> 的比較簡單易懂，而且也容易使用，所以本篇將圍繞在 <!-- raw HTML omitted -->ASPUnit<!-- raw HTML omitted --> 這個測試框架上。</p>
<!-- raw HTML omitted -->
<h2 id="下載與安裝">下載與安裝</h2>
<p>你可以到 <a href="http://aspunit.sourceforge.net/">ASPUnit 的官方網站</a>去下載 ASPUnit ，目前是 0.9.2 版。你也可以用我改過的<a href="/resources/aspunit/ASPUnit_zhtw.zip">這個中文版本</a>，它已通過了 XHTML 驗證。</p>
<p>註：我只是將裡面的文字翻譯成中文，並不會影響功能的執行。</p>
<p>ASPUnit 就是用 ASP 開發的，所以一定要有可以執行 ASP 的環境，我的環境是 IIS 6.0 。我不確定這個能不能在 Apache 上的 ASP 模組正常執行，有興趣的朋友如果試成功的話麻煩告知我一下。</p>
<p>另外如果你下載了官方版本，解開後請放在網站根目錄下的 aspunit 資料夾下，否則你得先修改程式裡的 CSS 路徑。</p>
<p>檔案： include\ASPUnitRunner.asp</p>
<pre><code>... 略 ...
Const STYLESHEET = &quot;/aspunit/include/ASPUnit.css&quot; ' 改這裡
... 略 ...

</code></pre><p>至於我修改的版本就沒多大關係了，裡面的路徑是用相對的。</p>
<p>註：不過我建議大家用絕對路徑的方式來安裝，因為你可能同時會有多個專案需要用到它。例如裝在 /ASPUnit 底下會是個不錯的選擇，這樣就能夠使用 <!-- raw HTML omitted --> 的方式來引入測試框架檔案。</p>
<h2 id="為什麼要測試">為什麼要測試</h2>
<p>想像一下，當你寫好一個 ASP 類別時，你如何知道它執行結果是不是正確的呢？舉個簡單的例子，假設我們有個商品類別好了，它的程式碼如下：</p>
<p>檔案： Product.asp</p>
<pre><code>&lt;%
Class Product
    Private Name
    Private Price
    Private Discount
    Public Sub Init(sName, iPrice, uDiscount)
        Name = sName
        Price = iPrice '* uDiscount
    End Sub
    Public Function GetName()
        GetName = Name
    End Function
    Public Function GetPrice()
        GetPrice = Price
    End Function
End Class
%&gt;

</code></pre><p>想知道它是不是能動作，一般我們會這麼寫：</p>
<p>檔案： TestProduct1.asp</p>
<pre><code>&lt;!-- #include file=&quot;Product.asp&quot; --&gt;
&lt;%
Dim oProduct1 : Set oProduct1 = New Product : oProduct1.Init &quot;商品1&quot;, 100, 1    ' 不打折
Dim oProduct2 : Set oProduct2 = New Product : oProduct2.Init &quot;商品2&quot;, 120, 0.9  ' 打 9 折
Response.Write oProduct1.GetName &amp;amp; &quot; 的價格是 &quot;
Response.Write oProduct1.GetPrice &amp;amp; &quot;&lt;br /&gt;&quot;
Response.Write oProduct2.GetName &amp;amp; &quot; 的價格是 &quot;
Response.Write oProduct2.GetPrice &amp;amp; &quot;&lt;br /&gt;&quot;
%&gt;

</code></pre><p>輸出：</p>
<pre><code>商品1 的價格是 100
商品2 的價格是 120

</code></pre><p>但是這種驗證方式每次都得依賴我們人腦去判斷輸出的結果是否在我們的預期之內，而所謂的預期結果則存放在我們的腦海裡，時間一久，也許就會忘了它是不是對的。我們希望測試程式能夠自動檢查執行結果是不是正確的，而且能夠將測試結果用比較人性化的方式輸出。這樣一來就不必面對一堆奇怪的輸出訊息而手足無措。</p>
<p>這些工作 ASPUnit 這個測試框架都幫我們作好了，雖然一樣要寫上面的測試程式，但是在 ASPUnit 中，我們不必自行輸出這些結果來判斷。我們可以預先把我們預期會得到的結果告訴 ASPUnit ，讓它來決定測試的結果是不是正確的；如果一個測試正確了，畫面就會給我們一個綠色光棒 (後面會詳述) ，一旦全都是綠色光棒時，我們就能確定所有的測試都成功了！</p>
<p>註：綠色光棒在 Java 的 JUnit 測試框架是測試成功的意思，是很有趣也很有意義的一種表示方法。</p>
<p>接下來，我將介紹如何撰寫一個能讓 ASPUnit 解讀的測試程式。</p>
<h2 id="使用-aspunit-測試框架">使用 ASPUnit 測試框架</h2>
<h3 id="基本結構">基本結構</h3>
<p>先來介紹一下 ASPUnit 的基本結構，這樣在撰寫測試時比較能夠清楚自己在做什麼。</p>
<p>你也許已經看過 Java 的 JUnit 測試框架的 UML 圖了，其實 ASPUnit 也是基於 JUnit 的一套測試框架，因此在概念與實作上都非常類似。下面的 UML 圖中，我把 ASPUnit 比較重要的幾個類別畫了出來。</p>
<p><img src="/resources/aspunit/images/001.gif" alt=""></p>
<p>其中 Test 介面是虛擬的 (實際上並不存在) ，因為 ASP (VBScript) 並沒有介面 (interface) 這種東西，所以就忘了這件事吧，我們朝向 Duck Typing 前進。 (Duck Typing 的意思是說「如果它走起路來像鴨子，叫起來也像鴨子，那麼它一定是鴨子！」)</p>
<p>那麼為什麼要有一個 Test 介面？因為 Test 介面包含了一個 Run 函式，以便 ASPUnitRunner 能夠正確呼叫。因此在意義的表達上， Test 介面是不可或缺的 (只是這裡是用 Duck Typing ) 。</p>
<p>而實作了 Test 介面的類別，我們稱為測試容器。測試容器是什麼呢？這裡指的就是 TestCase 和 TestSuite 這兩個類別。 TestCase 會包含數個測試案例，算是比較小的容器； TestSuite 則是更大的測試容器，任何實作了 Run 函式的測試容器都可以放到 TestSuite 裡面，包含它自己。</p>
<p>註：上面說的就是 Composite 這個著名的設計模式 (Design Patterns) ！簡單來說就是大的包小的，不然就自己包自己，然後大家都一視同仁 (都是 Test 介面) 。在這篇 <a href="http://junit.sourceforge.net/doc/cookstour/cookstour.htm">JUnit A Cook&rsquo;s Tour</a> 文章中，提到了 JUnit 用了那些設計模式，有興趣的朋友可以參考看看。</p>
<h3 id="建立測試">建立測試</h3>
<p>在 ASPUnit 中，最簡單的測試容器 (Test Container) 類別如下，它會繼承 TestCase ： (當然又是 Duck Typing 。)</p>
<p>檔案： Sample/ProductTest.asp</p>
<pre><code>&lt;%
Class ProductTest ' Extends TestCase
    Public Function TestCaseNames()
        TestCaseNames = Array()
    End Function
    Public Sub SetUp()
    End Sub
    Public Sub TearDown()
    End Sub
End Class
%&gt;

</code></pre><p>TestCaseNames 這個函式會回傳一個包含測試案例名稱的陣列，如果你的測試案例沒放在這裡面，那麼 ASPUnitRunner 就不會執行任何測試。所以這個類別雖然能執行，但也看不出什麼結果。至於 SetUp 及 TearDown 則一定要有，後面我們會說明它們的用途。</p>
<p>現在我們把要測試的類別引入，請在 ProductTest.asp 的第一行加上：</p>
<pre><code>&lt;!-- #include file=&quot;Product.asp&quot;--&gt;

</code></pre><p>然後為 ProductTest 類別建立一個新的公開函式：</p>
<pre><code>Public Sub TestProduct(oTestResult)
    Dim oProduct : Set oProduct = New Product
    oTestResult.AssertExists oProduct, &quot;物件不存在！&quot;
    Set oProduct = Nothing
End Sub

</code></pre><p>這個公開函式就是一個測試案例，它必須帶入一個 TestResult 物件，這個 TestResult 物件會提供下列方法幫我們分析記錄測試的結果：</p>
<!-- raw HTML omitted -->
<p>Assert 就是斷言的意思，它表示我們認為程式到這裡的執行結果應該為何。而 sMessage 則是當結果不如我們預期時，我們想要顯示的訊息。如果測試成功的話，就只會顯示測試成功。而上面的測試案例是說，我們先建立一個 Product 物件，並斷言此物件是存在的。</p>
<p>寫好測試案例 (函式) 後，記得將它的函式名稱放到 TestCaseNames 的陣列裡：</p>
<pre><code>TestCaseNames = Array(&quot;TestProduct&quot;)

</code></pre><p>這樣 ASPUnitRunner 才能夠知道要測試什麼。</p>
<p>最後的整個測試容器程式如下：</p>
<p>檔案： Sample/ProductTest.asp</p>
<pre><code>&lt;!-- #include file=&quot;Product.asp&quot; --&gt;
&lt;%
Class ProductTest ' Extends TestCase
    Public Function TestCaseNames()
        TestCaseNames = Array(&quot;TestProduct&quot;)
    End Function
    Public Sub SetUp()
    End Sub
    Public Sub TearDown()
    End Sub
    Public Sub TestProduct(oTestResult)
        Dim oProduct : Set oProduct = New Product
        oTestResult.AssertExists oProduct, &quot;物件不存在！&quot;
        Set oProduct = Nothing
    End Sub
End Class
%&gt;

</code></pre><h3 id="執行測試">執行測試</h3>
<p>好了，當有了測試程式，怎麼讓它跑起來呢？基本上我們就是要用 UnitRunner 去執行所有的測試，請看以下程式：</p>
<p>檔案： Sample/Go.asp</p>
<pre><code>&lt;%
Option Explicit
%&gt;
&lt;!-- #include file=&quot;../include/ASPUnitRunner.asp&quot;--&gt;
&lt;!-- #include file=&quot;ProductTest.asp&quot;--&gt;
&lt;%
Dim oRunner
Set oRunner = New UnitRunner
oRunner.AddTestContainer New ProductTest
oRunner.Display()
%&gt;

</code></pre><p>首先，我們得先引入 UnitRunner 類別，然後引入要測試的容器類別檔。</p>
<p>接著建立一個 UnitRunner 物件及容器物件 (也就是 ProductTest) ，然後把容器物件放到 UnitRunner 裡面。</p>
<p>最後把呼叫 UnitRunner 的 Display 函式，以顯示執行介面。</p>
<p>執行此程式，我們會得到下圖：</p>
<p><img src="/resources/aspunit/images/002.gif" alt=""></p>
<p>整個 ASPUnitRunner 的執行介面分成了兩個部份：上方的控制台和下方的執行結果。我們可以選擇要測試的容器類別及該容器裡所包含的測試案例，而且也可以選擇是否顯示已經成功通過測試的案例。</p>
<p>但是先別管其他設定，直接按下「執行測試」，就會看到：</p>
<p><img src="/resources/aspunit/images/003.gif" alt=""></p>
<p>表示我們的測試成功了！</p>
<h2 id="進行多個測試案例">進行多個測試案例</h2>
<p>前面提過一個測試容器裡面可以包含數個測試案例，而上面的 TestProduct 函式就是一個測試案例。當我們在撰寫測試時，會把被測試類別裡的每一個方法視為一個單元 (注意：這是狹義的解釋) ，每個測試案例都是只針對一個方法來撰寫。測試案例的名稱在 ASPUnit 中是不重要的，只要能夠表達出想測試什麼即可。但是一般還是會按照慣例，使用 TestXXX 的方式來命名。</p>
<p>註：在日本人開發的 ASPunit 中，應該會自動取得 TestXXX 來執行，而不用我們自行加在 TestCaseNames 中。像 JUnit 也是，只要命名成 testXXX() ，那麼測試框架就會自動取得這些測試案例。</p>
<p>我們將 ProductTest 加入以下的測試案例：</p>
<pre><code>Public Sub TestGetName(oTestResult)
    Dim oProduct : Set oProduct = New Product
    oProduct.Init &quot;商品1&quot;, 100, 1
    oTestResult.AssertEquals &quot;商品1&quot;, oProduct.GetName, &quot;名稱不同！&quot;
    Set oProduct = Nothing
End Sub
Public Sub TestGetPrice(oTestResult)
    Dim oProduct : Set oProduct = New Product
    oProduct.Init &quot;商品1&quot;, 100, 1
    oTestResult.AssertEquals 100, oProduct.GetPrice, &quot;價格不同！&quot;
    Set oProduct = Nothing
End Sub

</code></pre><p><!-- raw HTML omitted -->注意！在 AssertEquals 函式裡，預期結果要在前面！實際結果在後面！我常會不小心犯下這種錯誤。<!-- raw HTML omitted --></p>
<p>當然，別忘了把這兩個測試案例的名稱放到 TestCaseNames 中：</p>
<pre><code>TestCaseNames = Array(&quot;TestProduct&quot;, &quot;TestGetName&quot;, &quot;TestGetPrice&quot;)

</code></pre><p>接著再執行 Go.asp ：</p>
<p><img src="/resources/aspunit/images/004.gif" alt=""></p>
<p>我們可以看到，三個測試都過了。</p>
<h2 id="失敗的測試">失敗的測試</h2>
<p>但是測試過了，不表示一切都完美了，有可能我們根本沒有測試到重點。現在我們再加入一個測試，它會將建立一個打九折的商品，而原價是 100 元，而打折後應該是 90 元。我們說過，要讓測試自動化，所以預期結果 90 元就應該寫在測試中。</p>
<pre><code>Public Sub TestGetDiscountedPrice(oTestResult)
    Dim oProduct : Set oProduct = New Product
    oProduct.Init &quot;商品2&quot;, 100, 0.9
    oTestResult.AssertEquals 90, oProduct.GetPrice, &quot;價格不同！&quot;
    Set oProduct = Nothing
End Sub

</code></pre><p>別忘了加入 TestCaseName 陣列，再執行一次：</p>
<p><img src="/resources/aspunit/images/005.gif" alt=""></p>
<p>啥米！竟然失敗了！和我們預期的結果不同，價格仍然是 100 元！為什麼呢？</p>
<p>回到我們的 Product 類別，因為 GetPrice 剛剛已經通過測試，所以想必問題應該會是在 Init 這個初始化函式上：</p>
<pre><code>Public Sub Init(sName, iPrice, uDiscount)
    Name = sName
    Price = iPrice '* uDiscount
End Sub

</code></pre><p>看到沒？在乘法符號前竟然多了個單引號，使得後面的運算變成了註解 (雖然這是我故意的) 。把這個單引號去掉之後再執行一次測試：</p>
<p><img src="/resources/aspunit/images/006.gif" alt=""></p>
<p>不就成功了嗎？</p>
<p>註：黃色的是失敗，紅色則是錯誤，而錯誤通常是指執行時期的 Error ，這裡我就不詳述了。</p>
<h2 id="測試設備">測試設備</h2>
<p>每個測試案例的執行流程 (也就是 Run 函式執行的時候) ，其概念是採用 Template Method 這個設計模式，換句話說，它會依次呼叫以下的測試容器函式成員：</p>
<ul>
<li>SetUp() ' 在每個測試案例前做設定動作</li>
<li>TestXXX() ' 測試案例</li>
<li>TearDown() ' 在每個測試案例後做清除動作</li>
</ul>
<p>註：或許你會覺得很懷疑， Template Method 模式不是要用到繼承嗎？嘿，這就是 ASPUnit 作者們功力高深之處了。有興趣的話去追一下程式，包你功力大增！ (但在追以前請保持頭腦清晰，免得到最後迷失了方向。)</p>
<p>那為什麼要執行 SetUp 和 TearDown 呢？原因在於當我們如果有多個測試案例，而這些測試案例在每次執行時可能都會初始化一些相同的變數或物件，那麼我們就可以把這些相同的動作放到 SetUp 函式裡。相對的，如果要清除這些變數或物件，就會放到 TearDown 中。</p>
<p>我們把剛剛的 ProductTest.asp 改成下面的型式：</p>
<pre><code>&lt;!-- #include file=&quot;Product.asp&quot; --&gt;
&lt;%
Class ProductTest ' Extends TestCase
    Private oProduct
    Public Function TestCaseNames()
        TestCaseNames = Array(&quot;TestProduct&quot;, &quot;TestGetName&quot;, &quot;TestGetPrice&quot;, &quot;TestGetDiscountedPrice&quot;)
    End Function
    Public Sub SetUp()
        Set oProduct = New Product
    End Sub
    Public Sub TearDown()
        Set oProduct = Nothing
    End Sub
    Public Sub TestProduct(oTestResult)
        oTestResult.AssertExists oProduct, &quot;物件不存在！&quot;
    End Sub
    Public Sub TestGetName(oTestResult)
        oProduct.Init &quot;商品1&quot;, 100, 1
        oTestResult.AssertEquals &quot;商品1&quot;, oProduct.GetName, &quot;名稱不同！&quot;
    End Sub
    Public Sub TestGetPrice(oTestResult)
        oProduct.Init &quot;商品1&quot;, 100, 1
        oTestResult.AssertEquals 100, oProduct.GetPrice, &quot;價格不同！&quot;
    End Sub
    Public Sub TestGetDiscountedPrice(oTestResult)
        oProduct.Init &quot;商品2&quot;, 100, 0.9
        oTestResult.AssertEquals 90, oProduct.GetPrice, &quot;價格不同！&quot;
    End Sub
End Class
%&gt;

</code></pre><p>在上面的程式裡， oProduct 這個變數變成了 ProductTest 類別的私有變數，所以它在整個 ProductTest 裡都可以被參用到。</p>
<p>在這裡，我們把 oProduct 稱為「測試設備 (Fixture) 」。測試設備可以有很多個，當然最好是每個測試都會用到的物件，我們才將它轉為測試設備；如果只有一個測試案例會用到，那麼直接在該案例中自行建立即可。</p>
<p>因此 Runner 在呼叫測試容器的 Run 函式時，就會依照以下順序去執行其他函式：</p>
<ul>
<li>SetUp()</li>
</ul>
<!-- raw HTML omitted -->
<p>那麼為什麼不保留 oProduct 的內容，直接讓接下來的測試案例使用呢？反覆的建立和銷毀，效率不是很差嗎？</p>
<p>錯！單元測試並沒有特別要求測試時的執行效率，其在意的是程式的穩定性；也就是說，我們需要的是每個測試案例自行運作時的獨立性。而 SetUp 和 TearDown 兩個函式能協助我們建立每次執行測試時的隔離環境，讓我們能信任測試結果的可靠性。</p>
<h2 id="總結">總結</h2>
<p>其實我還很多關於單元測試的東西沒有說明，不過這篇文章主要目的是介紹 ASPUnit 以及單元測試一些簡單的概念。當然我想對大家來說，這篇文章的紀錄意義遠大於實用意義，有多少人會真的會去用它呢？實在很難說。</p>
<p>有很多文章和書籍都提到了測試，以下列出幾本我覺得不錯的中文著作，希望對大家有用：</p>
<ul>
<li><a href="http://www.tenlong.com.tw/BookSearch/Search.php?isbn=9867910311&amp;sid=12245">極致軟體製程 (Extreme Programming Explained)</a></li>
<li><a href="http://www.tenlong.com.tw/BookSearch/Search.php?isbn=9861541489&amp;sid=26120">敏捷軟體開發：原則、樣式及實務 (Agile Software Development: Principles, Patterns, and Practices)</a></li>
<li><a href="http://www.tenlong.com.tw/BookSearch/Search.php?isbn=9867594061&amp;sid=17667">重構─改善既有程式的設計</a></li>
</ul>
<p>總而言之，測試是一件非常重要的事情，不論你用哪一種程式語言，找到一個足以信任的測試框架，做好自動化測試，這樣做起事來才能事半功倍。</p>
<p>歡迎隨時回來指正我。</p>
<h2 id="相關文章">相關文章</h2>
<ul>
<li><a href="http://www.jaceju.net/blog/archives/51/">ASP 物件導向 (1) - 基礎</a></li>
<li><a href="http://www.jaceju.net/blog/archives/52/">ASP 物件導向 (2) - 初級技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/54/">ASP 物件導向 (3) - 進階技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/57/">ASP 物件導向 (4) - 動態載入類別</a></li>
<li><a href="http://www.jaceju.net/blog/archives/59/">ASP 物件導向 (5) - Me 關鍵字</a></li>
<li><a href="http://www.jaceju.net/blog/archives/76/">ASP 物件導向 (6) - 單元測試</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>JavaScript 的靜態變數與靜態方法</title>
			<link>https://jaceju.net/javascript-static-property-and-static-method/</link>
			<pubDate>Mon, 06 Feb 2006 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/javascript-static-property-and-static-method/</guid>
			<description>之前提到 JavaScript 撰寫物件時的寫法： var 物件類別名稱 = function([參數]) { this.屬性1 = null; this.屬性2 = new Array(); this.方法1 = fun</description>
			<content type="html"><![CDATA[<p>之前提到 JavaScript 撰寫物件時的寫法：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">物件類別名稱</span> <span class="o">=</span> <span class="kd">function</span><span class="p">([</span><span class="nx">參數</span><span class="p">])</span> <span class="p">{</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">屬性1</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">屬性2</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Array</span><span class="p">();</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">方法1</span> <span class="o">=</span> <span class="kd">function</span><span class="p">([</span><span class="nx">參數</span><span class="p">])</span> <span class="p">{</span>
    <span class="c1">// 方法1程式碼
</span><span class="c1"></span>    <span class="k">this</span><span class="p">.</span><span class="nx">屬性1</span> <span class="o">=</span> <span class="s1">&#39;DEF&#39;</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">this</span><span class="p">.</span><span class="nx">方法2</span> <span class="o">=</span> <span class="kd">function</span><span class="p">([</span><span class="nx">參數</span><span class="p">])</span> <span class="p">{</span>
    <span class="c1">// 方法2程式碼
</span><span class="c1"></span>    <span class="k">this</span><span class="p">.</span><span class="nx">屬性2</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Array</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

</code></pre></div><p>這種方法在有物件實體時才會有作用，如果要像 Java 或 PHP 一樣呼叫靜態方法的話，就要這樣寫：</p>
<!-- raw HTML omitted -->
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">物件類別名稱</span> <span class="o">=</span> <span class="kd">function</span><span class="p">([</span><span class="nx">參數</span><span class="p">])</span> <span class="p">{</span>
<span class="p">}</span>

<span class="c1">// 一定要在建構函式之後再宣告，否則會有錯誤。
</span><span class="c1"></span><span class="nx">物件類別名稱</span><span class="p">.</span><span class="nx">靜態變數</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="nx">物件類別名稱</span><span class="p">.</span><span class="nx">靜態方法</span> <span class="o">=</span> <span class="kd">function</span><span class="p">([</span><span class="nx">參數</span><span class="p">])</span> <span class="p">{</span>
  <span class="c1">// 這時候不可使用 this 關鍵字
</span><span class="c1"></span>  <span class="nx">物件類別名稱</span><span class="p">.</span><span class="nx">靜態變數1</span> <span class="o">=</span> <span class="s1">&#39;DEF&#39;</span><span class="p">;</span>
<span class="p">}</span>

</code></pre></div><p>這樣我們可以利用它來建立一個 Singleton 類別  ：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">MyTest</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">msg</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">echo</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">msg</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">setMsg</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">msg</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">msg</span> <span class="o">=</span> <span class="nx">msg</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="c1">// 這裡是關鍵
</span><span class="c1"></span><span class="nx">MyTest</span><span class="p">.</span><span class="nx">_instance</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="nx">MyTest</span><span class="p">.</span><span class="nx">getInstance</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">MyTest</span><span class="p">.</span><span class="nx">_instance</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">MyTest</span><span class="p">.</span><span class="nx">_instance</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MyTest</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">msg</span> <span class="o">=</span> <span class="s1">&#39;test!&#39;</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">MyTest</span><span class="p">.</span><span class="nx">_instance</span><span class="p">;</span>
<span class="p">}</span>

</code></pre></div><p>測試如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;test.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="c">&lt;!--</span>
<span class="kd">var</span> <span class="nx">test1</span> <span class="o">=</span> <span class="nx">MyTest</span><span class="p">.</span><span class="nx">getInstance</span><span class="p">();</span>
<span class="nx">test1</span><span class="p">.</span><span class="nx">echo</span><span class="p">();</span> <span class="c1">// show &#39;null&#39;
</span><span class="c1"></span><span class="nx">test1</span><span class="p">.</span><span class="nx">setMsg</span><span class="p">(</span><span class="s1">&#39;this is test1!&#39;</span><span class="p">);</span>
<span class="nx">test1</span><span class="p">.</span><span class="nx">echo</span><span class="p">();</span> <span class="c1">// show &#39;this is test1!&#39;
</span><span class="c1"></span><span class="kd">var</span> <span class="nx">test2</span> <span class="o">=</span> <span class="nx">MyTest</span><span class="p">.</span><span class="nx">getInstance</span><span class="p">();</span>
<span class="nx">test2</span><span class="p">.</span><span class="nx">echo</span><span class="p">();</span> <span class="c1">// show &#39;this is test1!&#39;
</span><span class="c1"></span><span class="nx">test2</span><span class="p">.</span><span class="nx">setMsg</span><span class="p">(</span><span class="s1">&#39;this is test2!&#39;</span><span class="p">);</span>
<span class="nx">test1</span><span class="p">.</span><span class="nx">echo</span><span class="p">();</span> <span class="c1">// show &#39;this is test2!&#39;
</span><span class="c1">//--&gt;
</span><span class="c1"></span><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>

</code></pre></div><p>可以看得出來， test1 和 test2 其實都是指向 MyTest._instance 。</p>
<p>當然用 JavaScript 實作 Singleton 的實際意義不大 (建構式還是公開的) ，這裡我只是在強調 JavaScript 靜態方法及變數的寫法而已。</p>
]]></content>
		</item>
		
		<item>
			<title>用 Xdebug 來找出 PHP 程式錯誤</title>
			<link>https://jaceju.net/php-debuging-with-xdebug/</link>
			<pubDate>Mon, 19 Dec 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-debuging-with-xdebug/</guid>
			<description>有時候 PHP 在執行到某個錯誤時，只會丟出一行的訊息，告訴你程式發生錯誤。 例如以下的程式： &amp;lt;?php function test($var) { $var-&amp;gt;display(); } $abc = 123; test($abc); ?&amp;gt; 執行後會出現以下的錯誤： Fatal error: Call to a</description>
			<content type="html"><![CDATA[<p>有時候 PHP 在執行到某個錯誤時，只會丟出一行的訊息，告訴你程式發生錯誤。 例如以下的程式：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">function</span> <span class="nf">test</span><span class="p">(</span><span class="nv">$var</span><span class="p">)</span>
<span class="p">{</span>
   <span class="nv">$var</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">();</span>
<span class="p">}</span>
<span class="nv">$abc</span> <span class="o">=</span> <span class="mi">123</span><span class="p">;</span>
<span class="nx">test</span><span class="p">(</span><span class="nv">$abc</span><span class="p">);</span>
<span class="cp">?&gt;</span>

</code></pre></div><p>執行後會出現以下的錯誤：</p>
<pre><code>Fatal error:  Call to a member function display() on a non-object in D:\WEB\wwwroot\index.php on line 4

</code></pre><p>我們可以知道在第 4 行的地方因為 test 函式對 $var 呼叫了物件操作函式，卻因為 $var 不是物件而導致錯誤 (Fatal Error) 。但是我們怎麼知道 $var 變數是從那裡來的呢？就上例而言，很明顯地往回推到第 8 行呼叫 test 函式的地方，我們給它的是 $abc 變數，而 $abc 變數存放則是一個數值，也因此造成 test 函式的錯誤。</p>
<p>當然在程式碼比較短時，我們可以很容易知道整個程式執行的流程動向；不過當我們的程式碼暴漲到上百行或是中間引入不同檔案時，要一下子歸納出它的動線就很不容易了。換句話說，如果第 6 行中我們有上百行的程式碼，那麼我們如何一下子就找到 test 函式的引用點呢？</p>
<!-- raw HTML omitted -->
<h2 id="xdebug">Xdebug</h2>
<p>之前我寫了一篇 <a href="http://www.jaceju.net/blog/archives/21">Xdebug 的文章</a>，不過當時談到的是簡測 PHP 程式的效能。其實 Xdebug <!-- raw HTML omitted -->故名思意<!-- raw HTML omitted --><a href="http://140.111.34.46/chengyu/">顧名思義</a>就是讓我們可以對程式進行除錯，同時也記錄相關的資訊。而它有個很常用的功能，就是讓我們在程式出錯時，可以知道發生錯誤前，程式所執行的流程。</p>
<p>例如上例在使用安裝 Xdeubg 後，所出現的錯誤畫面： (安裝方式請參考上面提到的文章)</p>
<p><a href="//www.jaceju.net/resources/xdebug_debug/001.png"><img src="//www.jaceju.net/resources/xdebug_debug/001.png" alt="程式一的錯誤畫面"></a></p>
<p>其中紅色部份就是原來的錯誤訊息，而底下的表格部份就是程式執行的順序 (且已正確執行) 。所以我們能很清楚地知道程式是在第 8 行引用 test 函式後發生了錯誤，這樣一來就能減小我們搜索的範圍。</p>
<p>不過有種情況 Xdebug 發揮不了作用，那就是 PHP.INI 錯誤回報等級被調整過了。例如以下範例：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">class</span> <span class="nc">test</span>
<span class="p">{</span>
  <span class="k">private</span> <span class="nv">$var</span> <span class="o">=</span> <span class="s1">&#39;123&#39;</span><span class="p">;</span>
  <span class="k">public</span> <span class="k">function</span> <span class="nf">display</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">varr</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="k">function</span> <span class="nf">test</span><span class="p">()</span>
<span class="p">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nx">test</span><span class="p">;</span>
<span class="p">}</span>
<span class="nv">$abc</span> <span class="o">=</span> <span class="nx">test</span><span class="p">();</span>
<span class="nv">$abc</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">();</span>
<span class="cp">?&gt;</span>

</code></pre></div><p>如果當你執行這段程式時，沒有出現任何錯誤，那麼就表示 PHP.INI 錯誤回報等級沒有對「變數未定義」產生警告 ( NOTICE) 。通常這是在上線系統所必須關閉的，因為很多時候我們常會漏掉宣告這些變數 (給初始值) ；如果不關掉的話，頁面就會產生一堆讓使用者費解的訊息。</p>
<p>試著在 &lt;?php 的下一行加上：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">error_reporting(E_ALL);

</code></pre></div><p>重新執行後我們就會看到：</p>
<p><a href="//www.jaceju.net/resources/xdebug_debug/002.png"><img src="//www.jaceju.net/resources/xdebug_debug/002.png" alt="程式二的錯誤畫面"></a></p>
<p>上面的意思就是未宣告的屬性，回頭看一下類別程式，原來我們多輸入了一個 r 字母 (變成 var<!-- raw HTML omitted -->r<!-- raw HTML omitted -->) 。這種錯誤也常見於一個較大型的物件類別程式中如果有較長名稱的屬性時，我們很容易寫錯它們的名字 (尤其像英文不好的我) 。</p>
<p>因此如果要使用 Xdebug ，一定要記得把錯誤回報等級設定至最嚴格，這樣才能抓出所有的問題點。</p>
]]></content>
		</item>
		
		<item>
			<title>ASP 物件設計手法 (5) - Me 關鍵字</title>
			<link>https://jaceju.net/classic-asp-oo-5/</link>
			<pubDate>Fri, 16 Dec 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-oo-5/</guid>
			<description>一直以來我都以為 Me 這個關鍵字只能在 VB 上用，沒想到這兩天我用了 Me 當類別屬性時發生錯誤，這讓我得重新檢視它是不是個 VBScript 的預設關鍵字。 我找過 MSDN 的 Scripting ，</description>
			<content type="html"><![CDATA[<p>一直以來我都以為 Me 這個關鍵字只能在 VB 上用，沒想到這兩天我用了 Me 當類別屬性時發生錯誤，這讓我得重新檢視它是不是個 VBScript 的預設關鍵字。</p>
<p>我找過 <a href="http://msdn.microsoft.com/library/">MSDN</a> 的 <a href="http://msdn.microsoft.com/library/en-us/dnanchor/html/Scriptinga.asp">Scripting</a> ，裡面並沒有提到 Me 這個關鍵字，反而是在 VB 6.0 裡找到這個關鍵字的<a href="http://msdn.microsoft.com/library/default.asp?url=https://jaceju.net/library/en-us/vbenlr98/html/vakeyme.asp">說明</a>。真是神「 Me 」 呀！它在 VB 中的主要用途一為指向類別自己，二為指向 Form 物件。不過 ASP 中應該是沒有 Form 物件可參考 (這裡我保留這樣的想法，也許將來又發現自己錯了) ，所以對我來說這個關鍵字就是用在 Class 上的。</p>
<!-- raw HTML omitted -->
<h2 id="me-關鍵字的用法">Me 關鍵字的用法</h2>
<p>我寫了一個小程式來測試 Me 關鍵字，其實也沒有很複雜，用法和 JavaScript 的 this 關鍵字很像。</p>
<pre><code>&lt;%
Class ClassMessage
    Private Message

    Private Sub Class_Initialize()
        Me.SetMessage &quot;TEST&quot;
    End Sub

    Public Sub SetDisplay(oDisplay)
        oDisplay.Display Me
    End Sub

    Public Sub SetMessage(sMessage)
        Message = sMessage
        ' 不可以寫成：
        ' Me.Message = sMessage
    End Sub

    Public Function GetMessage()
        GetMessage = Message
    End Function
End Class

Class ClassDisplay
    Public Sub Display(oMessage)
        Response.Write oMessage.GetMessage
    End Sub
End Class

Dim oMessage : Set oMessage = New ClassMessage
Dim oDisplay : Set oDisplay = New ClassDisplay

oMessage.SetDisplay oDisplay
Set oMessage = Nothing
Set oDisplay = Nothing
%&gt;

</code></pre><p>這樣一來，有很多設計模式可用了，有空可以好好研究一下。</p>
<p>不過值得注意的是 Me 關鍵字似乎不能用在屬性上，所以我覺得它更像 PHP 的 self 關鍵字；也就是說 Me 關鍵字在某種程度上來說應該屬於類別層級，而非物件層級。 可是它又可以把自己丟給別的物件參考，和 PHP 的 $this 又有點像，我想這點要特別注意。</p>
<p>附帶一提， MSDN 最近連結大改，有點難以適應。</p>
<h2 id="相關文章">相關文章</h2>
<ul>
<li><a href="http://www.jaceju.net/blog/archives/51/">ASP 物件導向 (1) - 基礎</a></li>
<li><a href="http://www.jaceju.net/blog/archives/52/">ASP 物件導向 (2) - 初級技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/54/">ASP 物件導向 (3) - 進階技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/57/">ASP 物件導向 (4) - 動態載入類別</a></li>
<li><a href="http://www.jaceju.net/blog/archives/59/">ASP 物件導向 (5) - Me 關鍵字</a></li>
<li><a href="http://www.jaceju.net/blog/archives/76/">ASP 物件導向 (6) - 單元測試</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>ASP 物件設計手法 (4) - 動態載入類別</title>
			<link>https://jaceju.net/classic-asp-oo-4/</link>
			<pubDate>Sat, 10 Dec 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-oo-4/</guid>
			<description>為了避免類別重覆宣告，我想盡辦法做了一些調整，但都不如已意。後來我回想起自己曾經找過一篇動態載入 ASP 程式的文章，那時因為它無法達到我的需求而放</description>
			<content type="html"><![CDATA[<p>為了避免類別重覆宣告，我想盡辦法做了一些調整，但都不如已意。後來我回想起自己曾經找過一篇<a href="http://www.blueidea.com/tech/program/2003/101.asp">動態載入 ASP 程式的文章</a>，那時因為它無法達到我的需求而放棄，但是現在它卻有新的用法。</p>
<!-- raw HTML omitted -->
<h2 id="載入文件">載入文件</h2>
<p>首先透過 ADODB.Stream ，我們可以把一份文字檔載入記憶體：</p>
<pre><code>LoadFile.asp
&lt;%
Public Function LoadFile(sFilePath)
    Dim oStream
    On Error Resume Next
    Set oStream = Server.CreateObject(&quot;ADODB.Stream&quot;)
    With oStream
        .Type = 2
        .Mode = 3
        .CharSet = &quot;BIG5&quot;
        .Open
        .LoadFromFile Server.MapPath(sFilePath)
        If Err.Number &lt;&gt; 0 Then
            Err.Clear
            Response.Write &quot;[FUNCTION] LoadFile Error - 找不到檔案：&quot; &amp;amp; sFilePath
            Response.End
        End If
        LoadFile = .ReadText
        .Close
    End With
    Set oStream = Nothing
End Function
%&gt;

</code></pre><p>有些人習慣使用 FileSystemObject 來載入檔案內容，但是利用 ADODB.Stream 的效率會比較好。</p>
<h2 id="動態載入-asp-類別檔">動態載入 ASP 類別檔</h2>
<p>以下的程式就是從前面那篇文章所提供的程式修改而來的，</p>
<pre><code>Include.asp
&lt;!-- #include file = &quot;LoadFile.asp&quot; --&gt;
&lt;%
Public Const ASP_LEFT = &quot;&lt;%&quot;
Public Const ASP_RIGHT = &quot;%\&gt;&quot;
Public Function Include(sFileName)
    Dim sContent : sContent = LoadFile(sFileName)
    Dim iEnd : iEnd = 1
    Dim iStart : iStart = InStr(iEnd, sContent, ASP_LEFT) + 2
    Do While iStart &gt; iEnd + 1
        iEnd = InStr(iStart, sContent, ASP_RIGHT) + 2
        ExecuteGlobal(Mid(sContent, iStart, iEnd - iStart - 2))
        iStart = InStr(iEnd, sContent, ASP_LEFT) + 2
    Loop
End Function
%&gt;

</code></pre><p>其中 &ldquo;%&gt;&rdquo; 是為了避開 ASP 解譯程式上的錯誤。主要的概念就是利用 <a href="http://msdn.microsoft.com/library/en-us/script56/html/vsstmExecuteGlobal.asp">ExecuteGlobal</a> 來執行 ASP 檔案中的 &lt;% 及 %&gt; 兩個符號間的內容，如果使用 <a href="http://msdn.microsoft.com/library/default.asp?url=https://jaceju.net/library/en-us/script56/html/vsstmExecute.asp">Execute</a> 的話，會發生找不到 Class 的錯誤。</p>
<h2 id="建立物件">建立物件</h2>
<p>還記得前面所介紹的類別命名規則和工廠方法嗎？這裡就派上用場了。</p>
<pre><code>GetObject.asp
&lt;!-- #include file=&quot;Include.asp&quot; --&gt;
&lt;%
Public Const CLASS_DIR = &quot;&quot;
Public Function GetObject(sClassName)
    Dim sFileName
    sFileName = Replace(sClassName, &quot;_&quot;, &quot;/&quot;) &amp;amp; &quot;.asp&quot;
    Set GetObject = Nothing
    On Error Resume Next
    Include sFileName
    Execute &quot;Set GetObject = New &quot; &amp;amp; sClassName
    On Error Goto 0
End Function
%&gt;

</code></pre><p>首先我們把類別名稱中的 &ldquo;_&rdquo; (底線) 換成 &ldquo;/&rdquo; (斜線) 再加上 &ldquo;.asp&rdquo; ，然後利用工廠方法來回傳建立好的物件。</p>
<p>註：因為 ASP (VBScript) 沒有內建 GetObject 函式，所以我們可以這樣寫。如果是在 WScript 裡面就不能用 GetObject ，因為它是內建函式。</p>
<p>以下就是簡單的測試方式：</p>
<pre><code>&lt;!-- #include file=&quot;GetObject.asp&quot; --&gt;
&lt;%
Dim oCache1 : Set oCache1 = GetObject(&quot;HTML_Cache&quot;)
Response.Write TypeName(oCache1) &amp;amp; &quot;&lt;br /&gt;&quot; &amp;amp; vbCrLf
oCache1.SetExpire 10
If oCache1.IsCached Then
    Response.Write &quot;File1 is cached.&lt;br /&gt;&quot; &amp;amp; vbCrLf
End If
Set oCache1 = Nothing

Dim oCache2 : Set oCache2 = GetObject(&quot;HTML_Cache&quot;)
Response.Write TypeName(oCache2) &amp;amp; &quot;&lt;br /&gt;&quot; &amp;amp; vbCrLf
If oCache2.IsCached Then
    Response.Write &quot;File2 is cached.&lt;br /&gt;&quot; &amp;amp; vbCrLf
End If
Set oCache2 = Nothing
%&gt;

</code></pre><p>而測試用的類別檔如下 (我簡化了許多不必要的程式) ：</p>
<pre><code>HTML\Cache.asp
&lt;%
Class HTML_Cache
    Private Expire

    Public Function IsCached()
        IsCached = True
    End Function

    Public Sub SetExpire(iSecond)
        Expire = iSecond
    End Sub
End Class
%&gt;

</code></pre><p>測試一下，是不是很有趣呢？</p>
<h2 id="總結">總結</h2>
<p>對我而言，如果把複雜的事情簡化，是一種學習的動力。透過這些步驟，我能夠很輕鬆地載入我想要的類別，而不必再煩惱是不是載入了重複的類別檔案。這系列的文章如果能對某些想要學習的人有用，那麼就值得了。</p>
<h2 id="相關文章">相關文章</h2>
<ul>
<li><a href="http://www.jaceju.net/blog/archives/51/">ASP 物件導向 (1) - 基礎</a></li>
<li><a href="http://www.jaceju.net/blog/archives/52/">ASP 物件導向 (2) - 初級技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/54/">ASP 物件導向 (3) - 進階技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/57/">ASP 物件導向 (4) - 動態載入類別</a></li>
<li><a href="http://www.jaceju.net/blog/archives/59/">ASP 物件導向 (5) - Me 關鍵字</a></li>
<li><a href="http://www.jaceju.net/blog/archives/76/">ASP 物件導向 (6) - 單元測試</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>ASP 物件設計手法 (1) - 基礎</title>
			<link>https://jaceju.net/classic-asp-oo-1/</link>
			<pubDate>Tue, 06 Dec 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-oo-1/</guid>
			<description>這是寫給公司同事的一系列 ASP (VBScript) 物件設計手法文章，我盡可能寫得比較簡單易懂。雖然 ASP 目前已經不再是市場主流，但還是我的手邊有很多專案沒有辦法導入比</description>
			<content type="html"><![CDATA[<p>這是寫給公司同事的一系列 ASP (VBScript) 物件設計手法文章，我盡可能寫得比較簡單易懂。雖然 ASP 目前已經不再是市場主流，但還是我的手邊有很多專案沒有辦法導入比較先進的技術 (像 ASP.NET 或 JSP 等) 。所以瞭解如何用 ASP 的物件設計手法來解決問題，是這系列文章主要的目的。</p>
<p>在繼續下去之前，首先先釐清一個觀念，物件導向是一種思維，而非僅是語言的特性。當然，我也不想去仔細探討 ASP (VBScript) 是不是物件導向語言。我已經用這種方式寫了一些專案，執行成果也還不錯。對於那些有疑問的朋友，我還能夠拿出一點不算太難看的成果供他們參考。</p>
<p>經過了一些討論與思考，我決定把這系列文章更名為「ASP 物件設計手法」。</p>
<p>對於物件導向我不是高手，我也還在研究當中。我只是希望透過這樣的說明，讓我身邊的伙伴能夠清楚我的想法 (與寫法) 而已。</p>
<!-- raw HTML omitted -->
<h2 id="class-語法">Class 語法</h2>
<p>ASP (VBScript) 提供了一個 Class 敘述讓我們建立一個物件類別：</p>
<pre><code>&lt;%
Class ClassName
    ' 物件初始化
    Private Sub Class_Initialize()
    End Sub
    ' 物件結束
    Private Sub Class_Terminate()
    End Sub
End Class
%&gt;

</code></pre><p>其中 ClassName 是類別名稱，而 Class_Initialize() 及 Class_Terminate() 兩個私有函式是 VBScript 的特殊函式，它們分別會在物件被起始化及物件結束時呼叫 (也就是 Event ) ，例如：</p>
<pre><code>&lt;%
' 初始化一個 oTest 物件，並呼叫 ClassName 類別的 Class_Initialize 方法
Dim oTest : Set oTest = New ClassName
' 呼叫 Class_Terminate
Set oTest = Nothing
%&gt;

</code></pre><p>這裡有一些要注意的地方：</p>
<ul>
<li>不能重複宣告 Class 。一般 ASP 程式可以重複 include 多次 Function ，但是 Class 不行；而且 Class 裡面也不能重複宣告 Function 。</li>
<li>這些類別所產生出來的物件內容不能存到 Application 或 Session 變數中，只有利用 Server.CreateObject 方法所建立的物件才可以。</li>
<li>在執行 New 敘述時，我們並沒有沒辦法傳遞參數給建構函式，也就是說 Set oTest = New ClassName(&ldquo;xxx&rdquo;) 這樣的敘述是錯的。</li>
<li>當 ASP 程式結束時，並不一定會自動呼叫 Class_Terminate 函式。我們可以手動以 Set oTest = Nothing 的方式來讓物件消失前，能確實執行 Class_Terminate 方法。</li>
<li>就像 VBScript 的 Function 中可以存取全域變數一樣，Class 裡也同樣也能夠直接取得全域變數。</li>
<li>ASP (VBScript) 是用 Pass By Reference 的方式來傳遞物件。</li>
<li>VBScript 的 Class 存取等級只有兩種： Public 及 Private 。</li>
</ul>
<!-- raw HTML omitted -->
<h2 id="預設函式">預設函式</h2>
<p>預設函式是指不需要特別指定函式的名稱，直接使用物件變數名稱來呼叫物件中的某個函式，例如：</p>
<pre><code>Dim oTest : Set oTest = New Test
' 呼叫 Sub
Call oTest(&quot;Test&quot;)
' 或是取得 Function 回傳值
Dim sTemp = oTest(&quot;Test&quot;)

</code></pre><p>我們可以用以下語法來建立一個預設的函式：</p>
<pre><code>Class Test
    ' 物件初始化
    Public Defalut Sub|Function MethodName() ' 注意 Default
    ' Do Something
    End Sub|Function
End Class

</code></pre><p>也就是在一般函式的 Sub 或 Function 前，加上一個 Default 關鍵字。特別要注意的是，預設函式必須是 Public 的存取等級。</p>
<p>這種方式非常好用，尤其是當我們想跟一些 ASP 內建物件做整合時可以用到。</p>
<h2 id="繼承">繼承？</h2>
<p>ASP (VBScript) 沒有繼承語法，不過可以用其他方式來達成。當然，沒有繼承語法的協助，寫一些程式時會比較累一點。</p>
<p>第一種方式是使用 SSI 的 include 指令，這種方式我稱為「假繼承」，因為實際上根本就沒有繼承任何 Class ，我只是把共用的變數或函式寫在一個 ASP 檔中。</p>
<pre><code>ClassA.asp
&lt;%
' Class ClassA 實際上是沒有這個類別的
    ' 物件屬性
    Private Fields
    ' 物件初始化
    Private Sub Class_Initialize()
    Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
    End Sub
    ' 物件結束
    Private Sub Class_Terminate()
    Set Fields = Nothing
    End Sub
' End Class
%&gt;

</code></pre><pre><code>ClassB.asp
&lt;%
Class ClassB
    ' 假繼承 ClassA
    %&gt;&lt;!-- #include file=&quot;ClassA.asp&quot; --&gt;&lt;%
    ' 非共用函式
    Public Function OtherMethod()
    ' ... 略 ...
    End Function
End Class
%&gt;

</code></pre><p>因為無法在 Class 重複宣告 Function ，所以這種方式的缺點就是只要函式實作方式有些許不同，就不能寫到共用的 ClassA 中。</p>
<p>第二種則是採用<!-- raw HTML omitted -->裝飾者模式 (Decorator Pattern)<!-- raw HTML omitted --> ，基本上這是用<!-- raw HTML omitted -->委派 (delegate)<!-- raw HTML omitted --> 取代繼承。</p>
<pre><code>ClassA.asp
&lt;%
Class ClassA
    ' 物件屬性
    Public Fields
    ' 物件初始化
    Private Sub Class_Initialize()
    Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
    End Sub
    ' 物件結束
    Private Sub Class_Terminate()
    Set Fields = Nothing
    End Sub
    ' 要 Override 的函式
    Public Function OverrideMethod()
    Fields.Add &quot;Key1&quot;, 123
    End Function
    ' 其他函式
    Public Function OtherMethod()
    ' ... 略 ...
    End Function
End Class
%&gt;

</code></pre><pre><code>ClassB.asp
&lt;%
Class ClassB
    ' 被託付者
    Private Parent
    ' 物件初始化
    Private Sub Class_Initialize()
        Set Parent = New ClassA
    End Sub
    ' 物件結束
    Private Sub Class_Terminate()
        Set Parent = Nothing
    End Sub
    ' 要 Override 的函式
    Public Function OverrideMethod()
        Parent.Fields.Add &quot;Key2&quot;, 456
    End Function
    ' 其他函式
    Public Function OtherMethod()
        Parent.OtherMethod
    End Function
End Class
%&gt;

</code></pre><p>這種方式的缺點，就是要對每個函式都委派給被託付者去完成；如果被託付者的函式非常多，就會多出一些雞肋出來。另外 ClassA 對 ClassB 必須是透明的 (Public) ，也就是說除了Class_Initialize 及 Class_Terminate 外， ClassB 要能夠取得 ClassA 的所有屬性及函式 (有點像 PHP 的 protected ) 。</p>
<p>不過要記住一件事，上面的方式只是為了重用程式碼而已，並不是真正的繼承。如果重用的程式不是很複雜的話，那麼最好還是看狀況來決定要不要使用。</p>
<h2 id="設定或取得屬性">設定或取得屬性</h2>
<p>類別屬性的部份，我後來大部份會使用一個私有的 Dictionary 物件來儲存。 Dictionary 物件有點像是 PHP 的 associative array ，利用字串索引來儲存每一個元素。這麼做的好處可以讓我們用很簡單的方式來設定物件的屬性：</p>
<pre><code>ClassC.asp
&lt;%
Class ClassC
    ' 物件屬性
    Private Fields
    ' 物件初始化
    Private Sub Class_Initialize()
        Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
        Fields.Add &quot;Attr1&quot;, &quot;&quot;
        Fields.Add &quot;Attr2&quot;, 0
        Fields.Add &quot;Attr3&quot;, Array()
    End Sub
    ' 物件結束
    Private Sub Class_Terminate()
        Set Fields = Nothing
    End Sub
    ' 設定指定的屬性
    Public Sub SetField(sName, vValue)
        If Fields.Exists(sName) Then
            If IsObject(vValue) Then
                Set Fields(sName) = vValue
            Else
                Fields(sName) = vValue
            End If
        End If
    End Sub
    ' 取得指定的屬性
    Public Function GetField(sName)
        GetField = Null
        If Fields.Exists(sName) Then
            If IsObject(Fields(sName)) Then
                Set GetField = Fields(sName)
            Else
                GetField = Fields(sName)
            End If
        End If
    End Function
End Class
%&gt;

</code></pre><p>當然，在 SetField 函式裡有時候會需要驗證格式，不過通常我會先利用其他方式驗證完畢後再存入，以減輕這個類別的重量。</p>
<p>VBScript 還有提供一個 Property 語法來讓我們設定或取得類別屬性，但我大部份還是會利用上面的方式來設定。</p>
<h2 id="使用類別">使用類別</h2>
<p>類別使用的方式很簡單，就是先用 New 關鍵字來產生一個物件實體：</p>
<pre><code>&lt;%
Dim oTempC : Set oTempC = New ClassC
oTempC.SetField &quot;Attr1&quot;, &quot;123&quot;
oTempC.SetField &quot;Attr2&quot;, 123
oTempC.SetField &quot;Attr3&quot;, Array(123, 456)
Set oTempC = Nothing
%&gt;

</code></pre><p>我們也可以用 With 語法來存取，這樣會比較快一點。 (JScript 的 With 則比較慢。)</p>
<pre><code>&lt;%
Dim oTempC : Set oTempC = New ClassC
With oTempC
    .SetField &quot;Attr1&quot;, &quot;123&quot;
    .SetField &quot;Attr2&quot;, 123
    .SetField &quot;Attr3&quot;, Array(123, 456)
End With
Set oTempC = Nothing
%&gt;

</code></pre><h2 id="類別命名方式">類別命名方式</h2>
<p>類別的命名方式並沒有特別之處，就是如同一般 ASP 變數命名的規則。不過這裡我有一個建議，就是利用 PHP PEAR 函式庫的命名規則來歸納我們的類別。</p>
<p>例如：</p>
<pre><code>/inc/class/HTML/Template.asp
&lt;%
Class HTML_Template
    ' ... 略 ...
End Class
%&gt;

</code></pre><pre><code>/inc/class/HTML/Form.asp
&lt;%
Class HTML_Form
    ' ... 略 ...
End Class
%&gt;

</code></pre><pre><code>/inc/class/HTML/Cache.asp
&lt;%
Class HTML_Cache
    ' ... 略 ...
End Class
%&gt;

</code></pre><p>在引入類別檔時，我會利用一個類似 package 的檔案把所有相關的類別一起 include ：</p>
<pre><code>/inc/HTML.asp
&lt;!-- #include file=&quot;class/HTML/Template.asp&quot; --&gt;
&lt;!-- #include file=&quot;class/HTML/Form.asp&quot; --&gt;
&lt;!-- #include file=&quot;class/HTML/Cache.asp&quot; --&gt;

</code></pre><p>因此引用時，就只要引入 /inc/HTML.asp 即可 (有點像 C# 的 using 指令) 。 不過還是要看狀況而定，這樣的方式不一定比較好。</p>
<h2 id="相關文章">相關文章</h2>
<ul>
<li><a href="http://www.jaceju.net/blog/archives/51/">ASP 物件導向 (1) - 基礎</a></li>
<li><a href="http://www.jaceju.net/blog/archives/52/">ASP 物件導向 (2) - 初級技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/54/">ASP 物件導向 (3) - 進階技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/57/">ASP 物件導向 (4) - 動態載入類別</a></li>
<li><a href="http://www.jaceju.net/blog/archives/59/">ASP 物件導向 (5) - Me 關鍵字</a></li>
<li><a href="http://www.jaceju.net/blog/archives/76/">ASP 物件導向 (6) - 單元測試</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>ASP 物件設計手法 (2) - 初級技巧</title>
			<link>https://jaceju.net/classic-asp-oo-2/</link>
			<pubDate>Tue, 06 Dec 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-oo-2/</guid>
			<description>Design Patterns 是我近來研究的課題之一，我在後來的 ASP 專案裡，為了解決一些問題而導入了部份的 Design Patterns 觀念。或許有人會認為 ASP (VBScript) 沒辦法使用正統的 Design Patterns ，不過我注重的</description>
			<content type="html"><![CDATA[<p>Design Patterns 是我近來研究的課題之一，我在後來的 ASP 專案裡，為了解決一些問題而導入了部份的 Design Patterns 觀念。或許有人會認為 ASP (VBScript) 沒辦法使用正統的 Design Patterns ，不過我注重的是 Design Patterns 的觀念所帶來的解題方式，而不是 Design Patterns 的形。</p>
<p>以下我會說明一些我常用的技巧，這些都是從 Design Patterns 中得到的一些啟發。</p>
<!-- raw HTML omitted -->
<h2 id="工廠方法">工廠方法</h2>
<p>工廠方法通常用於建立在抽象層次上屬於同一類的物件，例如同樣是交通工具，我們可能會建立巴士、火車或飛機其中一種。一般可以直接用一個函式來取得物件，當然如果想用類別也可以，但要看程式複雜度而定 (如果該類別不單純只有工廠方法) ：</p>
<pre><code>/inc/VehicleFactory.asp
&lt;%
Function VehicleFactory(sType)
    Dim oVehicle
    Set VehicleFactory = Nothing
    On Error Resume Next
    Execute &quot;Set oVehicle = New Vehicle_&quot; &amp;amp; sType
    Set VehicleFactory = oVehicle
    On Error Goto 0
End Function
%&gt;

</code></pre><p>我們先將要傳回的物件設為 Nothing 物件，然後利用 On Error Resume Next 來啟用錯誤處理機制。如果當想要建立的物件不存在時，我們就可以利用錯誤處理機制讓傳回的物件會依然保持為 Nothing 物件。</p>
<p>用法非常簡單：</p>
<pre><code>&lt;!-- #include virtual=&quot;/inc/Vehicle.asp&quot; --&gt;
&lt;!-- #include virtual=&quot;/inc/VehicleFactory.asp&quot; --&gt;
&lt;%
Set oVehicle = VehicleFactory(&quot;Bus&quot;)
If Not oVehicle Is Nothing Then
    ' ... 略 ...
End If
%&gt;

</code></pre><p>工廠方法其實還滿常用的，像是利用 ADODB.Connection 來建立資料庫連線 (可能連到 Access 或 MSSQL) ，這也算是工廠方法的一種變形。另外 Server 物件的 CreateObject 方法也是經典的工廠方法之一。</p>
<h2 id="複製物件">複製物件</h2>
<p>複製物件是讓一個物件有分身的功能，也就是 Prototype Pattern 。如果單純只用 Set 指令，並沒有辦法讓物件變成兩個，例如：</p>
<pre><code>&lt;%
Class ClassA
    Private Number
    Public Sub SetNumber(vValue)
        Number = vValue
    End Sub
    Public Function GetNumber
        GetNumber = Number
    End Function
End Class
Dim oA, oB
Set oA = New ClassA
oA.SetNumber 1
Set oB = oA
oB.SetNumber 2
Response.Write oA.GetNumber &amp;amp; vbCrLf ' 顯示 2
Response.Write oB.GetNumber &amp;amp; vbCrLf ' 顯示 2
%&gt;

</code></pre><p>這樣只是讓 oB 變成 oA 物件的別名，一旦 oB 物件改變屬性， oA 物件也會跟著改變。</p>
<p>那如何讓 oA 和 oB 分別成為不同的個體呢？我習慣使用以下的方式：</p>
<pre><code>ClassD.asp
&lt;%
Class ClassD
    ' 物件屬性
    Private Fields
    ' 物件初始化
    Private Sub Class_Initialize()
        Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
        Fields.Add &quot;Attr1&quot;, &quot;&quot;
        Fields.Add &quot;Attr2&quot;, 0
        Fields.Add &quot;Attr3&quot;, Array()
    End Sub
    ' 物件結束
    Private Sub Class_Terminate()
        Set Fields = Nothing
    End Sub
    ' 設定指定的屬性
    Public Sub SetField(sName, vValue)
        If Fields.Exists(sName) Then
            If IsObject(vValue) Then
                Set Fields(sName) = vValue
            Else
                Fields(sName) = vValue
            End If
        End If
    End Sub
    ' 取得指定的屬性
    Public Function GetField(sName)
        GetField = Null
        If Fields.Exists(sName) Then
            If IsObject(Fields(sName)) Then
                Set GetField = Fields(sName)
            Else
                GetField = Fields(sName)
            End If
        End If
    End Function
    ' 設定分身的屬性
    Public Sub CloneFields(oFields)
        Dim sKey
        For Each sKey In oFields
            If IsObject(oFields(sKey)) Then
                Set Fields(sKey) = oFields(sKey) // 注意這裡
            Else
                Fields(sKey) = oFields(sKey) // 注意這裡
            End If
        Next
    End Sub
    ' 取得分身
    Public Function Clone()
        Dim oTempD
        Set oTempD = New ClassD
        oTempD.CloneFields Fields
        Set Clone = oTempD
    End Function
End Class
Dim oD1, oD2
Set oD1 = New ClassD
oD1.SetField &quot;Attr1&quot;, &quot;123&quot;
oD1.SetField &quot;Attr2&quot;, 123
oD1.SetField &quot;Attr3&quot;, Array(123, 456)
Set oD2 = oD1.Clone
oD1.SetField &quot;Attr1&quot;, &quot;456&quot;
Response.Write oD1.GetField(&quot;Attr1&quot;) &amp;amp; vbCrLf ' 顯示 123
Response.Write oD1.GetField(&quot;Attr2&quot;) &amp;amp; vbCrLf ' 顯示 123
Response.Write oD2.GetField(&quot;Attr1&quot;) &amp;amp; vbCrLf ' 顯示 456
Response.Write oD2.GetField(&quot;Attr2&quot;) &amp;amp; vbCrLf ' 顯示 123
%&gt;

</code></pre><p>在 Clone 函式中，我讓物件自己產生自己，然後利用 CloneFields 把現在物件的屬性值複製一份到新的物件上，這也就是我為什麼要用 Dictionary 物件來存放物件屬性值的原因。</p>
<h2 id="dictionary-物件">Dictionary 物件</h2>
<p>Dictionary 物件可以參考微軟的 <a href="http://msdn.microsoft.com/library/en-us/script56/html/b4a7ddb3-2474-49ef-8540-8d67a747c8db.asp">MSDN</a> ，這裡我僅簡單說明一下它的存取方式。我常用的 Dictionary 存取方式有兩種：第一種是當你不想透過索引值來處理每一個項目：</p>
<pre><code>&lt;%
Set oTempDic = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
oTempDic.Add oTempDic.Count &quot;1&quot;
oTempDic.Add oTempDic.Count &quot;2&quot;
oTempDic.Add oTempDic.Count &quot;3&quot;
oTempDic.Add oTempDic.Count &quot;4&quot;
oTempDic.Add oTempDic.Count &quot;5&quot;
Dim oItem
For Each oItem In oTempDic.Items
    Response.Write oItem &amp;amp; vbCrLf
Next
%&gt;

</code></pre><p>第二種方式則是如果你需要索引值時：</p>
<pre><code>&lt;%
Set oTempDic = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
oTempDic.Add oTempDic.Count &quot;1&quot;
oTempDic.Add oTempDic.Count &quot;2&quot;
oTempDic.Add oTempDic.Count &quot;3&quot;
oTempDic.Add oTempDic.Count &quot;4&quot;
oTempDic.Add oTempDic.Count &quot;5&quot;
Dim oItem
Dim sKey
For Each sKey In oTempDic
    Set oItem = oTempDic(sKey) // 注意這裡
    Response.Write sKey &amp;amp; &quot; =&gt; &quot; &amp;amp; oItem &amp;amp; vbCrLf
Next
%&gt;

</code></pre><p>Dictionary 物件非常有用，我常用它來存放物件屬性。而且它能夠存放到 Session 當中，因此後面有很多有用的技巧都會依賴它來實現。</p>
<h2 id="註冊機制">註冊機制</h2>
<p>註冊機制是一個很有趣的方式，我常用它來做一些同類型資料或是動作上的處理；例如產生下拉式選單，或是表單完成後一連串的表格更新。常見的方式如下：</p>
<pre><code>&lt;%
Class ClassE
    ' 物件屬性
    Private Items
    ' 物件初始化
    Private Sub Class_Initialize()
        Set Items = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
    End Sub
    ' 註冊項目
    Public Sub Register(vItem)
        Items.Add Items.Count, vItem
    End Sub
    ' 依序處理每個項目
    Public Function DoSomething()
        Dim vItem
        For Each vItem In Items.Items
            Response.Write vItem &amp;amp; vbCrLf
        Next
    End Function
    ' 物件結束
    Private Sub Class_Terminate()
        Set Items = Nothing
    End Sub
End Class
Dim oE : Set oE = New ClassE
With oE
    .Register &quot;123&quot;
    .Register &quot;456&quot;
    .Register &quot;789&quot;
    .DoSomething
End With
%&gt;

</code></pre><p>vItem 如果是同類型的物件，其實就和 Observer Pattern 很像。我曾用這種方式來更新一連串的表格，也就是讓每個 vItem 成為一個執行 SQL 指令的物件，在接到要更新的資料後，讓每個 vItem 負責自己所要儲存的表格。</p>
<h2 id="相關文章">相關文章</h2>
<ul>
<li><a href="http://www.jaceju.net/blog/archives/51/">ASP 物件導向 (1) - 基礎</a></li>
<li><a href="http://www.jaceju.net/blog/archives/52/">ASP 物件導向 (2) - 初級技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/54/">ASP 物件導向 (3) - 進階技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/57/">ASP 物件導向 (4) - 動態載入類別</a></li>
<li><a href="http://www.jaceju.net/blog/archives/59/">ASP 物件導向 (5) - Me 關鍵字</a></li>
<li><a href="http://www.jaceju.net/blog/archives/76/">ASP 物件導向 (6) - 單元測試</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>ASP 物件設計手法 (3) - 進階技巧</title>
			<link>https://jaceju.net/classic-asp-oo-3/</link>
			<pubDate>Tue, 06 Dec 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/classic-asp-oo-3/</guid>
			<description>接下來的技巧會比較複雜一點，不過如果能夠善用的話，就會是一項很好用的武器。我在較大型的專案裡用過這樣的方式，它提供了我在開發程式不一樣的角度</description>
			<content type="html"><![CDATA[<p>接下來的技巧會比較複雜一點，不過如果能夠善用的話，就會是一項很好用的武器。我在較大型的專案裡用過這樣的方式，它提供了我在開發程式不一樣的角度。至少我不必再寫一些複雜的判斷式，減少錯誤的發生。當然錯誤還是會有，但這是個人思考邏輯的問題，和物件導向開發方式關係不大。</p>
<!-- raw HTML omitted -->
<h2 id="集合體">集合體</h2>
<p>設計購物車時，我常用集合體的方式來完成。一個購物車基本型如下：</p>
<pre><code class="language-aspx-vb" data-lang="aspx-vb">&lt;%
' 購物車
Class Shopping_Cart
    Private List

    Private Fields

    ' 物件初始化
    Private Sub Class_Initialize()
        Set List = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
        Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
        Fields.Add &quot;Amount&quot;, 0
    End Sub

    ' 加入商品
    Public Sub AddProduct(oProduct)
        List.Add List.Count, oProduct
    End Sub

    ' 更新購物車
    Public Sub Update()
        Dim oProduct
        Fields(&quot;Amount&quot;) = 0
        For Each oProduct In List.Items
            oProduct.Refresh
            Fields(&quot;Amount&quot;) = Fields(&quot;Amount&quot;) + oProduct.GetField(&quot;SubTotal&quot;)
        Next
    End Sub

    ' 取得總金額
    Public Function GetAmount()
        GetAmount = Fields(&quot;Amount&quot;)
    End Function

    ' 物件結束
    Private Sub Class_Terminate()
        Set List = Nothing
        Set Fields = Nothing
    End Sub
End Class
%&gt;

</code></pre><p>我利用了 Dictionary 物件來幫我維護每項商品物件，也用到了註冊機制。集合體的類型還有很多種，以上是較基本的操作方式。</p>
<p>商品的部份很簡單，就是基本的設定屬性及取得屬性。為了方便說明，我沒有把商品名稱或其他商品屬性放進去。</p>
<pre><code class="language-aspx-vb" data-lang="aspx-vb">&lt;%
' 商品
Class Shopping_Product
    Private Fields

    Private Sub Class_Initialize()
        Set Fields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
        Fields.Add &quot;Price&quot;, 0
        Fields.Add &quot;Quantity&quot;, 0
        Fields.Add &quot;SubTotal&quot;, 0
    End Sub

    Private Sub Class_Terminate()
        Set Fields = Nothing
    End Sub

    Public Sub SetField(sName, vValue)
        If Fields.Exists(sName) Then
            Fields(sName) = vValue
        End If
    End Sub

    Public Function GetField(sName)
        GetField = Null
        If Fields.Exists(sName) Then
            GetField = Fields(sName)
        End If
    End Function

    Public Sub Refresh()
        Fields(&quot;SubTotal&quot;) = Fields(&quot;Price&quot;) * Fields(&quot;Quantity&quot;)
    End Sub
End Class
%&gt;

</code></pre><p>以下是一個簡單的測試：</p>
<pre><code>&lt;!-- #include virtual=&quot;/inc/class/Shopping/Cart.asp&quot; --&gt;
&lt;!-- #include virtual=&quot;/inc/class/Shopping/Product.asp&quot; --&gt;
&lt;%
Dim oCart : Set oCart = New Shopping_Cart
Dim oP1 : Set oP1 = New Shopping_Product
Dim oP2 : Set oP2 = New Shopping_Product
Dim oP3 : Set oP3 = New Shopping_Product
oP1.SetField &quot;Price&quot;, 100
oP1.SetField &quot;Quantity&quot;, 1
oP2.SetField &quot;Price&quot;, 120
oP2.SetField &quot;Quantity&quot;, 2
oP3.SetField &quot;Price&quot;, 150
oP3.SetField &quot;Quantity&quot;, 1
oCart.AddProduct oP1
oCart.AddProduct oP2
oCart.AddProduct oP3
oCart.Update
Response.Write oCart.GetAmount
%&gt;

</code></pre><h2 id="序列化及反序列化">序列化及反序列化</h2>
<p>這裡說的序列化其實不是真的序列化，只是我找不到好的名詞來說明。之前提過利用 Class 關鍵字所產生出來的物件是不能存在 Session 中的，因此我會利用 Dictionary 這種用 Server.CreateObject 所產生出來的物件來存放物件資訊。</p>
<p>以上面的購物車為例，如果我想要把商品資料存入 Session ，不能把 Cart 直接存入 Session 中，這樣會使得放在 List 屬性中的商品資訊消失；這時候就要改用 Dictionary 物件來儲存。</p>
<p>商品的序列化動作比較簡單，就是將 Fields 屬性傳回即可；而反序列化時，則是把取出的 Fields 屬性指定回去：</p>
<pre><code class="language-aspx-vb" data-lang="aspx-vb">&lt;%
' 商品
Class Shopping_Product
    Private Fields

    ' ... 略 ...

    Public Function Serialize()
        Set Serialize = Fields
    End Function

    Public Function Unserialize(oFields)
        Set Fields = oFields
    End Function
End Class
%&gt;

</code></pre><p>至於購物車的序列化比較複雜一點，首先要將每個商品的序列化存到一個 Dictionary 物件 (oItems)，再把這個 Dictionary 物件 (oItems) 放到 Cart 的序列化物件 (oFields) 中，最後回傳 Cart 的序列化物件；反序列化則正好相反。不過反序列化要先產生一個商品物件 (oProduct) ，再把商品的序列化物件 (oItem) 指定給該商品物件。</p>
<pre><code class="language-aspx-vb" data-lang="aspx-vb">&lt;%
' 購物車
Class Shopping_Cart
    Private List

    Private Fields

    ' ... 略 ....

    Public Function Serialize()
        Dim oFields : Set oFields = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
        Dim oItems : Set oItems = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
        Dim oProduct
        &lt;strong&gt;For Each oProduct In List.Items&lt;/strong&gt;
        &lt;strong&gt;oItems.Add oItems.Count, oProduct.Serialize&lt;/strong&gt;
        &lt;strong&gt;Next&lt;/strong&gt;
        &lt;strong&gt;oFields.Add &quot;Items&quot;, oItems&lt;/strong&gt;
        oFields.Add &quot;Amount&quot;, Fields(&quot;Amount&quot;)
        Set Serialize = oFields
        Set oItems = Nothing
        Set oFields = Nothing
    End Function

    Public Sub Unserialize(oSerializedFields)
        If oSerializedFields Is Nothing Then Exit Sub
        &lt;strong&gt;Set List = Server.CreateObject(&quot;Scripting.Dictionary&quot;)&lt;/strong&gt;
        Dim oItem
        Dim oProduct
        For Each oItem In oSerializedFields(&quot;Items&quot;).Items
            &lt;strong&gt;Set oProduct = New Shopping_Product&lt;/strong&gt;
            &lt;strong&gt;oProduct.Unserialize oItem&lt;/strong&gt;
        List.Add List.Count, oProduct
        Next
        Fields(&quot;Amount&quot;) = oSerializedFields(&quot;Amount&quot;)
    End Sub
End Class
%&gt;

</code></pre><p>用法如下：</p>
<pre><code>&lt;!-- #include virtual=&quot;/inc/class/Shopping/Cart.asp&quot; --&gt;
&lt;!-- #include virtual=&quot;/inc/class/Shopping/Product.asp&quot; --&gt;
&lt;%
Dim oCart : Set oCart = New Shopping_Cart
Dim oP1 : Set oP1 = New Shopping_Product
Dim oP2 : Set oP2 = New Shopping_Product
Dim oP3 : Set oP3 = New Shopping_Product
oP1.SetField &quot;Price&quot;, 100
oP1.SetField &quot;Quantity&quot;, 1
oP2.SetField &quot;Price&quot;, 120
oP2.SetField &quot;Quantity&quot;, 2
oP3.SetField &quot;Price&quot;, 150
oP3.SetField &quot;Quantity&quot;, 1
oCart.AddProduct oP1
oCart.AddProduct oP2
oCart.AddProduct oP3
oCart.Update
Set Session(&quot;Cart&quot;) = oCart.Serialize
Dim oNewCart : Set oNewCart = New Shopping_Cart
oNewCart.Unserialize Session(&quot;Cart&quot;)
%&gt;

</code></pre><h2 id="巢狀-dictionary-物件">巢狀 Dictionary 物件</h2>
<p>這是另一種集合體和序列化的變形，通常是用在把資料庫中的資料轉換成 Dictionary 物件上。主要原理是在迴圈中建立一個 Dictionary 物件 (oItem) ，然後在設定好內容後，把它指定給最外層的 Dictionary 物件 (oItems) 。</p>
<pre><code>&lt;%
Dim oItems : Set oItems = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
Dim oItem
Dim oField
While Not oTempRS.EOF
    Set oItem = Server.CreateObject(&quot;Scripting.Dictionary&quot;)
    ' 將所有欄位放到 oItem 中
    For Each oField In oTempRS.Fields
        oItem.Add oField.Name, oField.Value
    Next
    oItems.Add oItems.Count oItem
    Set oItem = Nothing
Wend
Response.Write oItems(0)(&quot;Attr1&quot;) &amp;amp; vbCrLf
Response.Write oItems(0)(&quot;Attr2&quot;) &amp;amp; vbCrLf
Response.Write oItems(1)(&quot;Attr1&quot;) &amp;amp; vbCrLf
Response.Write oItems(1)(&quot;Attr2&quot;) &amp;amp; vbCrLf
%&gt;

</code></pre><h2 id="總結">總結</h2>
<p>雖然 ASP (VBScript) 在物件導向上的機制不如 ASP (JScript) 來得先進，但是以上的技巧還是能夠解決多數的問題。這些技巧都是我在開發專案時所得到的心得，如果有更多有趣的方法，也歡迎大家分享。</p>
<h2 id="相關文章">相關文章</h2>
<ul>
<li><a href="http://www.jaceju.net/blog/archives/51/">ASP 物件導向 (1) - 基礎</a></li>
<li><a href="http://www.jaceju.net/blog/archives/52/">ASP 物件導向 (2) - 初級技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/54/">ASP 物件導向 (3) - 進階技巧</a></li>
<li><a href="http://www.jaceju.net/blog/archives/57/">ASP 物件導向 (4) - 動態載入類別</a></li>
<li><a href="http://www.jaceju.net/blog/archives/59/">ASP 物件導向 (5) - Me 關鍵字</a></li>
<li><a href="http://www.jaceju.net/blog/archives/76/">ASP 物件導向 (6) - 單元測試</a></li>
</ul>
]]></content>
		</item>
		
		<item>
			<title>撰寫可以接受 callback 虛擬型態參數的函式</title>
			<link>https://jaceju.net/write-php-function-with-callback/</link>
			<pubDate>Thu, 08 Sep 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/write-php-function-with-callback/</guid>
			<description>前面介紹過 callback 虛擬型態，但不限於 PHP 內建函式，我們也可以在函式中自行處理 callback 虛擬型態的參數。 例如 PHP 內建了一個 array_map 函式，但是無法處理多階陣列。我們可以</description>
			<content type="html"><![CDATA[<p>前面介紹過 callback 虛擬型態，但不限於 PHP 內建函式，我們也可以在函式中自行處理 callback 虛擬型態的參數。</p>
<!-- raw HTML omitted -->
<p>例如 PHP 內建了一個 array_map 函式，但是無法處理多階陣列。我們可以自行建立一個 array_map_recursive 函式來使用，當然其介面也得和 array_map 一樣：</p>
<pre><code>&lt;?php
/**
 * 用法：
 * $a = array (1, 2, 3, 4, 5);
 * $a = array_map_recursive('function_name', $a); // callback function
 * $a = array_map_recursive(array ('class_name', 'class_method'), $a); // callback static class method
 * $a = array_map_recursive(array (&amp;amp; $object, 'object_method'), $a); // callback object method
 */
function array_map_recursive($func, $arr)
{
    $result = array();
    foreach ($arr as $key =&gt; $value) {
        if (is_array($value)) {
            $result[$key] = array_map_recursive($func, $value);
        } else {
            if (is_array($func)) {
                if (is_object($func[0]))
                    $result[$key] = $func[0]-&gt;$func[1]($value);
                if (is_string($func[0]))
                    eval('$result[$key] = ' . $func[0] . '::' . $func[1] . '($value);');
            } else {
                $result[$key] = $func($value);
            }
        }
    }
    return $result;
}

</code></pre><p>其中 $func 參數就是 callback 虛擬型態。</p>
]]></content>
		</item>
		
		<item>
			<title>PHP 的 callback 虛擬型態</title>
			<link>https://jaceju.net/php-callback/</link>
			<pubDate>Thu, 25 Aug 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-callback/</guid>
			<description>PHP 提供了一種很有趣的虛擬型態 (Pseudo-type) : callback ，它其實是字串或陣列組成。主要用來處理一些有不容易寫死在程式裡的函式名稱。 它可以是以下寫法： // 呼叫函式，相</description>
			<content type="html"><![CDATA[<p>PHP 提供了一種很有趣的虛擬型態 (Pseudo-type) : callback ，它其實是字串或陣列組成。主要用來處理一些有不容易寫死在程式裡的函式名稱。</p>
<!-- raw HTML omitted -->
<p>它可以是以下寫法：</p>
<pre><code>// 呼叫函式，相當於 functionName();
'functionName'
// 呼叫類別靜態方法，相當於 className::methodName();
array ('className', 'methodName');
// 呼叫物件方法，相當於 $object-&gt;methodName();
array ($object, 'methodName');

</code></pre><p>例如：</p>
<pre><code>&lt;?php
$ary = array (
    'abc',
    'def',
    'ghi',
    array (
        '123',
        '456',
        '789',
    ),
);
function doSomething(&amp;amp; $v)
{
    $v = 'f : ' . $v;
}
class TestClass
{
    public function doSomething(&amp;amp; $v)
    {
        $v = 'c : ' . $v;
    }
}
$test = new TestClass();
var_dump(call_user_func('doSomething', 123));
var_dump(call_user_func(array ('TestClass', 'doSomething'), 123));
var_dump(call_user_func(array ($test, 'doSomething'), 123));
array_walk_recursive($ary, 'doSomething');
var_dump($ary);
array_walk_recursive($ary, array ('TestClass', 'doSomething'));
var_dump($ary);
array_walk_recursive($ary, array ($test, 'doSomething'));
var_dump($ary);
?&gt;

</code></pre>]]></content>
		</item>
		
		<item>
			<title>Smarty 入門</title>
			<link>https://jaceju.net/getting-started-with-smarty/</link>
			<pubDate>Wed, 08 Jun 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/getting-started-with-smarty/</guid>
			<description>這是我一年前寫的文章，之前寄放在朋友的站：[PHP 5知識交換中心]。 不過因為有針對舊有的內容做一些小調整，所以這次把它放回到自己的 Blog 裡。 序言</description>
			<content type="html"><![CDATA[<p>這是我一年前寫的文章，之前寄放在朋友的站：<a href="http://www.php5.idv.tw/">[PHP 5知識交換中心]</a>。</p>
<p>不過因為有針對舊有的內容做一些小調整，所以這次把它放回到自己的 Blog 裡。</p>
<h2 id="序言">序言</h2>
<p>剛開始接觸樣版引擎的 PHP 設計師，聽到 Smarty 時，都會覺得很難。其實筆者也不例外，碰都不敢碰一下。但是後來在剖析 XOOPS 的程式架構時，開始發現 Smarty 其實並不難。只要將 Smarty 基礎功練好，在一般應用上就已經相當足夠了。當然基礎能打好，後面的進階應用也就不用怕了。</p>
<p>這次的更新，主要加上了一些概念性的東西，當然也有一些進階的技巧。不過這些也許早已深入大家的程式之中，如果有更好的觀點，也歡迎大家能夠回饋。文章中的程式架構僅供參考，並不是最好的作法。不過如果剛學習Smarty，倒是可以先以這樣的寫法為基礎。</p>
<p>jaceju@gmail 2005/03/12</p>
<h2 id="smarty-介紹">Smarty 介紹</h2>
<h3 id="什麼是樣版引擎">什麼是樣版引擎</h3>
<p>不知道從什麼時候開始，有人開始對 HTML 內嵌入 Server Script 覺得不太滿意。然而不論是微軟的 ASP 或是開放源碼的 PHP，都是屬於內嵌 Server Script 的網頁伺服端語言。因此也就有人想到，如果能把程式應用邏輯 (或稱商業應用邏輯) 與網頁呈現 (Layout) 邏輯分離的話，是不是會比較好呢？</p>
<p>其實這個問題早就存在已久，從互動式網頁開始風行時，不論是 ASP 或是 PHP 的使用者都是身兼程式開發者與視覺設計師兩種身份。可是通常這些使用者不是程式強就是美工強，如果要兩者同時兼顧，那可得死掉不少腦細胞&hellip;</p>
<p>所以樣版引擎就應運而生啦！樣版引擎的目的，就是要達到上述提到的邏輯分離的功能。它能讓程式開發者專注於資料的控制或是功能的達成；而視覺設計師則可專注於網頁排版，讓網頁看起來更具有專業感！因此樣版引擎很適合公司的網站開發團隊使用，使每個人都能發揮其專長！</p>
<p>就筆者接觸過的樣版引擎來說，依資料呈現方式大概分成：需搭配程式處理的樣版引擎和完全由樣版本身自行決定的樣版引擎兩種形式。</p>
<p>在需搭配程式處理的樣版引擎中，程式開發者必須要負責變數的呈現邏輯，也就是說他必須把變數的內容在輸出到樣版前先處理好，才能做 assign 的工作。換句話說，程式開發者還是得多寫一些程式來決定變數呈現的風貌。而完全由樣版本身自行決定的樣版引擎，它允許變數直接 assign 到樣版中，讓視覺設計師在設計樣版時再決定變數要如何呈現。因此它就可能會有另一套屬於自己的樣版程式語法 (如 Smarty) ，以方便控制變數的呈現。但這樣一來，視覺設計師也得學習如何使用樣版語言。</p>
<h3 id="樣版引擎的運作原理">樣版引擎的運作原理</h3>
<p>首先我們先看看以下的運作圖：</p>
<p><img src="/resources/smarty_basic/1-01.gif" alt="test"></p>
<p>一般的樣版引擎 (如 PHPLib) 都是在建立樣版物件時取得要解析的樣版，然後把變數套入後，透過 parse() 這個方法來解析樣版，最後再將網頁輸出。</p>
<p><img src="/resources/smarty_basic/1-02.gif" alt="test"></p>
<p>對 Smarty 的使用者來說，<strong>程式裡也不需要做任何 parse 的動作了</strong>，這些 Smarty 自動會幫我們做。而且已經編譯過的網頁，如果樣版沒有變動的話， Smarty 就自動跳過編譯的動作，直接執行編譯過的網頁，以節省編譯的時間。</p>
<h3 id="使用-smarty-的一些概念">使用 Smarty 的一些概念</h3>
<p>在一般樣版引擎中，我們常看到區域的觀念，所謂區塊大概都會長成這樣：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="c">&lt;!-- START : Block name --&gt;</span>
區域內容
<span class="c">&lt;!-- END : Block name --&gt;</span>
</code></pre></div><p>這些區塊大部份都會在 PHP 程式中以 <code>if</code> 或 <code>for</code>, <code>while</code> 來控制它們的顯示狀態，雖然樣版看起來簡潔多了，但只要一換了顯示方式不同的樣版， PHP 程式勢必要再改一次！</p>
<p>**在 Smarty 中，一切以變數為主，所有的呈現邏輯都讓樣版自行控制。**因為 Smarty 會有自己的樣版語言，所以不管是區塊是否要顯示還是要重覆，都是用 Smarty 的樣版語法 (if, foreach, section) 搭配變數內容作呈現。這樣一來感覺上好像樣版變得有點複雜，但好處是只要規劃得當， PHP 程式一行都不必改。</p>
<p>由上面的說明，我們可以知道使用 Smarty 要掌握一個原則：將程式應用邏輯與網頁呈現邏輯明確地分離。就是說 PHP 程式裡不要有太多的 HTML 碼。程式中只要決定好那些變數要塞到樣版裡，讓樣版自己決定該如何呈現這些變數 (甚至不呈現也行) 。</p>
<h2 id="smarty-的基礎">Smarty 的基礎</h2>
<h3 id="安裝-smarty">安裝 Smarty</h3>
<p>首先，我們先決定程式放置的位置。</p>
<ul>
<li>Windows下可能會類似這樣的位置： <code>d:\appserv\web\demo\</code> 。</li>
<li>Linux下可能會類似這樣的位置： <code>/home/jaceju/public_html/</code> 。</li>
</ul>
<p>接著在程式主資料夾下建立一個 <code>library</code> 資料夾，這是我們用來放置 Smarty 套件的地方。</p>
<p>然後我們到 Smarty 的官方網站下載最新的 Smarty 套件： <a href="http://smarty.php.net">http://smarty.php.net</a>。</p>
<p>解開 Smarty 2.6.0 後，會看到很多檔案，其中有個 libs 資料夾。在 libs 中應該會有 3 個 <code>class.php</code> 檔 + 1 個 <code>debug.tpl</code> + 1 個 <code>plugin</code> 資料夾 + 1 個 <code>core</code> 資料夾。**然後直接將 <code>libs</code> 複製到剛剛建立的 <code>library</code> 資料夾下，再更名為 Smarty 就可以了。**就這樣？沒錯！這種安裝法比較簡單，適合一般沒有自己主機的使用者。</p>
<p>至於 Smarty 官方手冊中為什麼要介紹一些比較複雜的安裝方式呢？基本上依照官方的方式安裝，可以只在主機安裝一次，然後提供給該主機下所有設計者開發不同程式時直接引用，而不會重覆安裝太多的 Smarty 複本。而筆者所提供的方式則是適合要把程式帶過來移過去的程式開發者使用，這樣不用煩惱主機有沒有安裝 Smarty 。</p>
<h3 id="程式的資料夾設定">程式的資料夾設定</h3>
<p>以筆者在Windows安裝Appserv為例，程式的主資料夾是 <code>d:\appserv\web\demo\</code> 。安裝好 Smarty 後，我們在主資料夾下再建立這樣的資料夾：</p>
<p><img src="/resources/smarty_basic/2-01.gif" alt="test"></p>
<p><strong>在 Linux 底下，請記得將 <code>templates_c</code> 的權限變更為 <code>777 </code>。</strong> Windows 下則將其唯讀取消。</p>
<h3 id="第一個用-smarty-寫的小程式">第一個用 Smarty 寫的小程式</h3>
<p>我們先設定 Smarty 的路徑，請將以下這個檔案命名為 <code>main.php</code> ，並放置到主資料夾下：</p>
<p><strong>main.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">include</span> <span class="s2">&#34;library/Smarty/Smarty.class.php&#34;</span><span class="p">;</span>
<span class="nx">define</span><span class="p">(</span><span class="s1">&#39;APP_PATH&#39;</span><span class="p">,</span> <span class="nx">str_replace</span><span class="p">(</span><span class="s1">&#39;\\&#39;</span><span class="p">,</span> <span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="nx">dirname</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">)));</span>
<span class="nv">$tpl</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Smarty</span><span class="p">();</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">template_dir</span> <span class="o">=</span> <span class="nx">APP_PATH</span> <span class="o">.</span> <span class="s2">&#34;/templates/&#34;</span><span class="p">;</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">compile_dir</span> <span class="o">=</span> <span class="nx">APP_PATH</span> <span class="o">.</span> <span class="s2">&#34;/templates_c/&#34;</span><span class="p">;</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">config_dir</span> <span class="o">=</span> <span class="nx">APP_PATH</span> <span class="o">.</span> <span class="s2">&#34;/configs/&#34;</span><span class="p">;</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">cache_dir</span> <span class="o">=</span> <span class="nx">APP_PATH</span> <span class="o">.</span> <span class="s2">&#34;/cache/&#34;</span><span class="p">;</span>
</code></pre></div><p>照上面方式設定的用意在於，程式如果要移植到其他地方，只要改 <code>APP_PATH</code> 就可以啦。 (這裡是參考 XOOPS 的 )</p>
<p>Smarty 的樣版路徑設定好後，程式會依照這個路徑來抓所有樣版的相對位置 (範例中是 <code>d:/appserv/web/demo/templates/</code> ) 。然後我們用 <code>display()</code> 這個 Smarty 方法來顯示我們的樣版。</p>
<p>接下來我們在 <code>templates</code> 資料夾下放置一個 <code>test.htm</code> ： (副檔名叫什麼都無所謂，但便於視覺設計師開發，筆者都還是以 <code>.htm</code> 為主。)</p>
<p><strong>templates/test.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">&#34;Content-Type&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;text/html; charset=utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>{$title}<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
{$content}
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div><p>現在我們要將上面的樣版顯示出來，並將網頁標題 (<code>$title</code>) 與內容 (<code>$content</code>) 更換，請將以下檔案內容命名為 <code>test.php</code> ，並放置在主資料夾下：</p>
<p><strong>test.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require</span> <span class="s2">&#34;main.php&#34;</span><span class="p">;</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">,</span> <span class="s2">&#34;測試用的網頁標題&#34;</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">,</span> <span class="s2">&#34;測試用的網頁內容&#34;</span><span class="p">);</span>
<span class="c1">// 上面兩行也可以用這行代替
</span><span class="c1">// $tpl-&gt;assign(array(&#34;title&#34; =&gt; &#34;測試用的網頁標題&#34;, &#34;content&#34; =&gt; &#34;測試用的網頁內容&#34;));
</span><span class="c1"></span><span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s1">&#39;test.htm&#39;</span><span class="p">);</span>
</code></pre></div><p>請打開瀏覽器，輸入 <code>http://localhost/demo/test.php</code> 試試看(依您的環境決定網址)，應該會看到以下的畫面：</p>
<p><img src="/resources/smarty_basic/2-02.gif" alt="test"></p>
<p>再到 <code>templates_c</code> 底下，我們會看到其中有一個奇怪的檔案：</p>
<p><strong>%%6B^6B2^6B2BF9A3%%test.htm.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span> <span class="cm">/* Smarty version 2.6.14, created on 2007-08-11 14:10:33
</span><span class="cm">compiled from test.htm */</span> <span class="cp">?&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">&#34;Content-Type&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;text/html; charset=utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span><span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_tpl_vars</span><span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">];</span> <span class="cp">?&gt;</span>
<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="cp">&lt;?php</span> <span class="k">echo</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="na">_tpl_vars</span><span class="p">[</span><span class="s1">&#39;content&#39;</span><span class="p">];</span> <span class="cp">?&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div><p>沒錯，這就是 Smarty 編譯過的檔案。它將我們在樣版中的變數轉換成了 PHP 的語法來執行，下次再讀取同樣的內容時， Smarty 就會直接抓取這個檔案來執行了。</p>
<p>最後我們整理一下整個 Smarty 程式撰寫步驟：</p>
<ul>
<li>載入 Smarty 樣版引擎。</li>
<li>建立 Smarty 物件。</li>
<li>設定 Smarty 物件的參數。</li>
<li>在程式中處理變數後，再用 Smarty 的 <code>assign</code> 方法將變數置入樣版裡。</li>
<li>利用 Smarty 的 <code>display</code> 方法將網頁秀出。</li>
</ul>
<h3 id="如何安排你的程式架構">如何安排你的程式架構</h3>
<p>上面我們看到除了 Smarty 所需要的資料夾外 (<code>library</code> 、  <code>templates</code> 、 <code>templates_c</code>) ，還有兩個資料夾： <code>configs</code> 、 <code>modules</code> 。其實這是筆者模仿 XOOPS 的架構所建立出來的，因為 XOOPS 是筆者所接觸到的程式中，少數使用 Smarty 樣版引擎的架站程式。所謂西瓜偎大邊，筆者這樣的程式架構雖沒有 XOOPS 的百分之一強，但至少給人看時還有 XOOPS 撐腰。</p>
<ul>
<li>
<p><code>configs</code> 資料夾一般都是用來存放設定檔的，當然也可以用放存放 Smarty 會用到的 config 設定。</p>
</li>
<li>
<p><code>modules</code> 這個資料夾則是用來放置程式模組的，如此一來便不會把程式丟得到處都是，整體架構一目瞭然。</p>
</li>
</ul>
<p>上面我們也提到 <code>main.php</code> ，這是整個程式的主要核心，不論是常數定義、外部程式載入、共用變數建立等，都是在這裡開始的。所以之後的模組都只要將這個檔案包含進來就可以啦。因此在程式流程規劃期間，就必須好好構思 <code>main.php</code> 中應該要放那些東西；當然利用 <code>include</code> 或 <code>require</code> 指令，把每個環節清楚分離是再好不過了。</p>
<p><img src="/resources/smarty_basic/2-03.gif" alt="test"></p>
<p>在上節提到的 Smarty 程式 5 步驟， <code>main.php</code> 就會幫我們先將前 3 個步驟做好，後面的模組程式只要做後面兩個步驟就可以了。</p>
<h2 id="從變數開始">從變數開始</h2>
<h3 id="如何使用變數">如何使用變數</h3>
<p>從上一章範例中，我們可以清楚地看到我們利用 <strong><code>{</code></strong> 及 <strong><code>}</code></strong> 這兩個標示符號 (delimiter) 將變數包起來，這是 Smarty 預設的標示符號。變數的命名方式和 PHP 的變數命名方式是一模一樣的，前面也有個 <code>$</code> 字號 (這和一般的樣版引擎不同)。標示符號就有點像是 PHP 中的 <code>&lt;?php</code> 及 <code>?&gt;</code> (事實上它們在大部份的解析過程中，的確會被替換成這個符號) ，所以以下的樣版變數寫法都是可行的：</p>
<ul>
<li>一般的寫法：</li>
</ul>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"> {$var}
</code></pre></div><ul>
<li>與變數中間留有空白：</li>
</ul>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">{ $var }
</code></pre></div><ul>
<li>啟始的標示符號和結束的標示符號不在同一行</li>
</ul>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">{$var
}
</code></pre></div><p>註：有時為了中文衝碼及 JavaScript 的關係，我們透過 <code>$left_delimiter</code> 和 <code>$right_delimiter</code> 兩個 Smarty 類別屬性，來將標示符號換掉。</p>
<p>在 Smarty 裡，<strong>變數預設是全域的</strong>，也就是說你只要指定一次就好了。**指定兩次以上的話，變數內容會以最後指定的為主。**就算我們在主樣版中載入了外部的子樣版，子樣版中同樣的變數一樣也會被替代，這樣我們就不用再針對子樣版再做一次解析的動作。</p>
<p>如果想要顯示的資料已經放在一個陣列裡了，是否要將裡面的變數分離出來，再做 assign 的動作呢？其實不需要！我們可以直接就把這個陣列 assign 到樣版物件中，如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$this_user</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="s2">&#34;fullname&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;Jace Ju&#34;</span><span class="p">,</span> <span class="s2">&#34;phone&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;012345678&#34;</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;jaceju@seed.net.tw&#34;</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;this_user&#34;</span><span class="p">,</span> <span class="nv">$this_user</span><span class="p">);</span>
</code></pre></div><p>然後在樣版中這裡使用：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">{$this_user.fullname}
{$this_user.phone}
{$this_user.email}
</code></pre></div><p>如此一來就不需將陣列分離， PHP 程式裡也不會有一大堆的 assign 了。</p>
<p>另外我們也可以把「物件」 assign 到 Smarty 裡，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$obj</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">SomeObject</span><span class="p">();</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;obj&#34;</span><span class="p">,</span> <span class="nv">$obj</span><span class="p">);</span>
</code></pre></div><p>而在樣版中也是與 PHP 一樣使用「 -&gt; 」來存取該物件的屬性與方法。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">{$obj-&gt;fullname}
{$obj-&gt;method1()}
</code></pre></div><h3 id="修飾你的變數">修飾你的變數</h3>
<p>上面我們提到 Smarty 變數呈現的風貌是由樣版自行決定的，所以 Smarty 提供了許多修飾變數的函式。使用的方法如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">{變數|修飾函式} <span class="c">&lt;!-- 當修飾函式沒有參數時 --&gt;</span>
{變數|修飾函式:&#34;參數(非必要，視函式而定)&#34;} <span class="c">&lt;!-- 當修飾函式有參數時 --&gt;</span>
</code></pre></div><p>範例如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">{$var|nl2br} <span class="c">&lt;!-- 將變數中的換行字元換成 &lt;br /&gt; --&gt;</span>
{$var|string_format:&#34;%02d&#34;} <span class="c">&lt;!-- 將變數格式化 --&gt;</span>
</code></pre></div><p>好，那為什麼要讓樣版自行決定變數呈現的風貌？先看看底下的 HTML ，這是某個購物車結帳的部份畫面。</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;total&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;21000&#34;</span> <span class="p">/&gt;</span>
總金額：21,000 元
</code></pre></div><p>一般樣版引擎的樣版可能會這樣寫：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;total&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;{total}&#34;</span> <span class="p">/&gt;</span>
總金額：{format_total} 元
</code></pre></div><p>它們的 PHP 程式中要這樣寫：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$total</span> <span class="o">=</span> <span class="mi">21000</span><span class="p">;</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;total&#34;</span><span class="p">,</span> <span class="nv">$total</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;format_total&#34;</span><span class="p">,</span> <span class="nx">number_format</span><span class="p">(</span><span class="nv">$total</span><span class="p">));</span>
</code></pre></div><p>而 Smarty 的樣版就可以這樣寫：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="p">&lt;</span><span class="nt">input</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;total&#34;</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;hidden&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34; {$total} &#34;</span> <span class="p">/&gt;</span>
總金額： {$total|number_format:&#34;&#34;} 元
</code></pre></div><p>Smarty 的 PHP 程式中只要這樣寫：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="nv">$total</span> <span class="o">=</span> <span class="mi">21000</span><span class="p">;</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;total&#34;</span><span class="p">,</span> <span class="nv">$total</span><span class="p">);</span>
</code></pre></div><p>所以在 Smarty 中我們只要指定一次變數，剩下的交給樣版自行決定即可。這樣瞭解了嗎？這就是讓樣版自行決定變數呈現風貌的好處！</p>
<h2 id="控制樣版的內容">控制樣版的內容</h2>
<h3 id="重覆的區塊">重覆的區塊</h3>
<p>上面筆者提到，針對單一的一階陣列，我們可以直接將它 assign 到樣版上；但如果我們想將資料庫所選取出來的多筆資料一次顯示到樣版上時，勢必要透過迴圈將資料傾印出來。在樣版裡，我們可以利用重覆的區塊來完成這樣的動作。</p>
<p>在 Smarty 樣板中，我們要重覆一個區塊有兩種方式： <code>foreach</code> 及 <code>section</code> 。而在程式中我們則要 assign 一個陣列，這個陣列中可以包含數組陣列。就像下面這個例子：</p>
<p>首先我們來看 PHP 程式是如何寫的：</p>
<p><strong>test2.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require</span> <span class="s2">&#34;main.php&#34;</span><span class="p">;</span>
<span class="nv">$array1</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span><span class="mi">1</span> <span class="o">=&gt;</span> <span class="s2">&#34;蘋果&#34;</span><span class="p">,</span> <span class="mi">2</span> <span class="o">=&gt;</span> <span class="s2">&#34;鳳梨&#34;</span><span class="p">,</span> <span class="mi">3</span> <span class="o">=&gt;</span> <span class="s2">&#34;香蕉&#34;</span><span class="p">,</span> <span class="mi">4</span> <span class="o">=&gt;</span> <span class="s2">&#34;芭樂&#34;</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;array1&#34;</span><span class="p">,</span> <span class="nv">$array1</span><span class="p">);</span>
<span class="nv">$array2</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;index1&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data1-1&#34;</span><span class="p">,</span> <span class="s2">&#34;index2&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data1-2&#34;</span><span class="p">,</span> <span class="s2">&#34;index3&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data1-3&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;index1&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data2-1&#34;</span><span class="p">,</span> <span class="s2">&#34;index2&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data2-2&#34;</span><span class="p">,</span> <span class="s2">&#34;index3&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data2-3&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;index1&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data3-1&#34;</span><span class="p">,</span> <span class="s2">&#34;index2&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data3-2&#34;</span><span class="p">,</span> <span class="s2">&#34;index3&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data3-3&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;index1&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data4-1&#34;</span><span class="p">,</span> <span class="s2">&#34;index2&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data4-2&#34;</span><span class="p">,</span> <span class="s2">&#34;index3&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data4-3&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;index1&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data5-1&#34;</span><span class="p">,</span> <span class="s2">&#34;index2&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data5-2&#34;</span><span class="p">,</span> <span class="s2">&#34;index3&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;data5-3&#34;</span><span class="p">));</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;array2&#34;</span><span class="p">,</span> <span class="nv">$array2</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s2">&#34;test2.htm&#34;</span><span class="p">);</span>
</code></pre></div><p>而樣版的寫法如下：</p>
<p><strong>templates/test2.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">&#34;Content-Type&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;text/html; charset=utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>測試重覆區塊<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">pre</span><span class="p">&gt;</span>
直接呈現 array1
{$array1.1}
{$array1.2}
{$array1.3}
{$array1.4}
利用 foreach 來呈現 array1
{foreach item=item1 from=$array1}
{$item1}
{/foreach}
利用 section 來呈現 array1
{section name=sec1 loop=$array1}
{$array1[sec1]}
{/section}
利用 foreach 來呈現 array2
{foreach item=index2 from=$array2}
{foreach key=key2 item=item2 from=$index2}
{$key2} : {$item2}
{/foreach}
{/foreach}
利用 section 來呈現 array2
{section name=sec2 loop=$array2}
index1: {$array2[sec2].index1}
index2: {$array2[sec2].index2}
index3: {$array2[sec2].index3}
{/section}
<span class="p">&lt;/</span><span class="nt">pre</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div><p>執行上例後，我們發現不管是 <code>foreach</code> 或 <code>section</code> 兩個執行結果是一樣的。那麼兩者到底有何不同呢？</p>
<p>第一個差別很明顯，就是 <code>foreach</code> 要以巢狀處理的方式來呈現我們所 assign 的兩層陣列變數，而 <code>section</code> 則以<code>主陣列[迴圈名稱].子陣列索引</code>即可將整個陣列呈現出來。由此可知， Smarty 在樣版中的 <code>foreach</code> 和 PHP 中的 <code>foreach</code> 是一樣的；而 <code>section</code> 則是 Smarty 為了處理如上列的陣列變數所發展出來的敘述。當然 <code>section</code> 的功能還不只如此，除了下一節所談到的巢狀資料呈現外，官方手冊中也提供了好幾個 <code>section</code> 的應用範例。</p>
<p><strong>不過要注意的是，丟給 <code>section</code> 的陣列索引必須是從 0 開始的正整數，即 <code>0, 1, 2, 3, ...</code>。</strong></p>
<p>如果您的陣列索引不是從 0 開始的正整數，那麼就得改用 <code>foreach</code> 來呈現您的資料。您可以參考官方討論區中的<a href="http://www.phpinsider.com/smarty-forum/viewtopic.php?t=1574">此篇討論</a>，其中探討了 <code>section</code> 和 <code>foreach</code> 的用法。</p>
<h3 id="巢狀資料的呈現">巢狀資料的呈現</h3>
<p>樣版引擎裡最令人傷腦筋的大概就是巢狀資料的呈現吧，許多著名的樣版引擎都會特意強調這點，不過這對 Smarty 來說卻是小兒科。</p>
<p>最常見到的巢狀資料，就算論譠程式中的討論主題區吧。假設要呈現的結果如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">table</span>
        <span class="na">summary</span><span class="o">=</span><span class="s">&#34;討論區格式&#34;</span>
        <span class="na">width</span><span class="o">=</span><span class="s">&#34;200&#34;</span>
        <span class="na">border</span><span class="o">=</span><span class="s">&#34;1&#34;</span>
        <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span>
        <span class="na">cellpadding</span><span class="o">=</span><span class="s">&#34;3&#34;</span>
        <span class="na">cellspacing</span><span class="o">=</span><span class="s">&#34;0&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">colspan</span><span class="o">=</span><span class="s">&#34;2&#34;</span><span class="p">&gt;</span>公告區<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;25&#34;</span><span class="p">&gt;</span><span class="ni">&amp;nbsp;</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;164&#34;</span><span class="p">&gt;</span>站務公告<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">colspan</span><span class="o">=</span><span class="s">&#34;2&#34;</span><span class="p">&gt;</span>文學專區<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;164&#34;</span> <span class="na">rowspan</span><span class="o">=</span><span class="s">&#34;2&#34;</span><span class="p">&gt;</span><span class="ni">&amp;nbsp;</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;164&#34;</span><span class="p">&gt;</span>奇文共賞<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>好書介紹<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">colspan</span><span class="o">=</span><span class="s">&#34;2&#34;</span><span class="p">&gt;</span>電腦專區<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;164&#34;</span> <span class="na">rowspan</span><span class="o">=</span><span class="s">&#34;2&#34;</span><span class="p">&gt;</span><span class="ni">&amp;nbsp;</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;164&#34;</span><span class="p">&gt;</span>軟體討論<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>硬體週邊<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</code></pre></div><p>程式中我們先以靜態資料為例：</p>
<p><strong>test3_1.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require</span> <span class="s2">&#34;main.php&#34;</span><span class="p">;</span>
<span class="nv">$forum</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;category_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;category_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;公告區&#34;</span><span class="p">,</span>
        <span class="s2">&#34;topic&#34;</span> <span class="o">=&gt;</span> <span class="k">array</span><span class="p">(</span>
            <span class="k">array</span><span class="p">(</span><span class="s2">&#34;topic_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;topic_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;站務公告&#34;</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;category_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">2</span><span class="p">,</span> <span class="s2">&#34;category_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;文學專區&#34;</span><span class="p">,</span>
        <span class="s2">&#34;topic&#34;</span> <span class="o">=&gt;</span> <span class="k">array</span><span class="p">(</span>
            <span class="k">array</span><span class="p">(</span><span class="s2">&#34;topic_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">2</span><span class="p">,</span> <span class="s2">&#34;topic_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;好書介紹&#34;</span><span class="p">),</span>
            <span class="k">array</span><span class="p">(</span><span class="s2">&#34;topic_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">&#34;topic_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;奇文共賞&#34;</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;category_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">3</span><span class="p">,</span> <span class="s2">&#34;category_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;電腦專區&#34;</span><span class="p">,</span>
        <span class="s2">&#34;topic&#34;</span> <span class="o">=&gt;</span> <span class="k">array</span><span class="p">(</span>
            <span class="k">array</span><span class="p">(</span><span class="s2">&#34;topic_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">4</span><span class="p">,</span> <span class="s2">&#34;topic_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;硬體週邊&#34;</span><span class="p">),</span>
            <span class="k">array</span><span class="p">(</span><span class="s2">&#34;topic_id&#34;</span> <span class="o">=&gt;</span> <span class="mi">5</span><span class="p">,</span> <span class="s2">&#34;topic_name&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;軟體討論&#34;</span><span class="p">)</span>
        <span class="p">)</span>
    <span class="p">)</span>
<span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;forum&#34;</span><span class="p">,</span> <span class="nv">$forum</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s2">&#34;test3.htm&#34;</span><span class="p">);</span>
</code></pre></div><p>樣版的寫法如下：</p>
<p><strong>templates/test3.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>巢狀迴圈測試<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">table</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;200&#34;</span> <span class="na">border</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span> <span class="na">cellpadding</span><span class="o">=</span><span class="s">&#34;3&#34;</span> <span class="na">cellspacing</span><span class="o">=</span><span class="s">&#34;0&#34;</span><span class="p">&gt;</span>
    {section name=sec1 loop=$forum}
        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">td</span> <span class="na">colspan</span><span class="o">=</span><span class="s">&#34;2&#34;</span><span class="p">&gt;</span> {$forum[sec1].category_name} <span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
        {section name=sec2 loop=$forum[sec1].topic}
        <span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;25&#34;</span><span class="p">&gt;</span><span class="ni">&amp;nbsp;</span><span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">td</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;164&#34;</span><span class="p">&gt;</span> {$forum[sec1].topic[sec2].topic_name} <span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
        {/section}
    {/section}
    <span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div><p>執行的結果就像筆者上面舉的例子一樣。</p>
<p>因此呢，在程式中我們只要想辦法把所要重覆值一層一層的塞到陣列中，再利用 <code>{第一層陣列[迴圈1].第二層陣列[迴圈2].第三層陣列[迴圈3]. ... .陣列索引}</code> 這樣的方式來顯示每一個巢狀迴圈中的值。至於用什麼方法呢？下一節使用資料庫時我們再提。</p>
<h3 id="轉換資料庫中的資料">轉換資料庫中的資料</h3>
<p>上面提到如何顯示巢狀迴圈，而實際上應用時我們的資料可能是從資料庫中抓取出來的，所以我們就得想辦法把資料庫的資料變成上述的多重陣列的形式。這裡筆者用 <a href="http://www.php.net/manual/en/ref.mysqli.php">MySQLi</a> 來抓取資料庫中的資料，如果各位有其他自己喜好的資料庫抽象套件的話，原理也是差不多的。</p>
<p>我們只修改 PHP 程式，樣版還是上面那個 (這就是樣版引擎的好處~)，而且抓出來的資料就是上面的例子。</p>
<p><strong>test3_2.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require</span> <span class="s2">&#34;main.php&#34;</span><span class="p">;</span>
<span class="c1">// 建立資料庫連結
</span><span class="c1"></span><span class="nv">$mysqli</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">mysqli</span><span class="p">(</span><span class="s2">&#34;localhost&#34;</span><span class="p">,</span> <span class="s2">&#34;username&#34;</span><span class="p">,</span> <span class="s2">&#34;password&#34;</span><span class="p">,</span> <span class="s2">&#34;testdb&#34;</span><span class="p">);</span>
<span class="c1">// 先建立第一層陣列及 SQL 指令
</span><span class="c1"></span><span class="nv">$category</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span>
<span class="nv">$sql1</span> <span class="o">=</span> <span class="s2">&#34;SELECT category_id, category_name FROM categories&#34;</span><span class="p">;</span>
<span class="c1">// 抓取第一層迴圈的資料
</span><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="nv">$result1</span> <span class="o">=</span> <span class="nv">$mysqli</span><span class="o">-&gt;</span><span class="na">query</span><span class="p">(</span><span class="nv">$sql1</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">while</span> <span class="p">(</span><span class="nv">$item_category</span> <span class="o">=</span> <span class="nv">$result1</span><span class="o">-&gt;</span><span class="na">fetch_assoc</span><span class="p">())</span> <span class="p">{</span>
        <span class="c1">// 建立第二層陣列及 SQL 指令
</span><span class="c1"></span>        <span class="nv">$topic</span> <span class="o">=</span> <span class="k">array</span><span class="p">();</span>
        <span class="nv">$sql2</span> <span class="o">=</span> <span class="nx">sprintf</span><span class="p">(</span>
            <span class="s2">&#34;SELECT topic_id, topic_name FROM topics WHERE category_id = &#39;%s&#39;&#34;</span><span class="p">,</span>
            <span class="nv">$item_category</span><span class="p">[</span><span class="s1">&#39;category_id&#39;</span><span class="p">]</span>
        <span class="p">);</span>
        <span class="c1">// 抓取第二層迴圈的資料
</span><span class="c1"></span>        <span class="k">if</span> <span class="p">(</span><span class="nv">$result2</span> <span class="o">=</span> <span class="nv">$mysqli</span><span class="o">-&gt;</span><span class="na">query</span><span class="p">(</span><span class="nv">$sql2</span><span class="p">))</span> <span class="p">{</span>
            <span class="k">while</span> <span class="p">(</span><span class="nv">$item_topic</span> <span class="o">=</span> <span class="nv">$result2</span><span class="o">-&gt;</span><span class="na">fetch_assoc</span><span class="p">())</span> <span class="p">{</span>
                <span class="c1">// 把抓取的資料推入第二層陣列中
</span><span class="c1"></span>                <span class="nx">array_push</span><span class="p">(</span><span class="nv">$topic</span><span class="p">,</span> <span class="nv">$item_topic</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="nv">$result2</span><span class="o">-&gt;</span><span class="na">close</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="c1">// 把第二層陣列指定為第一層陣列所抓取的資料中的一個成員
</span><span class="c1"></span>        <span class="nv">$item_category</span><span class="p">[</span><span class="s1">&#39;topic&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="nv">$topic</span><span class="p">;</span>
        <span class="c1">// 把第一層資料推入第一層陣列中
</span><span class="c1"></span>        <span class="nx">array_push</span><span class="p">(</span><span class="nv">$category</span><span class="p">,</span> <span class="nv">$item_category</span><span class="p">);</span>
        <span class="nv">$result1</span><span class="o">-&gt;</span><span class="na">close</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="nv">$mysqli</span><span class="o">-&gt;</span><span class="na">close</span><span class="p">();</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;forum&#34;</span><span class="p">,</span> <span class="nv">$category</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s2">&#34;test3.htm&#34;</span><span class="p">);</span>
</code></pre></div><p>在資料庫抓取一筆資料後，我們得到的是一個包含該筆資料的陣列。透過 <code>while</code> 敘述及 <code>array_push</code> 函式，我們將資料庫中的資料一筆一筆塞到陣列裡。如果您只用到單層迴圈，就把第二層迴圈 (紅色的部份) 去掉即可。</p>
<h3 id="決定內容是否顯示">決定內容是否顯示</h3>
<p>要決定是否顯示內容，我們可以使用 <code>if</code> 這個語法來做選擇。例如如果使用者已經登入的話，我們的樣版就可以這樣寫：</p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php">{if $is_login == true}
顯示使用者操作選單
{else}
顯示輸入帳號和密碼的表單
{/if}
</code></pre></div><p><code>if</code> 語法一般的應用可以參照官方使用說明，所以筆者在這裡就不詳加介紹了。不過筆者發現了一個有趣的應用：常常會看到程式裡要產生這樣的一個表格： (數字代表的是資料集的順序)</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">table</span>
        <span class="na">summary</span><span class="o">=</span><span class="s">&#34;橫向重覆表格&#34;</span>
        <span class="na">width</span><span class="o">=</span><span class="s">&#34;100&#34;</span>
        <span class="na">border</span><span class="o">=</span><span class="s">&#34;1&#34;</span>
        <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span>
        <span class="na">cellpadding</span><span class="o">=</span><span class="s">&#34;3&#34;</span>
        <span class="na">cellspacing</span><span class="o">=</span><span class="s">&#34;0&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>1<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>2<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>3<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>4<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>5<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>6<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">tr</span> <span class="na">align</span><span class="o">=</span><span class="s">&#34;center&#34;</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>7<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span>8<span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
</code></pre></div><p>這個筆者稱之為「橫向重覆表格」。它的特色和傳統的縱向重覆不同，前幾節我們看到的重覆表格都是從上而下，一列只有一筆資料。而橫向重覆表格則可以橫向地在一列中產生 n 筆資料後，再換下一列，直到整個迴圈結束。要達到這樣的功能，最簡單的方式只需要 <code>section</code> 和 <code>if</code> 搭配即可。</p>
<p>我們來看看下面這個例子：</p>
<p><strong>test4.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require</span> <span class="s2">&#34;main.php&#34;</span><span class="p">;</span>
<span class="nv">$my_array</span> <span class="o">=</span> <span class="k">array</span><span class="p">(</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;0&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;1&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;2&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;3&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;4&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;5&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;6&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;7&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;8&#34;</span><span class="p">),</span>
    <span class="k">array</span><span class="p">(</span><span class="s2">&#34;value&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;9&#34;</span><span class="p">)</span>
<span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;my_array&#34;</span><span class="p">,</span> <span class="nv">$my_array</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s1">&#39;test4.htm&#39;</span><span class="p">);</span>

</code></pre></div><p>樣版的寫法如下：</p>
<p><strong>templates/test4.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>橫向重覆表格測試<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">table</span> <span class="na">width</span><span class="o">=</span><span class="s">&#34;500&#34;</span> <span class="na">border</span><span class="o">=</span><span class="s">&#34;1&#34;</span> <span class="na">cellspacing</span><span class="o">=</span><span class="s">&#34;0&#34;</span> <span class="na">cellpadding</span><span class="o">=</span><span class="s">&#34;3&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
{section name=sec1 loop=$my_array}
<span class="p">&lt;</span><span class="nt">td</span><span class="p">&gt;</span> {$my_array[sec1].value} <span class="p">&lt;/</span><span class="nt">td</span><span class="p">&gt;</span>
{if $smarty.section.sec1.rownum is div by 2
<span class="err">&amp;&amp;</span> $smarty.section.sec1.rownum <span class="err">&lt;</span> $smarty.section.sec1.total}
<span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">tr</span><span class="p">&gt;</span>
{/if}
{/section}
<span class="p">&lt;/</span><span class="nt">tr</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">table</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div><p>重點在於 <code>$smarty.section.sec1.rownum</code> 及 <code>$smarty.section.sec1.total</code> 這兩個 Smarty 變數，在 <code>section</code> 迴圈中這個 <code>rownum</code> 變數會取得從 1 開始的 索引值，所以當 <code>rownum</code> 能被 2 除盡時，就輸出 <strong><code>&lt;/tr&gt;&lt;tr&gt;</code></strong> 使表格換列 (注意！是 <strong><code>&lt;/tr&gt;</code></strong> 在前面 <strong><code>&lt;tr&gt;</code></strong> 在後面) ，因此數字 2 就是我們在一列中想要呈現的資料筆數。而 <code>total</code> 這個變數會回傳資料總筆數，所以讓 <code>rownum</code> 在小於 <code>total</code> 這個變數才做輸出 <code>&lt;/tr&gt;&lt;tr&gt;</code> 的動作，會確保不會出現最後一個空的 <code>&lt;tr&gt;&lt;/tr&gt;</code> 。</p>
<p>各位可以由此去變化其他不同的呈現方式。</p>
<h3 id="載入外部內容">載入外部內容</h3>
<p>我們可以在樣版內載入 PHP 程式碼或是另一個子樣版，分別是使用 <code>include_php</code> 及 <code>include</code> 這兩個 Smarty 樣版語法； <code>include_php</code> 筆者較少用，使用方式可以查詢官方手冊，這裡不再敘述。</p>
<p>在使用 <code>include</code> 時，我們可以預先載入子樣版，或是動態載入子樣版。預先載入通常使用在有共同的文件標頭及版權宣告；而動態載入則可以用在統一的框架頁，而進一步達到如 Winamp 般可換 Skin 。當然這兩種我們也可以混用，視狀況而定。</p>
<p>我們來看看下面這個例子：</p>
<p><strong>test5.php</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="cp">&lt;?php</span>
<span class="k">require</span> <span class="s2">&#34;main.php&#34;</span><span class="p">;</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;title&#34;</span><span class="p">,</span> <span class="s2">&#34;Include 測試&#34;</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;content&#34;</span><span class="p">,</span> <span class="s2">&#34;這是樣版 2 中的變數&#34;</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">assign</span><span class="p">(</span><span class="s2">&#34;dyn_page&#34;</span><span class="p">,</span> <span class="s2">&#34;test5_3.htm&#34;</span><span class="p">);</span>
<span class="nv">$tpl</span><span class="o">-&gt;</span><span class="na">display</span><span class="p">(</span><span class="s1">&#39;test5_1.htm&#39;</span><span class="p">);</span>
</code></pre></div><p>樣版 1 的寫法如下：</p>
<p><strong>templates/test5_1.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">http-equiv</span><span class="o">=</span><span class="s">&#34;Content-Type&#34;</span> <span class="na">content</span><span class="o">=</span><span class="s">&#34;text/html; charset=utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span> {$title} <span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
{include file=&#34;test5_2.htm&#34;}
{include file=$dyn_page}
{include file=&#34;test5_4.htm&#34; custom_var=&#34;自訂變數的內容&#34;}
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</code></pre></div><p>樣版 2 的寫法如下：</p>
<p><strong>templates/test5_2.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>{$content}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</code></pre></div><p>樣版 3 的寫法如下：</p>
<p><strong>templates/test5_3.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>這是樣版 3 的內容<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</code></pre></div><p>樣版 4 的寫法如下：</p>
<p><strong>templates/test5_4.htm</strong></p>
<div class="highlight"><pre class="chroma"><code class="language-php" data-lang="php"><span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>{$custom_var}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</code></pre></div><p>這裡注意幾個重點：</p>
<ul>
<li>樣版的位置都是以先前定義的 <code>template_dir</code> 為基準。</li>
<li>所有 <code>include</code> 進來的子樣版中，其變數也會被解譯。</li>
<li><code>include</code> 中可以用 <code>變數名稱=變數內容</code> 來指定引含進來的樣版中所包含的變數，如同上面樣版 4 的做法。</li>
</ul>
<h2 id="範例下載">範例下載</h2>
<p><a href="/resources/smarty_basic/example.zip">下載</a>本文所有範例。</p>
<p>而其他 Smarty 更進階的部份，就請看筆者的拙作：「 <a href="http://www.flag.com.tw/book/5105.asp?bokno=F5471">PHP Smarty 樣版引擎</a>」。</p>
]]></content>
		</item>
		
		<item>
			<title>簡單測試 PHP 執行的效能</title>
			<link>https://jaceju.net/php-profiling-with-xdebug/</link>
			<pubDate>Mon, 30 May 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/php-profiling-with-xdebug/</guid>
			<description>有時候執行某些 PHP 網頁時，都會覺得很慢，但又不曉得問題出在哪？以下介紹一個好工具，可以讓我們很容易知道我們寫的 PHP 執行瓶頸在哪裡。 Xdebug 是 PHP 上的一個</description>
			<content type="html"><![CDATA[<p>有時候執行某些 PHP 網頁時，都會覺得很慢，但又不曉得問題出在哪？以下介紹一個好工具，可以讓我們很容易知道我們寫的 PHP 執行瓶頸在哪裡。</p>
<p><a href="http://www.xdebug.org/">Xdebug</a> 是 PHP 上的一個擴充模組，它可以協助我們追蹤程式上的偵錯訊息、程式執行了哪些區段，以及其他許多有用的資訊。不過這裡我們僅會用到檢視執行效能上的功能。我示範的平台是以 Windows + Apache 2.0 為主，PHP 版本為 5.0.4 。</p>
<!-- raw HTML omitted -->
<h2 id="安裝">安裝</h2>
<p>首先我們到 <a href="http://www.xdebug.org/download.php">Xdebug 官方網站</a> 下載最新的 Xdebug 2.1.0 (請對應你的 PHP 版本)，並將它複製到 PHP 安裝目錄 的 <code>ext</code> 子資料夾裡。</p>
<p>**補充：**請記得設定 <code>php.ini</code> 的 <code>extension_dir=&quot;c:/php5/ext/&quot;</code> ；或是把 Windows 的 PATH 系統變數後面加上 <code>;c:\php5\ext</code> ，然後重新啟動電腦。</p>
<p>**補充：**有時候 Windows 還是不能找到 <code>ext</code> 底下的 dll 檔 (例如 PHP 5.1 就會如此) ，這時候只要把 <code>c:\php5\ext;</code> 改加在 PATH 變數之前就行了。例如：</p>
<pre><code>c:\php5;c:\php5\ext;%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem

</code></pre><p>接著在 PHP.INI 中加入：</p>
<div class="highlight"><pre class="chroma"><code class="language-ini" data-lang="ini"><span class="na">[Xdebug] zend_extension_ts</span><span class="o">=</span><span class="s">php_xdebug.dll</span>
<span class="na">xdebug.profiler_enable</span><span class="o">=</span><span class="s">on</span>
<span class="na">xdebug.trace_output_dir</span><span class="o">=</span><span class="s">“C:/TEMP/php/xdebug”</span>
<span class="na">xdebug.profiler_output_dir</span><span class="o">=</span><span class="s">“C:/TEMP/php/xdebug” ```</span>

</code></pre></div><p>記得重新啟動 Apache ，否則 Xdebug 是不會動作的。</p>
<p>檢視 <code>phpinfo()</code> ：</p>
<p><img src="/resources/xdebug_profile/01.gif" alt=""></p>
<p>出現 Xdebug 的話，就算完成了。</p>
<h2 id="檢視執行效能">檢視執行效能</h2>
<p>接下來，我們任意執行一個 PHP 程式，讓 Xdebug 對它做側寫 (profiling) 。然後我們到 <code>C:\TEMP\php\xdebug\</code> 這個資料夾下，就會看到數支 <code>cachegrind.out.xxxxxxx</code> 檔。用文字編輯器開啟它，裡面就是我們剛剛執行的 PHP 檔所產生的相關偵錯資訊。</p>
<p>不過這樣實在是不容易看懂它，我們可以借重這個工具： <a href="http://sourceforge.net/projects/wincachegrind">WinCacheGrind</a> 來將這些文字檔作較好格式的輸出。一樣下載 WinCacheGrind 回來安裝，而安裝步驟很簡單，就是一直按「下一步」就可以安裝完成，這裡我就不多加描述。</p>
<p>安裝好後，開啟 WinCacheGrind ，我們可以看到以下畫面：</p>
<p><img src="/resources/xdebug_profile/02.gif" alt=""></p>
<p>接著選擇 <code>Tools / Options…</code> ，在 <code>Main</code> 頁籤中的 <code>Working folder</code> 選擇 <code>C:\Temp\php\xdebug</code> 後，按下 <code>OK</code> 。</p>
<p><img src="/resources/xdebug_profile/03.gif" alt=""></p>
<p>點兩下想查看的 <code>cachegrint.out</code> ，就會顯示出這支程式所執行過的函式、物件、以及它們的執行秒數。</p>
<p><img src="/resources/xdebug_profile/05.gif" alt=""></p>
<p>如果大家有發現什麼 Xdebug 有什麼更棒的功能，歡迎分享。</p>
]]></content>
		</item>
		
		<item>
			<title>CSS 排版觀念：Float</title>
			<link>https://jaceju.net/css-float/</link>
			<pubDate>Fri, 13 May 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/css-float/</guid>
			<description>如果說浮動 (float) 是 CSS 排版的重要技巧之一，實在一點也不為過；很多著名的 CSS 版型都會用到浮動技巧。本文就來介紹浮動所需要注意的地方，以及可能會碰到的問</description>
			<content type="html"><![CDATA[<p>如果說浮動 (float) 是 CSS 排版的重要技巧之一，實在一點也不為過；很多著名的 CSS 版型都會用到浮動技巧。本文就來介紹浮動所需要注意的地方，以及可能會碰到的問題。</p>
<!-- raw HTML omitted -->
<p>浮動是設定元素的 <code>float</code> 屬性，我們能設定向左 (<code>left</code>) 或向右 (<code>right</code>) 浮動。浮動基本上會使得元素在有足夠的空間時，往父元素的左邊或右邊靠緊。接著原本跟在這個元素後面的其他元素，就會自動往上跑。 (不過這裡會有部份要考量的地方，稍後再談。)</p>
<p>當元素被設定浮動時，會自動變成區塊顯示元素 (<code>display: block</code>) ，這時候我們就可以設定元素的 <code>width</code> 和 <code>height</code> 了。</p>
<p>不過要注意一點：當我們把 <code>position</code> 設為 <code>absolute</code> 時，浮動會失效。</p>
<p>浮動會因為元素先後順序而有所影響，例如我們有 A 、 B 兩個區塊顯示元素如下圖，其中虛線部份的內部為父元素的內容區：</p>
<p><img src="/resources/css_float/float1.png" alt=""></p>
<p>如果我們把 A 元素設為向右浮動，那麼 B 元素就會自動往上跑，如下圖：</p>
<p><img src="/resources/css_float/float2.png" alt=""></p>
<p>而把 B 元素向右浮動的話， A 元素並不會受到干擾， B 元素也不會往父元素的上邊靠，如下圖：</p>
<p><img src="/resources/css_float/float3.png" alt=""></p>
<p>那如果不往右浮動，而是向左浮動呢？那就非常複雜了。為什麼呢？因為各家瀏覽器的實作不同！如果我們試著把 A 元素向左浮動，就會發現 IE 會讓 B 元素跑到 A 元素的右邊；可是在 Firefox 和 Opera 上，雖然 A 元素會靠到父元素內容區的左邊，但是則會讓 B 的 Content 區被擠到 A 元素的下方 (詭異的是 B 元素的背景區卻靠向父元素內容區的上方) 。所以在排版時，要特別注意這種情況。</p>
<p>如果我們不希望在緊跟在 A 元素及 B 元素後面的元素被浮動所影響，那麼就該對此元素設定 <code>clear: both;</code> 。</p>
<p>另外元素的 Box Model 也會影響元素的浮動，我在 <a href="/blog/archives/17">CSS 排版觀念：Box Model</a> 裡提到了一些要點，這裡再說明一下。</p>
<p>Box Model 正確的寬度，通常就是影響浮動是否能正常的關鍵。如下圖，是我們常見的置中兩欄式 float 排版。 Sidebar 往左浮動， Content 則是往右浮動。</p>
<p><img src="/resources/css_float/float4.png" alt=""></p>
<p>在浮動 Sidebar 時，如果沒有考慮好 Content 的 <code>margin</code> 和 <code>padding</code> ，或是 Content 裡的內容過長無法折行時， Sidebar 的部份就會整個往下掉，如下圖。</p>
<p><img src="/resources/css_float/float5.png" alt=""></p>
<p>因此在利用浮動製作版面時，一定也要記得和 Box Model 相互搭配，這樣才能夠製作正確而實用的版型。</p>
<p>總而言之，浮動技巧是 CSS 排版中非常重要的一門學問。下次我們就來正式挑戰一些常見的版型，不但告訴你怎麼做，還告訴你為什麼要這樣做。</p>
]]></content>
		</item>
		
		<item>
			<title>CSS 排版觀念：Box Model</title>
			<link>https://jaceju.net/css-box-model/</link>
			<pubDate>Mon, 09 May 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/css-box-model/</guid>
			<description>CSS 排版有一個很重要的觀念： Box Model 。它描述了元素之間的彼鄰關係，同時也左右了我們是否能夠成功透過 CSS ，完成整個頁面的呈現。 Box Model 的意思是說，每一個元</description>
			<content type="html"><![CDATA[<p>CSS 排版有一個很重要的觀念： Box Model 。它描述了元素之間的彼鄰關係，同時也左右了我們是否能夠成功透過 CSS ，完成整個頁面的呈現。</p>
<!-- raw HTML omitted -->
<p>Box Model 的意思是說，每一個元素我們都可視它為一個 Box 。一個 Box 由以下屬性組成： <code>margin</code> 、 <code>padding</code> 、 <code>border</code> 、 <code>content</code> 。</p>
<p>註： <code>content</code> 非真的可用的 CSS 屬性，這裡只是為了方便說明。</p>
<p>而一個 Box 的實際寬度 (高度) 是由 <code>padding</code> + <code>border</code> + <code>width</code> (<code>height</code>) 所組成，如下圖 (取自 <a href="http://msdn.microsoft.com/library/default.asp?url=https://jaceju.net/library/en-us/dnie60/html/cssenhancements.asp">MSDN</a>)：</p>
<p><img src="/resources/css_boxmodel/boxdim.gif" alt="IE Box Model"></p>
<p>所以一般我們指定的 <code>width</code> 和 <code>height</code> 是 content 的寬和高，而沒有包含 <code>border</code> 和 <code>padding</code> 。換句話說，一個元素真正佔用的視覺空間，應該是 content + <code>padding</code> + <code>border</code> ，這是標準的 CSS 規範。</p>
<p>不過在 IE5/5.5 時代，一個元素的寬高則包含了 content + padding + border 。這個錯誤的實作，造成現今許多 CSS 排版上的困擾，但是也不是沒不是沒有辦法解決。 <a href="http://tantek.com/CSS/Examples/boxmodelhack.html">Box Model Hack</a> 提供了解決之道，重點在於利用 IE5/5.5 對 CSS 解讀上的 Bug ，讓我們所希望之元素正確的寬高能正確地在 IE5/5.5 顯示出來。</p>
<p>對於 <code>absolute</code> 和 <code>fixed</code> 而言，錯誤的 Box Model 或許影響較小 (不過也絕對不是沒有影響，像是如果要正確控制圖層的寬度時)；但對 <code>relative</code> 和 <code>static</code> 來說，因為它們都還是會保留其所佔有的空間。因此如何正確地調整 content 的大小，就會影響到我們的排版。</p>
<p>以下我們來看看 Box Model 的各個組成分子。</p>
<p>請特別注意：我在以下圖示中，元素上色的部份，除了有特別說明外，都是包含 border + padding + content ，這點非常重要！因為除了 <code>body</code> 標籤外，元素的 <code>background</code> 屬性的作用都不會包含 <code>margin</code> 。</p>
<h2 id="border">border</h2>
<p><code>border</code> 是一個「加上去」的屬性，換句話說，一般都是用來區隔元素與元素用的。 <code>border</code> 的外圍即為元素的最外圍，因此計算元素實際的寬高時，就一定要將 <code>border</code> 納入。換句話說， <code>border</code> 會佔有空間，所以在計算精細的版面時，一定要將 <code>border</code> 的影響考慮進去。</p>
<p>如下圖，黑色虛線部份即為 <code>border</code> ：</p>
<p><img src="/resources/css_boxmodel/border1.png" alt="border"></p>
<p>還有一點要特別注意，如果我們在元素上設定背景色時， IE 是作用在 padding + content ，而 Firefox 則是作用在 border + padding + content 上。</p>
<h2 id="padding">padding</h2>
<p><code>padding</code> 會在元素內容的周圍加上我們所指定大小的空間；而如果我們沒有指定元素的寬高時，那麼該元素的內容就會受到 <code>padding</code> 所擠壓。如下圖：</p>
<p><img src="/resources/css_boxmodel/padding1.png" alt="padding"></p>
<p>如果元素的內容中有行內顯示元素時，我們可以利用 padding 的設定來讓它們在我們想要的地方折行，而不用對 content 指定寬度；這樣的技巧我用在全版面的兩欄式版面上，使得我不用對難用的 <code>width</code> 屬性傷腦筋。</p>
<p>其實 <code>padding</code> 就這麼簡單，不過可別小看它，在 CSS 排版裡， <code>padding</code> 加上 <code>margin</code> 的設定，就能夠使版面千變萬化。</p>
<h2 id="margin">margin</h2>
<p><code>margin</code> 的意義就是該元素與其他元素間的邊界距離，它的應用大概也算是 CSS 排版很重要的技術之一。所以我打算多花一點時間解釋它。</p>
<p>我們可以分以下兩種狀況解釋：「元素與相鄰元素的距離」及「元素與父元素間的距離」。</p>
<p>「元素與相鄰元素的距離」指得是元素間是緊鄰的 (不論是區塊顯示元素或行內顯示元素) ，而沒有階層關係。例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;i1&#34;</span><span class="p">&gt;</span>行內顯示元素1<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;</span><span class="nt">span</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;i2&#34;</span><span class="p">&gt;</span>行內顯示元素2<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
</code></pre></div><p>這兩個 <code>span</code> 標籤就是緊鄰關係。而 <code>span</code> 標籤預設屬於行內顯示元素 (<code>display: inline</code>) ，因此它們的邊界距離就是 <code>i1</code> 的 <code>margin-right</code> 加上 <code>i2</code> 的 <code>margin-left</code> ，如下圖。</p>
<p><img src="/resources/css_boxmodel/margin1.png" alt="行內 margin"></p>
<p>另一種緊鄰關係是區塊顯示元素，例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;b1&#34;</span><span class="p">&gt;</span>區塊顯示元素1<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;b2&#34;</span><span class="p">&gt;</span>區塊顯示元素2<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p><code>div</code> 標籤預設屬於區塊顯示元素，也就是在它的前後會加入換行的控制。要注意的是，區塊顯示元素它們的邊界距離是重疊的！而當 <code>b1</code> 的 <code>margin-bottom</code> 大於 <code>b2</code> 的 <code>margin-top</code> 時， <code>b1</code> 和 <code>b2</code> 實際的距離就以 <code>b1</code> 的 <code>margin-bottom</code> 為準，如下圖。</p>
<p><img src="/resources/css_boxmodel/margin2.png" alt="區塊 margin"></p>
<p>還有一種緊鄰關係是浮動元素，基本上它會是個區塊顯示元素，但 <code>margin</code> 的呈現關係和行內顯示元素是很像的，這我會在介紹浮動元素時再加以說明。</p>
<p>「元素與父元素間的距離」就是指元素之間有階層關係時的邊界距離。例如：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;b3&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;b4&#34;</span><span class="p">&gt;</span>內部區塊<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p>其中 b3 就是父元素， b4 則是子元素。 它們的邊界關係如下圖：</p>
<p><img src="/resources/css_boxmodel/margin3.png" alt="父子元素 margin"></p>
<p>我們可以看到，子元素的邊界起始會以父元素的 Content 區為基準。</p>
<p>上面我們都是將 margin 設為正值，例如將元素的 <code>margin-top</code> 設為 <code>20px</code> ，那麼元素上面就會多出 <code>20px</code> 的空間。注意，我是說多出空間，而非向下移動！有什麼差別呢？</p>
<p>向下移動的定義是我們讓元素成為區塊顯示 (<code>display: block</code>) 或是它原本就是區塊顯示元素，然後指定它的 <code>position</code> 屬性為 <code>relative</code> ，最後設定它的 <code>top</code> 屬性為正值。</p>
<p>而多出空間則不論它的 <code>position</code> 屬性設定為何，硬是擠進我們指定的空間。而且設定 <code>margin</code> 之後，頁面內容超過螢幕顯示範圍，即時有捲軸也無法呈現完整的內容。</p>
<p>不過 IE6 和 FireFox 兩者對 Box Model 在 margin 的實作又有點不太一樣。 IE6 就算指定了父元素的 <code>height</code> 屬性，如果子元素的高度超過父元素的 <code>height</code> ，父元素就會被撐大，然後保留子元素 <code>margin-bottom</code> 的空間；而 FireFox 就不會。哪個實作是對的，我也不太清楚。</p>
<p>margin 也可以指定為負值，例如我在這篇「<a href="/blog/archives/9">如何正確實作出固定寬度且置中的版型？</a>」裡，運用到了將 <code>margin-left</code> 設定為負值的技巧。這裡我再加以說明，將 <code>margin</code> 設定為負值是怎麼一回事。</p>
<p>在「元素與元素間緊鄰」時，我們將 <code>margin</code> 設定為負值，會使得 <code>margin</code> 設定為負值的元素「疊」到另一個元素上 (不過還是要視另一個元素所設定的邊界距離而定) 。例如，我們將 A 區塊的 <code>margin-bottom</code> 設為 0 ， B 區塊的 <code>margin-top</code> 設為 -10px ，那麼 B 區塊的文字就會疊到 A 區塊的文字上。</p>
<p>「元素間有階層性關係」時的關係，如果子元素的所指定的 margin 負值的絕對值大於父元素的 border + padding 時，就會使得子元素跑到父元素的外部去了。如圖，我們指定藍色元素的 <code>margin-left</code> 為 <code>-100px</code> ，那麼該元素就會往左跑 <code>100px</code> ；這時如果其父元素 (淺黃色) 的 <code>border-left</code> 和 <code>padding-left</code> 相加小於 <code>100px</code> ，我們就會看到該元素就會突出於父元素的左邊。</p>
<p><img src="/resources/css_boxmodel/margin4.png" alt="negative margin"></p>
<p>總而言之，將 <code>margin</code> 指定為負值在 CSS 排版上有非常大的用處，如果能將它了然於胸，相信你在 CSS 排版的功力一定會大增的。</p>
<p>這篇我僅描述了 CSS Box Model 的一些基本觀念，雖然不是非常深入，但希望能夠讓大家對它們有基本的認知。而 CSS 排版除了 <a href="/blog/archives/15">Position</a> 和 Box Model 外，還有一樣非常值得探討的技術，就是浮動 (float) ；我將會在下次的文章中談到它。</p>
]]></content>
		</item>
		
		<item>
			<title>CSS 排版觀念：Position</title>
			<link>https://jaceju.net/css-position/</link>
			<pubDate>Fri, 29 Apr 2005 00:00:00 +0800</pubDate>
			
			<guid>https://jaceju.net/css-position/</guid>
			<description>很多人都會用圖層來製作網頁，或許常會聽到所謂的絕對位置和相對位置。其實它們都是 CSS 中 position 的設定值，透過設定 position ，便能讓我們隨意移動元素的位置。 不過</description>
			<content type="html"><![CDATA[<p>很多人都會用圖層來製作網頁，或許常會聽到所謂的絕對位置和相對位置。其實它們都是 CSS 中 <code>position</code> 的設定值，透過設定 <code>position</code> ，便能讓我們隨意移動元素的位置。</p>
<p>不過它們之間到底有什麼不同呢？本文做個簡單的說明。</p>
<!-- raw HTML omitted -->
<h2 id="參數說明">參數說明</h2>
<p>首先我把其中的關係整理成表：</p>
<!-- raw HTML omitted -->
<p>當我們對元素的 <code>position</code> 屬性，指定了 <code>absolute</code> 、 <code>relative</code> 或 <code>fixed</code> 後，這個元素就可以移動了。我們可以用 <code>top</code>, <code>left</code>, <code>right</code>, <code>bottom</code> 這四種屬性來指定元素要呈現的位置。</p>
<p>註：由於 IE 不支援 <code>position: fixed</code> ，使得固定位置這個好用的技巧一直不受大家的重視。但在這裡我還是提一下。你可以使用 FireFox 來感受一下固定位置的強大威力，或是等待新版的 IE 支援。</p>
<p>接下來我們來解釋上面的表列裡，每個參數說明的意義。</p>
<h2 id="畫面位置參考基準">畫面位置參考基準</h2>
<p>以絕對位置 (<code>position: absolute</code>) 而言，故名思義，它是以父元素的邊界為絕對起點。例如如果我們設定 <code>top: 50px</code> ，那麼這個元素就會在距離父元素內容區上邊界 50px 的地方呈現，如下圖：</p>
<p><img src="/resources/css_position/absolute_1.png" alt="position: absolute"></p>
<p>__補充：__如果父元素的 <code>position</code> 不是 <code>absolute</code> 或 <code>relative</code> 時，那麼元素的位置就會再對應到父元素的上層元素；如果其親代元素的 <code>position</code> 都沒有設定 <code>absolute</code> 或 <code>relative</code> 時，就以視窗最大可視範圍邊界為基準。</p>
<p>而以相關位置 (<code>position: relative</code>) 而言，其意義就是相對於原本的位置。例如我們指定 top: 50px 時，那麼這個元素就會從原本應該呈現的位置往下移動 50px 。如下圖，紅色虛線部份就是未設定 position: relative 前，元素原該應該在的位置：</p>
<p><img src="/resources/css_position/relative_1.png" alt="position: relative"></p>
<p>而固定位置 (position: fixed) 指的就是固定在視窗最大可視範圍上，如果不指定位置 (top, left, right, bottom) 時，那元素就會固定在原本的位置；而指定位置後，就會以視窗最大可視範圍的邊界為絕對基準點。如果頁面內容超過視窗最大可視範圍大小時，那麼不論我們如何捲動頁面，元素都會固定在視窗最大可視範圍上我們所指定的位置。</p>
<h2 id="移動參考基準">移動參考基準</h2>
<p>當頁面可以捲動的時候， <code>absolute</code> 、 <code>relative</code> 、 <code>static</code> 都會跟著移動。只有 <code>fixed</code> 會固定在視窗最大可視範圍上，不會跟著移動。</p>
<h2 id="可改變顯示位置">可改變顯示位置</h2>
<p>就是我們可以透過指定元素的 <code>top</code> 、 <code>left</code> 、 <code>right</code> 、 <code>bottom</code> 四個屬性，使元素改變顯示位置。如果元素是 <code>position: static</code> 時，會自動忽略所設定的 <code>top</code> 、 <code>left</code> 、 <code>right</code> 、 <code>bottom</code> 。</p>
<h2 id="可調整大小">可調整大小</h2>
<p>我們可以透過 <code>width</code> 、 <code>height</code> 來調整元素內容區的大小，不過當 <code>position</code> 是 <code>relative</code> 或是 <code>static</code> 時，元素的 <code>display</code> 屬性必須為 <code>block</code> 才可調整其大小。</p>
<h2 id="從顯示流程中去除">從顯示流程中去除</h2>
<p>顯示流程的意義就是頁面上的每一個元素的呈現，換句話說，就是該元素會出現的位置，及其佔用的空間等等。</p>
<p>我們可以將原來的頁面想成是一個圖層，每個元素都是一個一個緊接在前一個元素後面。如下圖，在尚未指定 <code>position</code> 時，粉紫色區塊會緊接在淺藍色區塊後。</p>
<p><img src="/resources/css_position/layer1.png" alt="從顯示流程中去除_1"></p>
<p>請注意，我在這裡提到的圖層，指的是瀏覽器去解譯 HTML 後，將元素呈現出來的圖層，而非一般我們所認為，以絕對位置呈現的圖層；你可以把它想像成是 Photoshop 裡的圖層。</p>
<p>當我們指定淺藍色區塊的 <code>position</code> 屬性為 <code>absolute</code> 或 <code>fixed</code> 後，淺藍色區塊就會跑到另一個圖層；而粉紫色區塊因為淺藍色區塊已經從原圖層的顯示流程中去除了，所以它就自動往上跑。如下圖，紅色虛線就是粉紫色區塊原本的位置。</p>
<p><img src="/resources/css_position/layer2.png" alt="從顯示流程中去除_2"></p>
<p>而元素如果指定為 <code>relative</code> 時，雖然也能移動，但原本的頁面圖層還是會保留該元素所佔有的空間。</p>
<h2 id="後記">後記</h2>
<p>或許你在看完這篇文章之後，還是無法很清楚地了解 <code>position</code> 屬性的運作方式。建議你打開你的瀏覽器 (最好是用 Firefox) ，再用你慣用的 HTML 編輯器去試試它們之間的差異。然後回來看看這篇文章，你也許就能明白我的意思。</p>
<p>__補充：__如果頁面在框架裡時 (<code>frame</code> 或 <code>iframe</code>) ，所有參照視窗最大可視範圍邊界的元素就會改為參照框架邊界。</p>
<h2 id="範例">範例</h2>
<p>以下這個範例，你可以看到設定 position 前及設定 position 後的關係：</p>
<!-- raw HTML omitted -->
]]></content>
		</item>
		
	</channel>
</rss>
