2012年5月2日水曜日

Bridgeパターン

今回はBridgeパターンを復習してみる。Bridgeパターンは文字通り橋渡しをするパターンであり、まずは機能と実装を分離することの大切さを思い出す必要がある。

具体的なケースとして以下が考えられる。

任意の処理を行うバッチプログラムを作成するためのクラス設計


基本クラス
すべてのバッチプログラムに共通の処理(ログ出力)を実装し、ビジネスロジックについては各継承クラスに任せる。
継承クラスA
ビジネスロジックとしてAという機能を実装
継承クラスB
ビジネスロジックとしてBという機能を実装

変更要求

このクラス設計にしたがって開発を行った場合、次のような変更要求があると途端にクラス設計の美しさが損なわれる。
  • CおよびDという機能を実現するバッチを追加してほしい。また、これらのバッチは起動時にあるファイルをロックし、終了時にロックを解除する必要がある。

要求への対応

基本クラスへロック機能を追加すると、既存のプログラムに影響がでてしまうので下記いずれかが対応として考えられる。
  1. 基本クラスにロック機能を追加した中間クラスを作成し、継承クラスCとDではこの中間クラスを継承する。
  2. ロック機能は別クラスに集約し、継承クラスCとDでは集約クラスのロック機能を利用する。

対応案の考察

  • 1番目の方法は、基本クラスから始まる「実装を追加するための継承」という流れに「機能を追加するための継承」という流れを混ぜてしまう。
  • 2番目の方法は、ロック機能とビジネスロジックの結合が強くなりがち。(やっぱロック機能いらないや、とか別のバッチにもロック機能つけて、とか言われた場合にビジネスロジックを含むクラスの修正が必要になってしまう)

さらに、Eという機能を実現するバッチを追加してほしい。また、このバッチの実行開始と終了時にEメールを送信してほしい。などと言われると余計面倒なことになる。

機能と実装の分離

このような場合に、Bridgeパターンに基づいて機能(を追加するための継承)と実装(を追加するための継承)を別の流れになるようなクラス設計にしておけば、変更要求に強い構造になる。
Abstraction
機能を追加する流れの最上位クラス。Implementorのインスタンスを保有する(=Bridge担当)
Implementor
実装を追加する流れの最上位クラス
Refined Abstraction
機能を追加する際にAbstractionを継承して作成するクラス。例で言うところのロック機能の追加はここで実装する。
Concrete Implementor
実装を追加する際にImplementorを継承して作成するクラス。例で言うところの継承クラスA~Dはこれのこと。

Mementoパターン

たまにはデザインパターンについて復習でもしてみる。今回はMementoパターン。
Mementoには記念品や思い出といった意味があり、ざっくりとまとめると「状態を保存しておけば、やり直しができるじゃないか!」といったもの。

Mementoパターンに登場するのは下記の3クラス。(便宜上クラスとしているが、役という表現のほうがしっくりくるかも)

Originator

Save(Mementoを作成する)とLoad(Mementoを読み込む)の機能を有するクラス。

Memento

セーブデータそのものを表すクラス。

Caretaker

Originatorに働きかけてSaveとかLoadを実行し、セーブデータ(Memento)を管理するクラス。


ポイント

  • Originator君はMementoを作成する人なので、Mementoのすべてに触れることができる。
  • 対してCaretakerさんはMementoを管理(永続化とか)するだけなので、Mementoの必要最低限についてのみ触れられれば良い。
って感じにすると、Mementoが美しくカプセル化される。

2011年10月26日水曜日

Cookieを使ってもいいの?

Cookieといえば、ログインを伴うサイト構築を行う際に、セッションIDを保存するアレだ。
しかし、なんとなく使っているCookieなので、セキュリティとかどうなの?とかHTML5のWeb Storageと比べてどうなの?など、突き詰めるとモヤっとした部分が多い。

今回は、とりあえずCookieの使用を検討するにあたって知っておくべきと思われることをまとめる。

Cookieとは

複数のページを跨いだ、あるいはブラウザの起動を跨いだ情報共有を行うための機構である。 その実態は、PC上に保存されたテキストファイルであり、ページにアクセスする度にサーバーへ送信される。

Cookieのパラメータ

パラメータ意味
KEY=VALUECookieに設定するキーと値
expires=VALUE有効期限、省略=ブラウザ終了まで、過去=Cookie削除
domain=VALUECookieの送信先ドメイン
path=VALUECookieの送信先パス
secureSSL通信時のみCookieを送信する

Cookieの発行方法


その1:HTML
<meta http-equiv="Set-Cookie" content="VISITED=1; expires=Sat,26-Nov-2011 00:00:00 GMT;">

その2:JavaScript
document.cookie
  ="VISITED=1; expires=Sat,26-Nov-2011 00:00:00 GMT;";

その3:サーバーサイド

HTTPヘッダで指定する

Set-Cookie: VISITED=1; expires=Sat,26-Nov-2011 00:00:00 GMT;

Cookieにまつわるセキュリティの話


Domainパラメータ

ockeghem(徳丸浩)の日記によれば、RFC6265に規定されている通り、Cookieの送信先ドメインはDomainパラメータで指定したドメインおよびそのサブドメインであるらしい。
送信先を限定するという意味では、Domainパラメータを指定しない(=発行元ホストのみに限定する)ほうが安全である。

但し、IE9、iモードではDomain指定無しでもRFC6265を無視してサブドメインへも送信してしまうので注意が必要。


Pathパラメータ

PathパラメータではCookieの送信先パスのプレフィクスを指定できるが、その効果は「無用なCookie送信を抑制する」くらいであり、「指定したパス以外にCookieが送信されないことを保証」できるものではない。

高木浩光@自宅の日記に詳細が記されているが、同一ドメイン内の任意のページでFRAME+JavaScriptを利用することでCookieを取得できてしまう。


Cookie Monster Bug(Cross Domain Cookie Injection)

  • .example.co.jpに対して発行したCookieは、そのサブドメインであるwww.example.co.jpにも送信される。
  • 同様に.co.jpに対して発行したCookieは、あらゆるxxx.xxx.co.jpに対して送信される。

故に.co.jpや.comに対してCookieが発行されないようにブラウザが規制すべきだが、IE9やOpera 11にはこの規制が無い…という問題。
(参考:属性型JPドメインと地域型JPドメインに対するCookie Monster Bug調査)

典型的な影響としてセッションIDの固定化攻撃(Session Fixation)というのがあるらしい。奴らはこんな感じで攻めてくる。


Session Fixation攻撃への対策

  • ログインの度にセッションIDを再発行して、ちゃんとCookieに設定し直す。


Cookie Monster Bugへの対策

Cookie Monster Bugの影響により、上位のドメインに対して発行されたCookieが干渉する可能性がある。一例として下記が挙げられる。

SESSIDという名前で2つの値が送信されているHTTPリクエスト

POST /login.cgi HTTP/1.0
(略)
Cookie: SESSID=malicious_cookie; SESSID=08afa677654dcbg44eadfb46e1858119
Content-Type: application/x-www-form-urlencoded
Content-Length: 35

対策として

  • 上位のドメインに対して有効期限切れのCookieを発行して無効化する
  • 複数SESSIDへの対応を行う(有効なものを探す or エラーとしてしまう)
  • Cookie使わない
といったことが考えられる。
参考:Cookie Monster襲来! 戦え、星野君

2011年7月5日火曜日

Honeycomb(Android 3.0)まとめ(その2)

前回に引き続き、7月4日に行われたAndroid Develoer Lab Private Sessionの内容について、備忘録を兼ねて要所をまとめてみる。

今回はプログラミングTipsについて。

互換性

parallel activities pattern

Honeycomb(Android OS 3.0)以上であるか、タブレット端末であるか、従来のスマートフォン(Android OS 2.x)であるかによってActivityを切り替えるTips

// OSバージョンを判定する方法
boolean isHoneycomb = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;

// さらに画面サイズを判定する方法
int layoutSize
  = context.getResources().getConfiguration().screenLayout
  & Configuration.SCREENLAYOUT_SIZE_MASK;

boolean isTablet = layoutSize == Configuration.SCREENLAYOUT_SIZE_XLARGE;


Interfaceでセンサーを隠蔽

例として画面の傾きを取得するための手段を提供するinterfaceを定義し、その実装としてデバイスの搭載センサーに応じてジャイロスコープセンサーと加速度センサーを切り替えるといった方法

// 傾きを取得するための手段を提供するinterface
interface IOrientationSensorListener
{
  public String getValue();
}

ジャイロスコープセンサーを利用する実装

// ジャイロスコープセンサーを利用する実装
class Gyro implements IOrientationSensorListener, SensorEventListener
{
  private SensorManager mSensor;
  private String mValue;
  
  // C'tor
  public Gyro(SensorManager sm)
  {
    mSensor = sm;
    mSensor.registerListener(
      this,
      sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
      SensorManager.SENSOR_DELAY_UI);
  }

  // センサーの値が変化した際に呼び出される
  @Override
  public void onSensorChanged(SensorEvent event)
  {
    if (event.sensor.getType() != Sensor.TYPE_GYROSCOPE)
    {
      return;
    }

    // 傾き状態を取得
    mType = MessageFormat.format(
      "x:{0},y:{1},z:{2}",
      event.values[0],
      event.values[1],
      event.values[2]);
  }

  @Override
  public String getValue()
  {
    return mValue;
  }
}

加速度センサーを利用する実装

// 加速度センサーを利用する実装
class Accelerometer implements IOrientationSensorListener, SensorEventListener
{
  private SensorManager mSensor;
  private String mValue;
  
  // C'tor
  public Accelerometer(SensorManager sm)
  {
    mSensor = sm;
    mSensor.registerListener(
      this,
      sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), // ★
      SensorManager.SENSOR_DELAY_UI);
  }

  // センサーの値が変化した際に呼び出される
  @Override
  public void onSensorChanged(SensorEvent event)
  {
    if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) // ★
    {
      return;
    }

    // 傾き状態を取得
    mType = MessageFormat.format(
      "x:{0},y:{1},z:{2}",
      event.values[0],
      event.values[1],
      event.values[2]);
  }

  @Override
  public String getValue()
  {
    return mValue;
  }
}
※飽くまでコンセプト。上記くらいの差異なら実装は一つにして、初期化パラメータでジャイロと加速度センサーを振り分けたほうが見通しが良さそう。

サポートしているフィーチャーの確認方法

一応。
PackageManager pm = getPackageManager();

// 加速度センサーのサポート状況
boolean supported
  = pm.hasSystemFeature(
      PackageManager.FEATURE_SENSOR_ACCELEROMETER);


画面の向きはPortraitが標準ではない

Tipsとは少し違うけど、タブレットで顕著になった画面の向きという要素に気をつける。Android主要端末の画面サイズ(small, normal, large, xlarge)でも触れたように、layoutのxmlを格納するディレクトリをport用およびland用に作成して対応すべき。



トラッキング

ユニークなインストール数の取得や、特定ユーザーの挙動をトラッキングする目的で使用できる識別子は2種類ある。また、前提として…

  • 工場出荷初期化を行っても変化しない値は使用すべきではない
  • それなりに高い精度でのトラッキングが目的であり、厳密に個人を識別する値と考えてはいけない


その1

工場出荷初期化を行うとクリアされる値であり、Root権限を取得している端末では書き換え可能な値なので個人を識別するものとして利用してはいけない。似ているけどSettings.System.ANDROID_IDではないので注意。

Settings.Secure.ANDROID_ID;


その2

数学的に重複する可能性が低いとみなすことのできるユニークな値。

UUID.randomUUID().toString();


Location Based Service

位置情報サービスを利用する際には常に電力消費の大きさが問題になる。また、位置情報を取得するまでの時間的なイニシャルコストも同様である。これらの問題を解決するためのTips

  • 前回取得した位置情報を活用する
  • 位置情報取得を1度だけ行う
  • 適切な頻度で位置情報をトレースする
  • 位置情報プロバイダの生死をハンドリングする
  • バックグラウンドで位置情報を取得しておく


前回取得した位置情報を活用する

利用可能な位置情報プロバイダが保有している位置情報を検査し、一定の条件を満たす有効なデータがあればこれを利用する。

List<String> providers = locationManager.getAllProviders();
for (String provider: providers)
{
  Location location
    = locationManager.getLastKnownLocation(provider);
  
  // locationの時刻を一定の閾値でテストして有効なものを利用する
  ...
}


位置情報取得を1度だけ行う

位置情報を追いかけ続ける必要性がなければ、可能なかぎり素早く現在の位置情報を取得するといった方法を採る。※但し、criteria(現在のデバイス状況など)に従って決定されるため、位置情報プロバイダは必ずしも素早いとは限らない。

locationManager.requestSingleUpdate(criteria, singleUpatePI);

また、上記はGingerbreadより前のAndroid OSでは利用できないため代替手段などを使う必要がある。



適切な頻度で位置情報をトレースする

例えば、頻度は毎15分、75m以上の移動があった場合など。

locationManager.requestLocationUpdates(
  AlarmManager.INTERVAL_FIFTEEN_MINUTES,
  75,
  new Criteria(),
  pendingIntent);


位置情報プロバイダの生死をハンドリングする

使用中のプロバイダが使えなくなった場合や、より良いプロバイダが利用可能になった場合についてブロードキャストレシーバを登録してハンドリングする。

// 位置情報プロバイダが利用できなくなったよーブロードキャストを
IntentFilter intentFilter
  = new IntentFilter(
      PlacesConstants.ACTIVE_LOCATION_UPDATE_PROVIDER_DISABLED);

// ハンドリングするレシーバを登録
registerReceiver(
  locProviderDisabledReceiver,
  intentFilter);


バックグラウンドで位置情報を取得しておく

UIの起動と同時に位置情報に依存した挙動を行う必要性がある場合には、サービスにおいて位置情報の取得を行っておく。但し、電力消費を考慮しFroyo(Android 2.2)以降では、Passive Providerを利用する。

locationManager.requestLocationUpdates(
  LocationManager.PASSIVE_PROVIDER,
  minTime,
  minDistabce,
  pendingIntent);

Honeycomb(Android 3.0)まとめ(その1)

7月4日に行われたAndroid Develoer Lab Private Sessionの内容について、備忘録を兼ねて要所をまとめてみる。ちなみに、内容は前日に行われたGoogle Android Developer Lab Tokyo 2011と同様らしい。

概要

  • SDKバージョンは11
  • Fragmentsなどの機能についての互換性は静的ライブラリの提供で保つ(Android Compatibility package)
  • Honeycombで追加したUIや機能は、スマートフォン版(2.x系)へ適用していく(Icecream Sandwich)

New Features

Honeycombで追加された新要素達は、こんな感じ。

  • Hardware Acceleration
  • Drag & Drop
  • Multi choice List
  • Bluetooth APIS
  • System wide clipboard
  • RenderSript
  • Loaders
  • App Widgets

ユーザーインターフェース特性

システムバー

Honeycombにおいて、画面最下部に常時表示されているインターフェースをシステムバーと呼称する。
ハードウェア的なボタンが存在しないタブレット端末では、ソフトウェア的にこのシステムバーを描画することにより、2.xと酷似したインターフェースを提供する。

システムバーの表示制御は下記のAPIで行うことができるが、描画領域を消す手段は提供されていない。ユーザーが混乱する(ホーム画面に戻る手段がなくなる等)ため必要な場合以外は常に表示することが推奨される。

View#SetSystemUiVisibility( STATUS_BAR_VISIBLE or STATUS_BAR_HIDDEN )

システムバーの右端には通知アイコンが表示される。通知アイコンをタップすることにより表示されるインターフェースが大幅に強化されているため、画像にあるようなメディアプレイヤー機能を通知領域のみで実現するといったことが可能になっている。



アクションバー

Honeycombにおいて、画面最上部に表示されているインターフェースをアクションバーと呼称する。
Android 2.xまでは、各デベロッパーが画面上部に配置するインターフェースを独自に実装してきた。

Honeycombでは多様なニーズに答えつつ統一されたユーザー体験を提供できるようにプラットフォームで画面上部のインターフェース(=アクションバー)を提供する。



Drag & Drop

大きな画面を有するタブレット端末ならではのインターフェースとして、Drag & Dropを標準APIとしてサポートする。



設計

Fragments

機能の集合体をFragmentと呼称し、この集合体の組み合わせによって様々なデバイスへの対応が可能になる。



見た目のデザイン

RenderScript

RenderScriptを利用するとハイパフォーマンスな3D描画処理をJNI無しで実装可能。



ハードウェアアクセラレーション

LiveWallpaperなどで利用されているGPUを使用した描画高速化を、通常のGUI部品にも適用できるようになった。

android:hardwareAccelerated


アニメーション

少量のコードでViewのプロパティを用いたアニメーションを行うことができる。

ObjectAnimator.ofFloat(myView, "alpha", 0f).start();

2011年6月26日日曜日

Java SE 7で何が変わるのか

来る2011年7月28日は、5年ぶりのJavaアップデート予定日。次期バージョンのJava SE 7にはいくかの新しい要素が含まれているので、先日のJJUG Cross Community Conferenceの振り返りを兼ねてまとめておく。

Java SE 7の主なトピックは以下の通り。

  • Project Coin (JSR 334)
  • NIO.2(JSR 203)
  • Fork/Join Framework
  • Invoke Dynamic (JSR292)

Project Coin

Project Coinとは、言語仕様な小さな変更を行うプロジェクト。

switch文でStringが使えるようになる

public void execute(String state)
{
  switch (state)
  {
  case "ready":
    startSomething();
    break;

  case "running"
    finishJob();
    brak;
  }
}

数値表現形式の追加

バイナリ表記(0bで書き始める)が追加された。

  • 1 (10進数表記)
  • 01 (8進数表記)
  • 0x1 (16進数表記)
  • 0b1 (バイナリ表記)

可読性向上を目的としたアンダースコア表記が追加された。

// クレカ番号など?
long data = 1234_5678_9012L;

マルチキャッチ

数種の例外処理をまとめることが可能になった。

try
{
  doSomething();
}
catch (IOException | SqlException e)
{
  e.printStackTrace();
}

例外再送の記述を簡易化

catch句にfinalを付けることで、メソッド定義のthrows部が不要になる。

public void execute() // throws IOException が不要
{
  try
  {
    doSomething();
  }
  catch (final IOException e)
  {
    throw e;
  }
}

Genericsインスタンス生成時の型推論強化

右辺では<>のみ記載すればOKになった。

// 今までの書き方
Map<String, String> data = new HashMap<String, String>();

// これからの書き方
Map<String, String> data = new HashMap<>();

try構文におけるリソース解放機能の追加

C#で言うところのusing的な感じ。自動的にclose()メソッドがコールされる。

try ( InputStream is = new FileInputStream("/tmp/data.txt") )
{
  doSomething();
}
catch (IOException e)
{
  e.printStackTrace();
}

Android主要端末の画面サイズ(small, normal, large, xlarge)

「drawable-mdpi」や「layout-normal」とか、時間が無い開発業務の中では、気にしたら負けかなと思う。


…なんてこともあるかもしれないけど、こういったところを気にしないと、アプリが安っぽく見えたり、機種によって使いやすさが違いすぎるといったことが発生する。

先に挙げたのは、普段から何気なく使っているリソースディレクトリのことであり、何となく名前から想像がつく通り、画面サイズや解像度に応じて別々のリソース(画像やら画面レイアウトやら)を格納しておくことができる。細かいことはあんざい先生の本に任せて、今回はざっくりした説明といくつかの主要端末についての表を主題とする。

ざっくり

レイアウトファイルを格納するディレクトリは、以下を使い分ければ大概OK(例:layout-normal-long-port)

  • small, normal, large, xlarge(Androidで独自に定めた基準)
  • long, no-long(横長か否か)
  • port, land(画面が縦の時、横の時)

画像ファイルを格納するディレクトリは、以下を使い分ければOK

  • ldpi, mdpi, hdpi, xhdpi

主要端末の画面サイズ表

手元に情報がある端末について、計算でそれぞれのAndroid OSにおけるサイズ情報を算出し表にしてみた。

モデルサイズ解像度
SO-01B(Xperia)normalhdpi
SO-01C(Xperia arc)normalhdpi
SO-02C(Xperia acro)normalhdpi
IS03normalxhdpi
IS04normalhdpi
SHI05(IS05)normalhdpi
SC-02B(Galaxy S)normalhdpi
SC-02C(Galaxy S II)normalhdpi
N-04C(MEDIAS)normalhdpi
N-06C(MEDIAS WP)normalhdpi
L-07C(Optimus bright)normalhdpi
X06HT(Desire)normalhdpi
HTC Legendnormalmdpi
MZ604(Xoom wifi)xlargemdpi
L-06C(Optimus Pad)xlargemdpi

但し、注意点として同じnormalであっても、微妙にサイズが異なるので注意が必要。

320x569(単位はdip)

  • SO-01B(Xperia)
  • SO-01C(Xperia arc)
  • SO-02C(Xperia acro)
  • IS04
  • SHI05(IS05)
  • N-06C(MEDIAS WP)

320x533(単位はdip)

  • SC-02B
  • SC-02C
  • X06HT
  • L-07C

320x480(単位はdip)

  • IS03
  • HTC Legend

※解像度からしてもIS03は、日本で発売されているAndroid端末の中では特殊な機体と言える。

スペック情報から該当するサイズ情報を割り出す

簡単に端末スペックから該当するサイズ情報を算出するフォームを作ってみた。

入力

Width:px
Height:px
Density:(ex 1.0~2.0)
Density Dpi:

結果

Width:dip
Height:dip
画面サイズ:
解像度: