2014年4月10日 星期四

設計模式 (十) 開拓視野 part1

關於物件導向設計有三個基本概念,物件,封裝和抽象類別,設計人員對這些概念的看法是很重要的,傳統的看法有很大的侷限性

傳統的概念

  1. 將物件視為資料和方法的簡單集合
  2. 封裝視為資料隱藏
  3. 繼承 = 特殊化再利用
新的看法

  1. 將物件視為具有責任的東西
  2. 封裝視為隱藏一切的一種能力
  3. 繼承 = 物件分類的一種方法
  4. 共通性與可變性分析
  5. 概念視角,規約視角,實作視角抽象類別與衍生類別的關係
  6. 設計模式與敏捷程式設計方法的差別(冗餘性,可讀性,測試性)

物件

以傳統的方式來看,物件只不過是個資料處理的方法,然而新的看法則是物件是一個具有責任的實體,這兩個差別在於,將物件視為一個責任的實體會讓我們更專注於物件的意圖(責任),而不是如何實作他,我們可以利用這個概念
  1. 做出初步的設計,這個物件的責任為何,(interface abstract)各種抽象的概念
  2. 當我們將一切的職責分配完成再來關心實作部分
舉個例子來說

有個需求希望在畫面上顯示/移除形狀,依照這個需求我們可以想像出一個物件需要負責這些事情
  1. 選擇一種形狀
  2. 顯示
  3. 移除
  4. 取得位置
再進一步仔細想想,是否有發現,所有的形狀物件都一定擁有2,3,4三個必要功能(責任)
所以最後的UML圖會長像是下面的樣子
在此時我們尚未關注細節,我並不曉得方形該怎麼顯示,更不知道三角形該如何取得各點的位置,我不需要瞭解到細節,事實上,這種概念將物件的實作與使用他的物件解耦了。


封裝

傳統的看法為資料的隱藏,然後這種看法侷限性太大了,新的看法則是將封裝視為任何型式的隱藏
  1. 實作細節
  2. 衍生類別
  3. 設計細節
  4. 實體化設計

讓我們看看上面的UML圖,這是一個adapter pattern的UML圖

Shape shape = new Rectangle(); 
  • 這個動作為類的封裝

shape.Display();  
  • 實作的封裝

利用adapter pattern 將 third-party Circle封裝進 Circle  
  • 其他物件的封裝
Rectangle / Triangle/Circle ,詳細的資料只存在自己的class內部,例如一些flag ,各自的location....etc
  • 資料的封裝

用這種更寬廣方式來看待封裝,優點就是能夠帶來一種更容易分解程式的方法,而封裝層就成為了設計需要遵循的介面,透過封裝Shape的衍生類別,當我有新的形狀要加入時只需要新增一個class就好了,並不會對其他的程式造成影響。

早期的提倡者,曾將類別得再利用作為巨大的優勢,通常都是建立一個基礎類別之後,然後再從基礎類別衍生出新的特殊化的類別,如下圖


可以想成是普通的圓形以及以虛線畫成的特殊圓形,
利用繼承的好處是我可以快速的利用Circle已經實作的程式碼,但是...........
壞處呢?

  • 繼承造成了弱內聚,我再修改Circle的程式碼時,我還必須關注SpecialCircle是否會出現SideEffect。
  • 假設我今天希望畫一個虛線版本的三角形呢? 畫虛線的程式碼是否無法再利用,我必須重新建立一個新的物件叫做SpecialTriangle,然後裡頭再重新實作虛線的功能,這種作法減少了程式碼的再利用性
  • 同上面描述的,這也減少了物件的延展性,假設今天有更多更多的類別出現,粗線圓形,紅色圓形,漸層圓形,你會發現程式越來越難改,if else判斷越加越多,bug越來越多?!
而另一種方式則是依相同行為分類,使用聚合,關於這部分會在part2的時候做更多的說明~

2014年4月8日 星期二

設計模式 (九) adapter pattern

adapter pattern又稱轉換器,介面轉換。

adapter pattern 就是將一個類別的介面轉換成客戶希望的另一個介面,也就是讓原本不相容的介面一起工作。

舉例來說,假設有以下的需求

  1. 我希望每個shape類別都有顯示的行為
  2. 使用者不需要知道物件為甚麼形狀
接著大概會出現類似下面的程式碼

public abstract class Shape {

	public abstract void Show();
}

class Rectangle extends Shape
{

	@Override
	public void Show() {
		System.out.println("Rectangle");
	}
	
}

class Triangle extends Shape
{

	@Override
	public void Show() {
		System.out.println("Triangle");
	}
}

UML圖會長這樣

可以很看出,Rectangle和Triangle繼承並實作了Shape

接著在此時又出現了一個需求,需要再多實作一個五芒星的圖型
可是我並不知道如何實作這個功能,所以請教了google 大神之後,我得到了一個第三方的sdk
他的功能很完整,不過實作方法有點不同,如下

public class FiveStar
{
	public void FiveStarDisplay()
	{
		System.out.println("FiveStar");
	}
}

可以發現到,我現在必須使用if else 來判斷該怎麼顯示圖型

	public static void showShap(Object shape)
	{
		if(shape instanceof Shape){
			((Shape)shape).Show();
		}
		else if(shape instanceof FiveStar){
			((FiveStar)shape).FiveStarDisplay();
		}
	}
這是個悲劇的開始,客戶必須區分這是甚麼形狀,只要用到shape就必須多加if else 判斷,
並且或許在未來某天,還可能誤將 shape 轉型轉錯造成app crash。

那麼我們可以做甚麼來避免這場悲劇的發生呢?

先思考一個問題,怎麼做對我們最好?
如果我們有辦法將FiveStar視為Shape那是不是所有的問題都迎刃而解了呢?
可是該怎麼做呢?

看看下面的方式

class FiveStarAdapter extends Shape
{
	private FiveStar star;
	@Override
	public void Show() {
		// TODO Auto-generated method stub
		star.FiveStarDisplay();
	}
	
}

我們將 FiveStar封裝在 FiveStarAdapter裡這樣既達到了實作五芒星的功能,又可以維持客戶不需要區分是甚麼形狀就可以使用,接著就可以把showShap改成這樣


	public static void showShap(Shape shape)
	{
			shape.Show();
	}


程式變得更加簡潔,而且更低耦合度了


結論
Adapter的實作方式就是建立一個具備所需介面的新類別(FiveStarAdapter),然後包裝原有的類別(FiveStar),如此就可以將原有的類別轉型成Shape

2014年4月6日 星期日

設計模式 (八) facade pattern

Facade Pattern

定義一個更高層的介面,使子系統更加容易使用,提供更簡單的方法與系統交流

  • 意圖:希望簡化原有系統的使用方式
  • 問題:只需要使用某個複雜系統的子集,或者需要以一種特殊的方式與系統交流
  • 解決方案:façade為原有系統的客戶提供了一個新的介面
  • 參與者與協作者:為客戶提供一個簡化介面,更容易使用
  • 效果:façade模式簡化了對所需子系統的使用過程,由於façade並不提供完整的功能,客戶可能無法使用某些功能
  • 實作:定義一個或多個具備所需介面的新類別;讓新的類別使用原有的系統

下面有四個class 分別是家庭Facade控制器,風扇控制器,燈光控制器,電視控制器

public class HomeDeviceFacade {
 private FanController FC = new FanController();
 private LightController LC = new LightController();
 private TVController TC = new TVController();
 
 public void DeviceOn()
 {
  FC.TurnOn();
  LC.TurnOn();
  TC.TurnOn();
 }
 
 public void DeviceOff()
 {
  FC.TurnOff();
  LC.TurnOff();
  TC.TurnOff();
 }

 public static void main(String args[]) {
    
 System.out.println("====Facade====");
     HomeDeviceFacade facade = new HomeDeviceFacade();

        facade.DeviceOn();
        System.out.println();
        facade.DeviceOff();          
    }
}

class FanController
{
 private int speed;
 public void ChangeFanSpeed(int _speed)
 {
  speed = _speed;
 }
 public void TurnOn()
 {
  System.out.println("Fan on");
 }
 
 public void TurnOff()
 {
  System.out.println("Fan off");
 }
}

class LightController
{ 
 public void TurnOn()
 {
  System.out.println("Light on");
 }
 
 public void TurnOff()
 {
  System.out.println("Light off");
 }
}

class TVController
{
 private int vol;
 public void changeVolume(int _vol)
 {
  vol = _vol;
 }
 public void TurnOn()
 {
  System.out.println("TV on");
 }
 
 public void TurnOff()
 {
  System.out.println("TV off");
 }
}


可以看出Facade讓我們簡化了其他的動作,原本需要一個一個將電視風扇燈光打開,現在只需要一個步驟就可以將所有的裝置做開關,這簡化了系統

結論
Facade模式提出了一種通用方法,建立了新介面供客戶使用, 客戶並不需要原有系統的所有功能

Facade還有其他功用
  • 追蹤系統的使用情況
由於所有的裝置都會經過HomeDeviceFacade 所以我們可以利用這個class 追蹤裝置的使用狀況 
  • 改換系統
由於裝置都經由HomeDeviceFacade 做處理,當有新版本的 TVController2 出現時,我們的控制程式(Main)並不需要做更改,只需要修改HomeDeviceFacade  裡的 TVController即可。

最後依照慣例附上 Sample Code

2014年4月2日 星期三

Android 開發 (三十九) facebook send app request

什麼是app request ?

如下圖我們可以透過app寄送邀請給使用者

該怎麼寄送?

使用facebook 內建的sdk WebDialog來傳送資料


 WebDialog.RequestsDialogBuilder builder =
     new WebDialog.RequestsDialogBuilder(mActivity, Session.getActiveSession())
             .setOnCompleteListener(new WebDialog.OnCompleteListener() {
                 @Override
                 public void onComplete(Bundle values, FacebookException error) {
                     if (error != null) {
                         Log.w(TAG, "Web dialog encountered an error.", error);
                     } else {
                         Log.i(TAG, "Web dialog complete: " + values);
                     }
                 }
             });
     builder.build().show();

只需要使用builder的show之後,就會出現讓我們選取分享給朋友的頁面,在選取完成並按下傳送之後就會將訊息傳送給朋友。

比較奇怪的地方是setTitle和setMessage都沒有反應不知道用途在哪


在訊息傳送出去之後我原本以為會顯示在通知訊息的欄位,但是訊息通知卻顯示在遊戲邀請的位置

這時候就必須去facebook developer app 開發頁面做設定

在選擇平台的地方選擇 app on Facebook

設定Canvas URL and Source Canvas URL
在設定完成之後,再重新送一次通知,這次通知不再是出現在遊戲邀請欄位,
而是出現在下圖最右邊的通知中


這樣只要每次傳送app使用邀請,朋友就可以直接在通知看到,而不再需要刻意前往遊戲邀請(應該不會有幾個人會刻意過去那邊找),對於user來說,這樣也會有較好的user experience。

Android 開發 (三十八) facebook friendPicker

FriendPickerFragment 是 facebook sdk內含的fragment,初始化這個fragment必須擁有幾個參數
        intent.putExtra(FriendPickerFragment.USER_ID_BUNDLE_KEY, userId);
        intent.putExtra(FriendPickerFragment.MULTI_SELECT_BUNDLE_KEY, multiSelect);
        intent.putExtra(FriendPickerFragment.SHOW_TITLE_BAR_BUNDLE_KEY, showTitleBar);

userId 為顯示哪個user的朋友,預設為登入本人  (null)
multiSelect 是否可以多選
SHOW_TITLE_BAR_BUNDLE_KEY 文件並沒有解釋,如果設為false 就不會出現 Choose Friends的title 以及 done的按鈕,如下圖。



在進入Picker頁面時必須將  friendPickerFragment.loadData(false); 設為false,這樣才會開始load friend list,使用getSelection讓我們可以取得選取的friendList,在我們選取完成後,接著必須專注在如何將資料傳回呼叫端。

使用startActivityForResult讓我們在選取完成並按下done之後可以將資料回傳給呼叫者,
但是在這邊會遇到一個問題,List<GraphUser> 並不能利用 intent 傳送資料
所以在 facebook的sample code裡面,利用了application來傳遞資料,
我覺得這不是一個很好的解決方案,所以花了點時間去思考較好的方法,
讓我們看看下面的sample Code


             Intent intent = new Intent();
             ArrayList<String> vals = new ArrayList<String>();
             for(GraphUser user: friendPickerFragment.getSelection()){
              vals.add(user.getInnerJSONObject().toString());
             }
 
             
             intent.putStringArrayListExtra("list",vals);
                setResult(RESULT_OK, intent);
                finish();

利用getInnerJSONObject().toString() 將資料轉成String 並且利用 putStringArrayListExtra將資料放進intent以便於傳送至呼叫者,在呼叫端利用下面的程式碼還原

  List<String> vals= new ArrayList<String>();
      vals = data.getStringArrayListExtra("list");
      String result="";
  try {
   for (String val : vals) {
    JSONObject jsonObj;
    jsonObj = new JSONObject(val);

    GraphUser user = GraphObject.Factory.create(jsonObj,
      GraphUser.class);
    result += user.getName()+"\n";
    Log.d("Ted", user.getName());
   }
  } catch (JSONException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
將取得的String轉成JSONObject之後,再利用GraphObject.Factory 轉回GraphUser,
這樣我們就可以取得原本的資料了(雖然要經過一小段的轉換)。

最近又有機會接觸 facebook sdk ,我想之後應該還會有幾篇相關的介紹吧!!

2014年4月1日 星期二

Android 開發 (三十七) facebook login

在使用facebook sdk的時候,第一個步驟就是必須先登入
相關的前置設定可以參考Android 開發 (九) Facebook GraphApi Explorer

在相關設定設定完成之後,只需要使用UiLifecycleHelper
在這邊要特別注意,必須在每個state都使用 helper的method
範例如下


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        lifecycleHelper = new UiLifecycleHelper(this, new Session.StatusCallback() {
            @Override
            public void call(Session session, SessionState state, Exception exception) {
                onSessionStateChanged(session, state, exception);
            }
        });
        lifecycleHelper.onCreate(savedInstanceState);

    }
    
    @Override
    protected void onResume() {
     // TODO Auto-generated method stub
     super.onResume();
      lifecycleHelper.onResume();
     
    }
    @Override
    protected void onStop() {
     // TODO Auto-generated method stub
     super.onStop();
      lifecycleHelper.onStop();
    }
    
    @Override
    protected void onDestroy() {
     // TODO Auto-generated method stub
     super.onDestroy();
      lifecycleHelper.onDestroy();
      
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
     // TODO Auto-generated method stub
     super.onActivityResult(requestCode, resultCode, data);
      lifecycleHelper.onActivityResult(requestCode, resultCode, data);
    }

如果沒在每個state使用該使用的method將可能造成fb登入功能不正常,一直無法正常登入。
最近發現了fb登入不正常的問題,花了好久才找到這個原因......



2014年3月31日 星期一

設計模式的解析與活用 (一) 物件導向範型

從例子開始說明吧!!

講師與學生,老師的責任就是讓學生知道下堂課要去哪上課
所以老師應該會
  1. 獲取聽課名單
  2. 針對每個人
  3. 找到每個人要去聽的課
  4. 找到聽課的地點
  5. 找到前往路徑
  6. 告訴他們
但是實際上真的是這樣嗎?

實際上應該是老師貼一張課表給學生自己去看吧
其實這個行為就是責任轉移,讓學生去負責自己的行為

這兩種作法的差別在哪?

第一種方法講師必須關注許多細節,因為所有的事情都由講師負責
第二種方法講師只負責告知,接著學生負責前往所屬課堂

第二種方法的好處在於

假設今天增加了助教研究生,助教需要在下堂課前收集本堂課的學生對課程的評價
對講師來說依然只需要告訴學生下堂課的位置,學生各自會負責該做的責任,研究生會蒐集評價並前往下堂課,普通學生會前往下堂課,翹課的學生會自行翹課….etc

各司其職(負責自己的責任)

第二種方法有以下三個方面不同

  1. 人們對自己的行為負責
  2. 講師將不同類型的人(普通學生&研究生),一視同仁,把他們都視為學生,並且告知他們必須前往下一堂課
  3. 講師不需要知道學生如何前往下一間教室
用術語來說明的話就是

概念-軟體要負責甚麼
規約-怎麼使用軟體
實作-軟體怎麼履行責任

講師要負責甚麼 ClassRoom  getnextClassRoom()  取得每個學生的課堂
學生必須做甚麼 gotoNextClassRoom(ClassRoom room) 
研究生必須收集資料並且前往下堂課
普通學生必須前往下堂課

對講師來說他只需要負責告知的動作
普通學生與研究生會負責自己該負責的責任


封裝的概念

講師不知道哪些是一般學生,哪些是研究生,對講師隱藏了學生的類別(也就式封裝了學生)
雖然學生都是前往下堂課堂,但是行為卻不同

封裝的好處
  • 使用者不需在操心實作的部分,使用者只需要知道想要做甚麼,剩下的交由被呼叫者去處理
  • 可以在不考慮呼叫者的情況下實作(testable)
  • 其他物件對該物件內部是未知的,例如講師呼叫gotonextClassRoom,但是講師並不知道普通學生、研究生實際做了哪些事情

其實這個例子用到了LSP,以及DIP

普通的學生和研究生都是學生,都擁有學生的行為,例如前往下堂課,姓名,學號之類的
LSP-繼承自學生的物件必定擁有學生的行為

講師告訴學生必須前往下堂課,但是他並不知道是告訴研究生還是普通學生
DIP-細節依賴於抽象,講師只呼叫gotoNextClassRoom,學生會自己依照他們的類別,做他們該做的事情,物件只在概念上耦合,在實踐並不耦合

最後,要稍為推薦一下設計模式的解析與活用這本書,雖然目前也只看了一個章節,不過真覺得獲益良多阿!