Displaying posts filed under

プログラミング

Rails3でjQueryを使う場合の注意点

新規プロジェクトの作成時に -J オプションを付けてprototype.jsの読み込みをスキップした場合、config/application.rb の中で以下の設定が有効になる。

config.action_view.javascript_expansions[:defaults] = %w()

あとからjQueryを使うためにjquery-railsをインストールしたとき、この設定が有効なままだとビューの中で

javascript_include_tag :defaults

を書いてもjquery.js等が読み込まれない。

忘れずにコメントアウトしておく(コメントアウトすることで、jquery-railsが自動設定した値が有効になる)。

#config.action_view.javascript_expansions[:defaults] = %w()

あるいは、jqueryとapplication.js以外に常時読み込みたい.jsファイルがあるのなら、+= で配列に追加しておく。

config.action_view.javascript_expansions[:defaults]+= %w(mylib.js otherlib.js)

CouchDB 1.1.0をMac OS Xにインストール

公式WikiではHomebrewによるインストールが推奨されているので、brewコマンドで普通にインストールする。

brew install couchdb

Erlang R14B03 と couchdb1.1.0 がインストールされた。

この後Wikiの手順では

  • 手動でcouchdbユーザとcouchdbグループを作る
  • /usr/local 以下の所有者とパーミッションを変更して、/Library/LaunchDaemons/ に起動スクリプトをコピー
  • sudoを使ってcouchdbを起動/停止するaliasを設定

となっているが、brewで管理している /usr/local 以下の所有者を変更するのはちょっと嫌だし、どうせ開発用に使うだけだし、そもそもインストール後に表示されるメッセージには

If this is your first install, automatically load on login with:
    mkdir -p ~/Library/LaunchAgents
    cp /usr/local/Cellar/couchdb/1.1.0/Library/LaunchDaemons/org.apache.couchdb.plist ~/Library/LaunchAgents/
    launchctl load -w ~/Library/LaunchAgents/org.apache.couchdb.plist

と書いてあったので、起動スクリプトは ~/Library/LaunchAgents/ にコピーして、次のようなエイリアスを設定することにした。

alias restart_couch='launchctl stop org.apache.couchdb'
alias start_couch='launchctl load -w ~/Library/LaunchAgents/org.apache.couchdb.plist'
alias stop_couch='launchctl unload ~/Library/LaunchAgents/org.apache.couchdb.plist'

起動したり停止したりするのに sudo は不要。

start_couch
#起動
 
curl http://localhost:5984/
# =>{"couchdb":"Welcome","version":"1.1.0"}
 
stop_couch
#停止

設定ファイル等は /usr/local/etc/couchdb/ にある。

IBOutletで接続した変数はreleaseする必要があるのか

iOSとMac OS Xとで結論が異なる。OS X開発経験者が書いた書籍やWebサイトはこの違いを考慮していない場合があるので注意が必要。

……

IBOutletの宣言はインスタンス変数にもプロパティにも書くことができる。

@interface IBOutletTestAppDelegate : NSObject <UIApplicationDelegate> {
    UILabel *_retainLabel;
    UILabel *_assignLabel;
    IBOutlet UILabel *_variableLabel;
}
 
@property (nonatomic, retain) IBOutlet UILabel *retainLabel;
@property (nonatomic, assign) IBOutlet UILabel *assignLabel;
@end

iOSの場合、IBOutletをどちらに書いても、アウトレットの接続は常にNSObject(NSKeyValueCoding)の setValue:forKey: メソッドを使って行われる。このメソッドは(1)まず対応するプロパティのsetterを探して、(2)見つからない場合はインスタンス変数に代入してretainカウントを1増やす。よってassignプロパティを使うのでない限り、アウトレットで接続した変数は常にreleaseしなければならない

この動作は setValue:forKey: をオーバーライドしてみれば確認できる。

- (void)setValue:(id)value forKey:(NSString *)key
{
    //retainのプロパティとインスタンス変数の場合は
    //retainCountが1増える
    NSLog(@"[before setValue] %@ %d", key, [value retainCount]);
    [super setValue:value forKey:key];
    NSLog(@"[aflter setValue] %@ %d", key, [value retainCount]);
}

ただしAppleが提供するGuideでは常にプロパティを使うことを推奨している(Memory Management Programming Guideの『Memory Management of Nib Objects』など)。プロパティを使うことでretain/assignのポリシーが明確にできるし、retain/assignどちらの場合もviewを破棄する際にはnilを代入するだけで良い。特にretainの場合はreleaseとnil代入の操作を一度にできるので便利である。

……

Nibファイルによるオブジェクトの生成と、生成されたオブジェクトのretain状況についてはResource Programming Guideの『The Nib Object Life Cycle』にまとめられている。これを読むとOS Xの方が(GCを使わない場合)状況が複雑だということがわかる。

簡単にまとめると

iOSの場合

  • 生成されたオブジェクトは全てautorelease済みである
  • アウトレットの接続は setValue:forKey: メソッドで行われる

OS X(GCなし)の場合

  • 生成されたオブジェクトは、トップレベルのオブジェクトを除いてautorelease済みである。
  • アウトレットの接続は、(1)まずset{OutletName}:というメソッドを探して、(2)見つからない場合はインスタンス変数にretainカウントを増やさずに代入される

OS X(GCなし)の場合、トップレベルのオブジェクとそれ以外のオブジェクトとで扱いが異なるので、一貫したポリシーを立てるのが難しくなっている。もしretainのプロパティを使った場合、トップレベルのオブジェクトは2回releaseしなければならない。もしインスタンス変数に直接代入した場合、トップレベルのオブジェクトに限りreleaseしなければならない。

External Objectを使ってIBOutletからNibファイル外のオブジェクトに接続する

External Objectを使うことで、Nibファイル外のオブジェクトに対してIBOutletで接続できる。ちなみにこの機能はMac OS Xでは使用できず、iOSでのみ使える。

……

たとえばあるカスタムビューからモデルのインスタンスを参照したいとする。このビューでは常にモデルの状態を反映した表示を行いたいので、インスタンス変数にモデルへの参照を保持しておきたい。

ビューの生成・初期化をプログラム中で行うなら、initWithModel:のような初期化子を用意しておけば済む。しかしビューをNibファイルから生成する場合は初期化時にモデルを参照することができない(モデルは普通Nibファイルで生成しないので)。仕方がないのでモデルのインスタンスだけはUIViewControllerのviewDidLoadでビューに代入して、専用の初期化メソッドを呼び出すなどするしかない。これは非常に面倒だし忘れやすい。

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    //プロパティに代入してから表示を更新。面倒くさい
    _customView.model = _model;
    [_customView update];
 }

そこでExternal Objectの出番である。このオブジェクトはNibファイル外の既成のオブジェクトに対するPlaceholderとして機能する。ちょうど自由な名前をつけられる「File’s Owner」だと考えれば良い。

使い方は次のようになる。まずカスタムビューの.hファイルで、モデルに対するOutletを設定しておく。

@interface CustomView : UITableViewCell
{
    Model *_model;
}
 
@property (nonatomic, retain) IBOutlet Model *model;

モデルを使った初期化処理は、その他の初期化処理と同じくawakeFromNibの中に書くことができる。

- (void)awakeFromNib
{
    [super awakeFromNib];
 
    //モデルのデータを使って表示を初期化する
    _textField.text = _model.name;
}

続いてxibファイルを開いてExternal Objectの設定を行う。Libraryの中から「External Object」を選んでObjectsの欄にドラッグアンドドロップすると、Placeholdersの欄にオブジェクトが追加される。

追加されたオブジェクトを選択してIdentity inspectorを開き、「Custom Class」の中でモデルクラスのクラス名を指定する。

次にAttributes inspectorを開き、Identityの欄にxibファイル中で一意な識別子を設定する。この識別子が実際のオブジェクトを割り当てる際のキーになる。

あとは通常のOutlet接続と同じ操作で、カスタムビューのOutletプロパティからPlaceholders内のモデルに対して接続を行っておく。

これでxibファイルの準備ができたので、あとはNib読み込みの際にExternal Objectとして参照されるオブジェクトの実体を指定する。これにはNSBundleクラスの loadNibNamed:owner:options: メソッドの UINibExternalObjects オプションを使う。

Nibファイルの読み込みはUIViewControllerのloadViewメソッで行うのが良いだろう。以下のサンプルコードではコントローラがモデルのインスタンスを変数 _model に保持しており、xibファイル内でExternal Objectには Model というidentityを割り当てたと想定している。YourNibFileNameは読み込むNibの名称。

- (void)loadView
{
    NSArray *topLevelObjs = nil;
    NSDictionary *proxies = [NSDictionary dictionaryWithObject:_model forKey:@"Model"];
    NSDictionary *options = [NSDictionary dictionaryWithObject:proxies forKey:UINibExternalObjects];
    topLevelObjs = [[NSBundle mainBundle] loadNibNamed:@"YourNibFileName" owner:self options:options];
 
    if ([topLevelObjs count] == 0) {
        NSLog(@"Warning! Could not load nib file.\n");
        return;
    }
}

これによって特別な初期化操作をしなくても、Nibファイル内のカスタムビューとコントローラとで一つのモデルのインスタンスを共有することができる。

参考: Resource Programming Guide

ImageStore(改造版)を使って画像を非同期ダウンロードする

ImageStoreはiOSで画像を非同期ダウンロードするためのObjective-Cライブラリです。Satoshi Nakagawaさんがオープンソースのライブラリとして公開されています(New BSDライセンス)。手軽に使えて非常に便利なライブラリなのですが、そのままアプリに組み込むには少々難があります。

  1. ダウンロードをキャンセルする手段がない
  2. ImageStoreのインスタンスを破棄する以外に、キャッシュをクリアする方法がない
  3. 正常に画像がダウンロードできたかどうか判定する術がない

1がないので、ダウンロード中に画面移動が発生したりすると容易にクラッシュします。2がないので、随時キャッシュを消して再ダウンロードしたり、メモリ不足時にキャッシュを削除したりといったことができません。3のため、サーバが404を返した場合等にはリクエストが無限ループしてしまう危険があります(ImageStoreの中に含まれるサンプルアプリではそういう仕様になってしまっています)。また画像が取得できなかった時に代替画像を表示するといった処理も実装できません。

そこでオリジナル版からforkして、これらの不具合を解消した改造版を作りました。

https://github.com/tkyk/imagestore

(4月6日追記: 私の改造版とは使い方が異なりますが、本家ImageStoreでも同等の修正が行われました)

ImageStoreクラスには4つのメソッドが増えました。どんなメソッドかは名前から想像がつくと思います。

- (void)cancelAllDownloads;
- (void)cancelDownloadFromUrl:(NSString*)url;
- (void)clearAllCaches;
- (void)clearCacheForUrl:(NSString*)url;

ImageStoreDelegateにも新たにメソッドが宣言されています。

- (void)imageStoreDidFailNewImage:(ImageStore *)sender
                              url:(NSString *)url
                    fallbackImage:(UIImage**)anImage;

使い方

UITableViewの各Cellに画像を表示する処理を例に、使い方を説明していきます。現実のiPhoneアプリの開発で良くあるパターンだと思います。

まず tableView:cellForRowAtIndexPath: で UITableViewCell の表示を更新するときにgetImageを呼び出します。画像が既にキャッシュされていればその場でUIImageのインスタンスが返却され、キャッシュがなければnilが返されて非同期の画像ダウンロードが開始されます。

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell;
    //...略...
 
    //このindexPathに対応する画像のURL
    NSString *imageURL = [self getImageURLAtIndexPath: indexPath];
 
    //getImageにURLを渡す
    cell.imageView.image = [imageStore getImage: imageURL];
 
    return cell;
}

画像が正常に取得できるとImageStoreDelegateの imageStoreDidGetNewImage:url: が呼ばれるので、reloadDataを呼び出してセルの表示を更新します。これによって再度getImageが呼ばれ、キャッシュから画像が取り出されます。

- (void)imageStoreDidGetNewImage:(ImageStore *)sender url:(NSString *)url
{
    [self.tableView reloadData];
}

ここまではオリジナル版のImageStoreと同じ流れです。

しかしオリジナル版では画像の取得に失敗した場合にもこのメソッドが呼ばれてしまうため、リクエストが(成功するまで)無限にループしてしまいます。そこで私の改造版では画像の取得に失敗した場合、具体的には

  • ステータスコードが200番台では無い
  • MIMETypeがimage/*ではない
  • UIImageのinitWithDataがnilを返した

こういった場合、 代わりに imageStoreDidFailNewImage:url:fallbackImage: が呼び出されます。

// 代替となる画像(fallbackImage)を事前に読み込んでおく
- (id)initWithCoder:(NSCoder *)aDecoder
{
    //..略
    fallbackImage = [[UIImage imageNamed:@"no_image"] retain];
    return self;
}
 
// 読み込み失敗時にはこのメソッドが呼ばれるので、
// fallbackImageを代替画像として使用
- (void)imageStoreDidFailNewImage:(ImageStore *)sender
                              url:(NSString *)url
                    fallbackImage:(UIImage **)anImage
{
    *anImage = fallbackImage;
}

第一引数はImageStoreのインスタンス、第二引数は失敗したURLで、第三引数は代替画像(UIImageのインスタンス)に対するポインタです。このポインタに非ゼロの値をセットしておくと、その画像が代わりにキャッシュされた上で imageStoreDidGetNewImage:url: が呼び出されます。つまり「正常に取得できなかった」という結果をキャッシュすることができる、ということになります。

キャッシュのクリア

一般に、didReceiveMemoryWarningでは全画像キャッシュをクリアしておくべきでしょう。

- (void)didReceiveMemoryWarning
{
    [imageStore clearAllCaches];
    [super didReceiveMemoryWarning];
}

その他に、ダウンロードに失敗した画像のURLを記録しておいて、「再読込」操作時にキャッシュをクリアして再リクエストする、といった用途も考えられます。

参考: ImageStoreのコードリーディング – haoyayoi Dev Style

ダウンロードのキャンセル

多くの場合、viewWillDisappear:のタイミングで全ての画像のダウンロードをキャンセルするのが良いだろうと思います。

- (void)viewWillDisappear:(BOOL)animated
{
    [imageStore cancelAllDownloads];
    [super viewWillDisappear:animated];
}

ただしこのままだとダウンロード中に別の画面に移動した場合、ダウンロードが完了していなかった画像がそのままになってしまうので、戻ってきたときに自動でダウンロードが再開されるよう、viewWillAppear:でセルの表示を更新する必要があります。

- (void)viewWillAppear:(BOOL)animated
{
    [self.tableView reloadData];
    //またはvisibleCellsを辿って表示を更新するなど
    //for(UITableViewCell *cell in [self.tableView visibleCells])
    [super viewWillAppear:animated];
}

iOS開発におけるパターンによるオートマティズム レビュー

新しい知識を与えてくれる本ではない。ページ数も200ページ弱しかない。ある程度のiOS開発経験者なら、初めて知る内容はほとんど無いだろう。しかし数多の知識よりも遥かに価値ある知見を提供してくれる本だ。

以前も書いたが、iOS初学者が入門書を終えた後で「知識」を増やすには、下手に本を読んだりWebを漁ったりするより、Appleが提供している公式のプログラミングガイドを読むのが一番いい。フレームワークの各コンポーネントにどんな機能があり、どんな使い方があるか、他のどんな文書より網羅的に知ることができる。

しかし100通りのやり方を知っていても、実際にアプリケーションを作る時には、その中からたった1通りのやり方を選ばなければならない。その時その状況にあったベストなやり方を選ぶのはとても難しい。単に知識を増やすだけでは不十分で、フレームワークの設計思想や、作ろうとしている機能の性質についての深い理解、そして何より多くの経験・試行錯誤に基づく知見が必要だ。

この本が提供しようとしているのがまさにそれである。「いろいろ他の方法も考えられるが、この方法がベストである。なぜなら…」と本書は説く。プログラマの三大美徳としての”傲慢さ”に溢れた本である。著者を深く尊敬せずにはいられない。

というわけで、私もその傲慢さに倣って、いくつかの内容について強化/改善案を提案したい。

メモリ管理のパターン[4]について

この本では「インスタンス変数はretainし、releaseした後は常にnil代入する」という原則が貫かれている。とても合理的で分かりやすく、早速真似させてもらったのだが、deallocメソッドの中で同じ形の式が繰り返されるのがちょっと見苦しい。

- (void)dealloc
{
    [_identifier release], _identifier = nil;
    [_title release], _title = nil;
    [_link release], _link = nil;
    [_itemDescription release], _itemDescription = nil;
    [_pubDate release], _pubDate = nil;
 
    [super dealloc];
}

Xcodeの補完機能は優秀なので、こういった繰り返しを書くこと自体はさほど苦ではないのだが、個人的に見た目が(生理的に)我慢できないので、次のようなマクロを書いた。

#define DESTROY(x) [x release], x = nil
 
- (void)dealloc
{
    DESTROY(_identifier);
    DESTROY(_title);
    DESTROY(_link);
    DESTROY(_itemDescription);
    DESTROY(_pubDate);
 
    [super dealloc];
}

これで機械的な繰り返しは消え、よりタイプしやすく、読みやすくなる。「この変数の内容を破棄する」という意図もはっきりする。

メンバ変数とプロパティの命名規則について

特にパターンとして明示されてはいないのだが、この本では「インスタンス変数には _ を接頭辞として付け、_ を取り除いた名前をプロパティ名とする」という規約を採用している。これも大変優れた命名規則で、自分でも真似させてもらったのだが、プロパティをsynthesizeする時の、下のような機械的な繰り返しが例によって我慢ならない。

@synthesize identifier = _identifier;
@synthesize read = _read;
@synthesize title = _title;
@synthesize link = _link;
@synthesize itemDescription = _itemDescription;
@synthesize pubDate = _pubDate;

というわけで次のようなマクロを書いた。

#define PROPVAR(x) x = _ ## x
 
@synthesize
  PROPVAR(identifier),
  PROPVAR(read),
  PROPVAR(title),
  PROPVAR(link),
  PROPVAR(itemDescription),
  PROPVAR(pubDate);

セルのパターン[4]について

カスタムセルのサブビュー、特にUILabelに対して自前でハイライト処理が必要とあるが、実際にはhighlightedプロパティを持つサブビューには自動でハイライト処理が行われるので、自分で処理する必要はない。『Table View Programming Guide for iOS』より引用。

And if the cell is selectable, make sure that the cell content is highlighted appropriately when selected; this happens automatically if the subview implements (if appropriate) the accessor methods for the highlighted property.

iOSでJSONを扱う(YAJL使用)

環境: iOS 4.3, Xcode 4

iOS(iPhoneアプリ)でJSONを扱うライブラリとしてはJSON Frameworkが有名だが、敢えてyajlのObjective-Cバインディングを使ってみた。

インストール

プロジェクトページののREADMEに従ってインストールする。

1. githubのダウンロードページから YAJLiOS-{version}.zip をダウンロードして、展開してできた YAJLiOS.framework ディレクトリを自分のプロジェクトのFrameworksの中にドラッグ&ドロップして追加する。このとき『Copy items into destination group’s folder (if needed)』にチェックを入れておく。

2. XcodeでBuild Settingsを開き、「Other Linker Flags」に -ObjC-all_load の2つのフラグを追加する。

3. JSONを使いたいファイルでヘッダファイルをimportする。

#import <YAJLiOS/YAJL.h>

ドキュメントのインストール

ドキュメントは http://gabriel.github.com/yajl-objc/から参照できるが、Xcodeにインストールすることもできる。

1. githubのダウンロードページからYAJL-{version}.docset.tgzをダウンロードして、展開してできたYAJL.docsetを

~/Library/Developer/Shared/Documentation/DocSets/

の中にコピーする(このディレクトリはデフォルトでは存在しないので、自分で作成する)。

Xcodeを再起動すると、HelpのDocumentationから参照できるようになる。

使い方

JSONのデコード

NSData および NSString として表現されたJSONは yajl_JSON: メソッドで直接デコードできる。

NSError *error;
id result = [data yajl_JSON:&error];
 
if (error) {
    NSLog(@"JSON Parse Error: %@", [error localizedDescription]);
}

エラー引数なしの yajl_JSON の方を使う場合は@try…@catchによるエラー処理を忘れないこと。さもないと不正なJSONをパースしただけでアプリがクラッシュする。

@try {
    result = [data yajl_JSON];
}
@catch (NSException *exception) {
    if ([exception name] == YAJLParserException) {
        NSLog(@"JSON Parse Error: %@", exception);
        result = nil;
    } else {
        @throw;
    }
}

その他、SAXに似たストリーミング形式でのパースも行えるらしい。詳しくはドキュメント参照。

ちなみにarrayやobjectの中の”null”は [NSNull null] に、むき出しの”null”はnilに変換されるようだ。

[[@"[null]" yajl_JSON] objectAtIndex:0] == [NSNull null];
[@"null" yajl_JSON] == nil;

JSONへのエンコード

NSArray, NSDictionary, NSNumber, NSString, NSNull のインスタンスからは yajl_JSONString メソッドで直接JSONにエンコードできる。

NSArray *arr = [NSArray arrayWithObjects:
                @"abc",
                [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:3] forKey:@"key"],
                [NSNull null],
                nil];
NSLog(@"%@", [arr yajl_JSONString]);
//["abc",{"key":3},null]

その他のクラスについては、 -(id)JSON というメソッドを定義して上記5種類のいずれかのインスタンスを返すようにしておけば、やはりyajl_JSONStringでJSON化できる(未定義の場合は例外がthrowされる)。詳しくはドキュメントのYAJLCodingプロトコルの説明を参照。

Xcode4でiOSのドキュメントが消えてしまった場合の対処法

Xcodeのバージョンを4に上げた後、iOSのリファレンスが参照できなくなった。

HelpでiOSが選べなくなった

復活させるにはメニュー「Xcode」→「Preferences」→「Documentation」を選んで、「iOS 4.3 Library」の右側に表示されている「GET」ボタンを押すと、435MBほどのドキュメントがダウンロードされ、使用可能になる。

『基礎からのiOS SDK』正誤情報

基礎からのiOS SDK』の中で間違っている・問題のある部分をまとめました。今までこのブログに書いてきた内容と大半は重複しますが、可能な限り客観的に問題があると証明できるものだけを選び出しています。書籍サイトの問い合わせフォームから同一内容を送っておいたので、いずれは公式に正誤情報としてまとめられるかもしれません(その際にはこのエントリは削除します)。

全文は「続き」からどうぞ。

(続きを読む…)

jQuery 1.5の新機能: jQueryのサブクラスが作れるようになる

jQuery 1.5 beta1 のリリースノートを見ていたら面白そうな機能があったので紹介。

1.5正式版では jQuery.sub という名称に変更になったので、本文およびサンプルコードを修正しました。

jQuery.sub() でjQueryのサブクラスを作ることができる

var MyjQuery = jQuery.sub();

作成したサブクラスには独自のメソッドを定義することができる。定義の仕方、使い方は通常のjQueryと全く同じ。

MyjQuery.fn.writeHello = function(){ this.text('Hello World'); };
 
MyjQuery('p').writeHello();

通常のjQueryメソッドはサブクラスでもそのまま使える。サブクラスを作った後に追加したメソッドも使えるので、プラグインによる拡張と組み合わせても大丈夫。

MyjQuery('div').css('border', '1px red solid').writeHello();
 
//元のjQueryを拡張
jQuery.fn.pluginMethod = function(){ alert('OK'); };
 
//OK
MyjQuery('.klass').pluginMethod();

逆にサブクラスで追加したメソッドは元のjQueryからは見えないので、プロジェクトやプラグインに固有なメソッドを定義したり、元のjQueryにあるメソッドを上書きしたりといったことが安全にできる。

// text	メソッドを上書きする
MyjQuery.fn.text = function(val) {
  var orig = jQuery.fn.text;
  if(typeof val === "string") {
    return orig.call(this, "!!"+ val +"!!");
  } else {
    return orig.apply(this, arguments);
  }
};
 
// 上書きされたtextを呼び出す
MyjQuery('#ex').text("foo");
 
// 引数を与えなければ元のメソッドが呼ばれる
MyjQuery('#ex').text();	//!!foo!!
 
// もちろん元のメソッドには影響ない
jQuery('#ex').text('foo'); //foo

さらに応用として、プラグインで拡張メソッドを提供する場合の名前空間としても使えるかもしれない。例えば私が前に作った動的にSELECTを構築するためのjQueryプラグインは、グローバルなメソッド空間に2つのメソッドを追加してしまうのであまりマナーが良くない。

$('select').addOption(...);
$('select').clearOptions(...);
 
//本当は次のようにしたいが、動作しない
$('select').optionBuilder.addOption(...);
$('select').optionBuilder.clearOptions(...);
 
//次のような実装が推奨されている
$('select').optionBuilder('addOption', ...);
$('select').optionBuilder('clearOptions', ...);

1.5のサブクラス機能を使えば、次のように隔離されたサブクラスでのみ使用可能なメソッドとして提供することができる。

//サブクラスを使えば直接呼び出せる
var optionBuilder = jQuery.optionBuilder();
optionBuilder('select').addOption(...)
optionBuilder('select').clearOptions(...);
 
//関数の中に隔離する方法も
jQuery.optionBuilder(function($){
    $('select').addOption(...);
    $('select').clearOptions(...);
});

実装は次のような感じになるだろう。

/*
 * jQuery.optionBuilder.fn[method](...);
 * を
 * jQuery.fn.optionBuilder(method, ...);
 * にマップする
 */
jQuery.optionBuilder = (function(){
  var sub = jQuery.sub(),
      base = jQuery.fn.optionBuilder,
      slice = Array.prototype.slice,
      methods = ['addOption', 'clearOptions'];
 
  $.each(methods, function(i, method){
    sub.fn[method] = function(){
      return base.apply(this, [method].concat(slice.call(arguments)));
    };
  });
  return function(fn){
    if(typeof fn === 'function') {
      return fn(sub);
    }
    return sub;
  };
}());

参考: