Benyi Hsia

我是Benyi,這裡存放關於資訊科技的文章

[物件導向] 何謂介面(interface)?

| Comments

什麼是介面

現實生活中,介面 指的是的一個「統一的」、「標準的」規格,對於產品,或是物品進行使用上的規範等。

例如 USB 就是一個介面,無論這項電子產品(假設是隨身碟)內部怎麼設計、電路怎麼接、晶片中的資料怎麼存取最後只要把接頭做成 USB 的長方型樣子,遵照它的接腳標準,就可以插進電腦中進行使用。

對於電腦使用者來說,這個隨身碟怎麼設計的不用管,未來我想要換其他的產品使用,例如把 隨身碟 換成 光碟機 來使用那麼我只要把隨身碟從 USB 洞口裡面拔出來,插上光碟機的 USB 線就可以使用這台光碟機。

換個例子,以生活中的電源插座為例,今天我想要使用吹風機時,只要把電線插進牆上的兩孔插座,就可以使用換成吸塵器?沒問題!只要插座是兩個直線的形狀(台灣規格),就可以插進牆上使用。

你發現了嗎?不論我們用什麼電子產品,只要符合插頭形狀的,就可以互相替換使用。

這就是定義 介面 帶給我們的好處。

如下表所示

未來我們開發了新的產品,只要符合 USB 的標準,電腦就可以使用這項產品。

如果沒有介面

假設電腦中不存在 USB 的這個介面,那麼會有什麼問題呢?

電腦主機上可能會有很多的插孔,像是滑鼠插孔、光碟機插孔、鍵盤插孔、隨身碟插孔... 等等。而如果今天出現了一個新產品,但是電腦上沒有對應到他的插孔,想要使用這個新產品,你有兩個選擇:

  1. 硬生生的把電腦拆開,在已經設計好的主機板想辦法找兩個接點出來,然後在機殼上鑿個洞,把新的插孔裝上去。但是下次又有新產品出現的時候,你就要再鑿一次洞。
  2. 買一個新的,有此插孔的電腦,但是下次要用新產品的時候,你就得再買一次新電腦。

上面兩個方法,不管哪一個都是天馬行空,就算真的選了一個方法,又能重複幾次呢?(機殼又能禁得起再挖幾次洞呢?)

這時候,我們就需要一個統一的標準、統一的產品銜接方法
這個就叫做「介面」(interface)。

介面與實作(implementation)

介面可以讓我們在設計程式的時候,將該有的實作方法定義好,也就是只描述一個抽象的方法名稱。換句話說,每個產品都有每個產品各自的特性和他們的特色,但是只要實作這些方法的裝置,都可以叫作 USB 裝置。所以,支援 USB 的電腦,無論換了什麼東西,只要插得進 USB 插座的,都可以使用。(電腦依賴在 USB 的介面(interface)上,而不是依賴在 USB 滑鼠、USB 鍵盤… 等裝置上。)

USB 這個介面中,可能會有這些方法

  • boot() 裝置開機
  • isConnected() 裝置是否有連線
  • getErrorMessage() 裝置連線不成功的錯誤訊息
  • getPower() 裝置取得電源
  • getData() 裝置取得需要使用到的資料
  • saveData() 裝置把資料儲存起來
  • shutdown() 裝置關機

當定義好 介面(interface)之後,接下來我們要做的,就是在每個我們生產的裝置上實作這些方法。反過來說,只要實作這些方法的裝置,都可以稱作 USB 介面

抽象化(abstraction)與物件導向

由於電腦認得 USB 介面,未來只要有新的裝置插上電腦的時候,可能會先呼叫isConnected() 方法檢查是否有接上成功,直接呼叫每個裝置的 getPower() 方法取得電源,接著呼叫 boot() 方法讓它們開機等等,等到使用完畢之後,最後呼叫 shutdown() 方法讓這個裝置關機。

發現了嗎?在我所描述的上面這個過程中,有沒有提到「是什麼裝置」?有沒有提到這個裝置的 getPower()(取得電源)方法應該怎麼做? 沒有。這個就是我們利用介面把程式進行抽象化(abstraction)

抽象化指的是「只描述一個大概的流程跟邏輯,真正實現的方法交由底下各個裝置去做」

設計程式的時候,如果這個世界上只有滑鼠會接上電腦使用,那我們只要在電腦的邏輯中寫下處理「滑鼠」的程式就好了。

但事實不是,且未來也還會有更多的裝置被創造、發明出來。如果一開始就將處理「滑鼠」的邏輯寫在電腦的類別裡,會發生什麼事呢? 未來有新的裝置加入,我們就必須回去改這個程式,而且加的愈多,改的愈多。

正確的作法,應該是讓電腦依賴在 USB 這個 介面(interface)上,而不是單一的裝置,接著,在控制邏輯中呼叫他們各別的方法即可。

步驟1:先定義一個 USB 的介面 USBInterface

interface USBInterface
{

    /** 裝置開機 */
    public function boot();
    
    /** 裝置是否有連線 */
    public function isConnected();
    
    /** 裝置連線不成功的錯誤訊息 */
    public function getErrorMessage();
    
    /** 裝置取得電源 */
    public function getPower();

    /** 裝置取得需要使用到的資料 */
    public function getData();

    /** 裝置把資料儲存起來 */
    public function saveData();
    
    /** 裝置關機 */
    public function shutdown();
}

注意到了嗎? 雖然定義了一個 USB 裝置該有哪些方法,但並沒有寫任何的程式碼,也就是不包含實作(這樣才可以分離抽象的邏輯,而不是綁死在某一個裝置上)

步驟2:實作各個裝置來自介面的方法

class Mouse implements USBInterface
{
    /** 實作 USB 滑鼠的開啟方法 */
    public function boot()
    {
        if ( $this->isBoot() ) {
            return "滑鼠開啟成功";
        } else {
            $this->bootRetry();
        }
    }
    
    /** 實作其他方法 (略) */
    ...
}

class Keyboard implements USBInterface
{
    /** 實作 USB 鍵盤的開機方法 */
    public function boot()
    {
        if ( $this->bluetoothConnect() ) 
        {
            return "已連線到藍芽鍵盤";
                    
        } else {
        
            if ( $this->bootKeyboard() ) 
            {
                return "鍵盤開啟成功"    
            }

        }
    }

    /** 實作其他方法 (略) */
    ...
}

在這個步驟,只要把每個我們想新增的裝置,都宣告實作一個 USBInterface 介面,然後開始寫我們每個裝置裡面該有的實現方法。

你可以看到,雖然都是 boot() 這個方法,但是 MouseKeyboard 的實作方法不同,這個就是定義介面帶給我們的好處,如同我們在 USBInterface 這個介面中描述「身為一個 USB 裝置應該要有什麼方法」,往後這兩個裝置想要成為 USB 裝置的時候,就必須實作這些方法。

步驟3:撰寫主程式邏輯

class Computer
{

    public function __construct(USBInterface $device)
    {
        $this->device = $device;
    }

    /** 
     * 連接到裝置 
     **/
    public function connectDevice()
    {
        /** 裝置開機 */
        $this->device->boot();

        /** 如果未連線成功的話,傳回各裝置的錯誤訊息 */
        if ( !$this->device->isConnected() ) {
            return $this->device->getErrorMessage();
        }

        /** 取得電力與資料 */
        $this->device->getPower();
        $this->device->getData();

        /** 儲存資料成功,關機 */
        $result = $this->device->saveData();
        if ( true === $result ) {
            $this->device->shutdown();
        }

    }
}

我們在 Computer 這個類別依賴了 USBInterface 這個介面。當未來我們要連接到新的裝置時,建立 Computer 實體所需要的 $device 參數,必須是實作 USBInterface 這個介面的類別。

例如,電腦今天要連接滑鼠上來,我們會這樣呼叫

/** 建立一個 USB 滑鼠的實體 */
$device = new Mouse;

/** 建立一個電腦的實體 */
$computer = new Computer($device);
/** 連接到裝置 */
$computer->connectDevice();

而我們如果要改成連接其他的裝置,也只要修改 $device 這個實體,無論是什麼,符合 USBInterface 介面的,都可以傳遞進來使用不用怕出錯,因為只要是實作 USBInterface 這個介面的類別,都一定會有 boot()getPower() … 等等這些方法。主程式的邏輯中,也只要負責呼叫,管好自己的流程就行,不需要管到哪個裝置、怎麼實作的。

結論

  • 定義 介面 interface 可以讓我們把程式邏輯跟實作分離(抽象化),主程式不用管類別怎麼實作,只要顧好自己的流程,呼叫他們定義在介面中的方法就行。
  • 未來在修改程式方面,也只需要實作該介面,變成一個新的類別,就可以無痛擴充現有的程式,而不用回去改到主程式流程。
  • 主程式應該要依賴 介面,而不是依賴某一個類別。如果依賴某一個類別(例如電腦只依賴滑鼠),會造成耦合度太高抽換不易,擴充困難。反而是依賴在一個抽象的介面上,只需要實作它的方法,就可以拿進來程式使用。

PHP 正規表達式匹配中文字及表單驗證

| Comments

正規表達式 (Regular Expression) 是用來處理文字及字串格式的一個方法及表達式。

幾乎所有語言都支援正規表達式的使用,
也有提供相關的函數。

正規表達式可以處理與文字有關的情況,
例如使用者輸入的表單驗證,
出生年月日是否符合格式,
行動電話是不是 09 開頭中間一槓,後面 6 位數…… Email 是不是 xxx@zzz.com 這種格式等等等…。

我們可以用 /[a-zA-Z]+/ 來符合英文字,
例如 ABC abc sexy 會符合規則,
但是 abc123 就不符合
因為我們的規則是 「(英文字母a-z及A-Z) 一次或多次」。

我們也可以利用 ^ 來排除,
例如 /[^a-zA-Z]+/ 代表 「(英文字母a-z及A-Z)以外的任何字 一次或多次」。
所以這時候 123 你好嗎 幸せ 就會附合這個規則,
abcABC 則不會。

但是,假設我們今天要設計一個欄位,可能是姓名,或者是公司名稱,
需要限制欄位的資料不能包含特殊符號,或是只能符合特殊的格式。

這時候如果我們的語言是英文或是其他字母為主的語系,
那還算好處理

因為我們只要設定欄位的規則為 /[a-zA-Z]+/
代表這個欄位只能出現英文字。

這樣,簡簡單單就限制了使用者能輸入的文字,
而不會有其他的例外狀況。

但是,如果是中文呢? 要怎麼限制呢?
把所有的文字抓進來嗎? 把常用的 5000 多個字全部寫進規則裡嗎?

/[一二三四五六七八九十自流認以海美了車文馬總南進信比畫得過整備到著技總景當吸市轉生到故拉色情跟年多香期戲時民品去這因人的趣卻個得們克現就車過的媽原量定提無發八生可馬我大高到經於班表所最時越爸兩比呢自帶才化性義注我種路..]/

那不可能!

還是說用差集的方式,加上剛剛的 ^ 表達式,將特殊符號全部過濾掉?

/[^\!\@\#\$\%\^\&\*\(\)\'\"\[\]\,\.\~\`]/

但是符號這麼多,還有一些根本無法顯示的控制字元,根本無法一次窮舉出來。

爬了一些文,有些是用 unicode 限制範圍

/[\u4e00-\u9fcc]+/

*有的是到 9a05

這個也是一種解法,但是老實說,這樣的寫法也不是很容易閱讀。
其實 PHP 有提供另外一種 Perl 的集合 PCRE (Perl Compatible Regular Expressions) 可以直接套用。

/\p{Han}+/u

中間的 {Han} 表示中文字的意思。(記得行尾的 u,表示 unicode )
這樣子就可以 match 所有中文字,或是排除中文字等等。

也可以處理我們想要的東西,例如「台灣地址」

/^[0-9]{3,5}\s?\p{Han}{1,2}(縣|市)[\p{Han}\w\d\-]+$/u

[0-9]{3,5} 表示郵遞區號數字, 0-9 最少3個,最多5個。
\s? 表示空白可有可無

33325 10023 結束之後,通常就要接縣市,
所以設計了一個 \p{Han}{1,2}(縣|市)
這樣可以符合 桃市 竹縣 高雄市 嘉義縣 等。

PCRE 可用集合

集合 意義
{Hiragana} 平假名
{Katakana} 片假名
... 其他 參考網址

如此一來,比寫 \uXXXX 的代碼來得容易閱讀多
而且撰寫規則時,也很清楚知道什麼字代表什麼意思。

結論

表單驗證的重點主要是在於「允許」(白名單),而不是「框住」(黑名單)。
因為黑名單完全無法窮舉出來。

例如,此欄位「只允許英文」、「只允許數字3~5個」
而不是「除了單引號之外的所有字」

因為永遠不知道使用者輸入什麼給你
況且我們想得到的黑名單只有這些

真正的特殊符號還是有許多無法顯示、無法處理的。

如果你有驗證中文字的需求的話,希望這篇可以給你作為一個參考。

如錯誤還請指教

macOS Sierra 安裝 php7 及 MongoDB driver

| Comments

一直以來,都是使用 Mac 內建的 apache2 及 PHP 模組。
其實只要打幾個勾,去掉 LoadMoudule 前面的註解,設定一下就可以用了。

不過,因為 Laravel 對於系統的要求愈來愈高,
加上我的 Mac 內建的是 5.5 版,再也無法安裝 Laravel 5.2 以上的版本。

也因為最近因為要使用 jessenger/laravel-mongodb 這個套件,

我利用 Homebrew 安裝完 php70
在內建的 apache 準備 LoadModule ,結果找不到這個 .so 檔的路徑

又 MongoDB 的 driver 不知道怎麼搞的,
在 brew install 的結果顯示 already installed 但在 phpinfo() 裡面又找不到。
(而且還有 php55-mongophp55-mongodb 這兩種..)

反正就是經過種種原因,覺得乾脆移除內建的 apache 再把全部東西都用 Homebrew 安裝好了。

以上抱怨完之後,把過程寫下來希望可以幫助到其他人= =||

1. 移除內建的 apache

先讓 apache 暫停服務
sudo apachectl stop
移除內建的 apache
sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist 2>/dev/null

2. 使用 Homebrew 安裝所要的軟體

安裝 apache (apache 在這邊叫作 httpd)
brew install httpd24
安裝 php 7

-with-apache 很重要一定要加,因為加上去才會自動幫我們把 LoadModule 設定加到 httpd.conf 裡面
(不然還要自己找 php 路徑)

brew install php70 --with-apache
安裝 MongoDB 的 PHP 驅動
brew install php70-mongodb --with-apache

3. 重新啟動 apache

sudo apachectl restart

大致上已經完成,但是如果打開網頁的話,PHP 可是會直接噴出程式碼的。
所以我們還要設定一下 httpd.conf

4. 設定 apache

打開 httpd.conf 檔案
 sudo vim /usr/local/etc/apache2/2.4/httpd.conf
去除 php 行頭的註解

如果剛剛在安裝的時候,有使用 -with-apache 參數,
那麼你應該可以找到這行,而且是直接啟用的狀態而無需自己去除註解。

LoadModule php7_module        /usr/local/Cellar/php70/7.0.15_8/libexec/apache2/libphp7.so
監聽 80 port

使用 brew 安裝的 apache 會預設監聽 8080 port
看你的需要,如果你想要直接使用 80 而不想在網址後面加上 :8080 那你可以更改設定

Listen 80
增加 PHP 關聯

httpd.conf 的任意處,加上

<FilesMatch \.php$>
    SetHandler application/x-httpd-php
</FilesMatch>

5. 重新啟動 apache

sudo apachectl restart

6. 大工告成

打開終端機,輸入

php --version

應該可以直接看到類似如以下的結果

PHP 7.0.15 (cli) (built: Feb 25 2017 18:17:23) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies

使用 AngularJS 雙向綁定 Semantic UI Dropdown

| Comments

最近因為需要,使用 AngularJS 與 Semantic UI 作為網站開發的前端框架。
AngularJS 的特性與方便的地方,就是他的雙向資料綁定功能。

然而,Semantic UI 預設是使用 jQuery 來操作他的元件等。

以 Semantic UI 提供的 Dropdown 為例


  

會發現一個有趣的現象,
當下拉選單選取的時候,size 這個 model 會更新
但從 controller 更新 $scope.size 時 (或從另一個 input 更新 size),卻不會同步到 Dropdown 上跟著選取。

原因是 size 雖然與 <select> 綁定了,但 AngularJS 與 Semantic UI 各自為政
想要更新 Dropdown? 沒問題! 請透過 Semantic UI 提供的 jQuery API 來更新。

$('.dropdown').dropdown('set selected', 'XL');

使用 Semantic UI 提供的 API 固然能解決下拉選單的選擇問題,但這樣只有單向呀!

能不能當我選擇下拉選單的時候,size model 跟著更新;而手動指派 size 的時候,下拉選單跟著更新?

有鑑於此,只好使用 AngularJS 的 $watch,監視 $scope.size 的變化。當有變化的時候,手動同步到下拉選單上。

$scope.$watch('size', function (n, o) {
    $timeout(function() {
        angular.element('select[ng-model=size]').dropdown('set selected', n );
    });
}, true);

使用 $scope.$watch 監視 size 的變化,
當一有變化的時候,立刻執行一次 dropdownset selected 方法。

angular.element 的選擇器可以自定,但我習慣直接用 [ng-model=size] 來當作名稱,
因為這樣就不需要另外指定 ID 或 name 了。

如此一來,改變下拉選單,size 本來就會更新,但改變 size 時,下拉選單也可以同步更新了。

算是... 手動綁定吧..

PS. 所需要的 $scope $timeout 記得注入。

解決升級 OS X El Capitan 後內建 Apache 伺服器 Forbidden 的問題

| Comments

蘋果發表了最新的作業系統 OS X 10.11 El Capitan

身為一個兩光的攻城屍,當然要檢查一下自己本機的開發環境有沒有異狀
理所當然,打開隨便一個專案後,發現首頁是 Forbidden 的

檢查一下,發現以下的錯誤訊息

No matching DirectoryIndex (index.html) found, and server-generated directory index forbidden by Options directive

上述的錯誤訊息指的是「找不到 index.html」這個檔案
但其實我們的首頁多是 .php 結尾的 index.php

系統認不得 .php 這個檔名,
原因是每次更新後,內建的 Apache 都會自動把 PHP 模組預設不載入

這個時候,只要到 apache.conf 內,將 PHP 模組載入,就可以了!

  1. 進入設定 apache.conf 設定

    Benyi$ vim /etc/apache2/httpd.conf
    
  2. 將此行取消註解,也就是「要載入此模組」

    #LoadModule php5_module libexec/apache2/libphp5.so
    
  3. 輸入 :wq 存檔離開

  4. 重新啟動 Apache

    Benyi$ sudo apachectl restart
    

在 iPhone 上打字,為什麼我會用拼音輸入法

| Comments

我是蝦米族
我打嘸蝦米從國中二年級開始,到今年已經是第七年(大概也快第八年了吧)
(對了,要正字,是「嘸」蝦米而不是「無」蝦米!)

嘸蝦米的好,是眾所皆知
的確,我在電腦上,使用的是嘸蝦米輸入法
甚至這篇文章,也是用嘸蝦米一字一鍵所打出來的

不過,我今天不是要介紹嘸蝦米輸入法
而是要告訴你,為什麼我在手機上,不是用嘸蝦米,不是用注音,而是使用拼音輸入法

Facebook 設定公開貼文陌生人只能看 不准回覆!

| Comments

其實用 FB 也有好一段時間了

大概是從我國中三年級開始,那時候因為大家在玩「開心農場」,
不玩好像會跟不上流行一樣,於是就辨了一個 Facebook 帳號開始種菜

漸漸地,大家開始使用 Facebook 的社交功能,PO文、上傳照片等等

歷經幾次改版,其實我們的隱私漸漸灘在陽光下
洩露隱私早就不是什麼頭條大新聞,只能祈禱自己不要哪天占用博愛座被肉搜

Mac OS X 10.10 Yosemite Review 新介面概覽

| Comments

Apple 在 WWDC 2014 昭告天下他們即將推出代號為 Yosemite 優勝美地 的 Mac OS X 10.10

當時看到就覺得很心動,介面也是美到一個不行!
不過,依照官方的說法,預計是今年秋季會登場,但已經開放給開發者下載。

據幾個擁有開發者帳號的朋友說,其實最一開始開放給 developer 下載的 Mac OS X 10.10 Yosemite 測試版本 bug 很多

不過,前幾天忽然收到 Apple 的來信,指出新版的 Mac OS X 10.10 Yosemite 已經開放 beta 版供使用者測試了!真是令人期待!


會收到這封信,其實大概是一陣子前,Apple 有開放使用者登記,只要填寫 E-mail 帳號等等,可以嚐鮮的時候就會通知你。終於,當郵差來按門鈴的那一刻,實在是興奮到不能自已!

於是我立馬安裝,可能是一下子太多人下載,一直會下載失敗。不過,重試過程就略過,直接來看 Mac OS X 10.10 Yosemite 的介面和以往有什麼差吧!


版本號是 14A299l 的 Mac OS X 10.10 Yosemite Beta 版


Mission Control 背景改為毛玻璃的模糊特效


畫面底下的 Dock 與 Mavericks 的貢品桌不同


通知中心更美觀了,與以往不同,畫面不會整個往左推,而是直接推出來顯示


可以自行編輯要哪些 widget,也可以自行放上想要的工具,如計算機等


Finder 視窗,滑到下面一點的時候,視窗上面就會變成毛玻璃特效


Safari 也是有毛玻璃特效


原本視窗右上角的全螢幕不見了,而是整合進左上角的紅綠燈中。 如果想要使用先前的 + 功能 (最適視窗大小),只要按一下 option,就可以把視窗縮放到「最適視窗大小」,而不是全螢幕了。


Spotlight 改到螢幕正中央顯示


左上角的電池圖示變的較可愛,有點像 iPhone?


選單底圖也有毛玻璃特效


總結

以上是 Mac OS X 10.10 Yosemite 的概覽
此版本與上一版本 (10.9 Mavericks) 比較,很多介面都有不同之處,Apple 總是大膽創新啊!

個人認為與 iOS 愈來愈像 (比如那個電池圖示和毛玻璃特效)

整體來說,缺少那份 Mac 的 「專業感」,反而有點「玩具」的感覺
不過介面設計的美觀,加上人性化的功能,使用者體驗就會大大加分!

但是,此版本也不是沒有 bug,目前為止遇到幾個比較明顯,也影響的就是:

  1. 有時按 cmd+opt+4+space 想要截取特定視窗會失敗,怎麼按就只有選取範圍 (cmd+opt+4),無法反白視窗擷圖
  2. 不特定應用程式時常閃退
  3. 破圖

這是 Mac OS X 10.10 Yosemite Beta 的概覽,
如果有任何錯誤,還請指正

期待 Mac OS X 10.10 Yosemite 正式版發佈的那天!

CSS 如何把 div 垂直水平置中 (畫面正中央)

| Comments

CSS 要如何把 div 垂直及水平置中,放在畫面的正中央呢?

假設有一塊 div 長這樣

        #block {
            height: 200px; /*高度*/
            width: 400px;  /*寬度*/
            background-color: black;
        }

今天我們要把他放置在畫面的正中間
只要加上

            position: absolute;     /*絕對位置*/
            top: 50%;               /*從上面開始算,下推 50% (一半) 的位置*/
            left: 50%;              /*從左邊開始算,右推 50% (一半) 的位置*/

可是,加上以上的程式碼後
網頁會變成這樣子

雖然上面、左邊都推50%進來了,但是並不是我們想要的效果

這個時候,我們要再加上

            margin-top: -100px;     /*高度的一半*/
            margin-left: -200px;    /*寬度的一半*/

把整個 div 上、左各往回推一半,這樣子就是我們想要的置中效果了。


所有程式碼如下

        #block {
            height: 200px;
            width: 400px;
            background-color: black;

            position: absolute;     /*絕對位置*/
            top: 50%;               /*從上面開始算,下推 50% (一半) 的位置*/
            left: 50%;              /*從左邊開始算,右推 50% (一半) 的位置*/
            margin-top: -100px;     /*高度的一半*/
            margin-left: -200px;    /*寬度的一半*/

        }