MoyaSystem

もやしです。

JavaScriptにクラスはありません - プロトタイプで継承編 -

前回の内容はこちら。
JavaScriptにクラスはありません - オブジェクトリテラルとコンストラクタパターン編 - - MoyaSystem

方法3: プロトタイプを拝借する

クラスのないJavaScriptに用意されている、オブジェクト間でコードを再利用する方法の一つがプロトタイプです。同一のプロトタイプを持つオブジェクトでは、プロトタイプに定義されているプロパティにアクセスすることができます。
以下のコードではObject.create関数を使って、共通のプロトタイプを持つオブジェクトを生成しています。第1引数はプロトタイプ、第2引数はオブジェクト固有のプロパティとなります。

    var Person = function Person(){};
    Person.prototype.sayName = function(){
    	console.log("I am " + this.name);
    };
    Person.prototype.sayHo = function(){
    	console.log("Ho!");
    };

    var Singer = function Singer(){};
    Singer.prototype = new Person(); // Personのプロトタイプを継承
    Singer.prototype.sing = function(){
    	console.log(this.lyric);
    };

    var John = Object.create(Person.prototype,{
    	name: {value: "John"},
    	lyric: {value: "Imagine there's no heaven..."}
    });
    John.sayName(); // "I am John"
//  John.sing(); // undefined error

    var Bob = Object.create(Person.prototype,{
    	name: {value: "Bob"}
    });
    Bob.sayName(); // "I am Bob"

    John.sayHo = function(){
    	console.log("HOOOOOOOOOOOOO");
    };
    John.sayHo(); // "HOOOOOOOOOOOOO"
    Bob.sayHo(); // "Ho!"

    var LadyGaga = Object.create(Singer.prototype,{
    	name: {value: "Gaga"},
    	lyric: {value: "lalalala lalalala"}
    });
    LadyGaga.sayName(); // "I am Gaga"
    LadyGaga.sayHo(); // "Ho!"
    LadyGaga.sing(); // "lalalala lalalala"

Personを空のオブジェクトとして定義し、共有したいプロパティはすべてPerson.prototypeに定義するのがキモです。LadyGagaはPersonオブジェクトのプロトタイプを拡張したSingerオブジェクトとして作成しています。Singer.prototypeにPersonオブジェクトを代入することでPerson.prototypeにもアクセスすることができるようになります。ここでPerson.prototypeを代入してしまうと、Singer.prototypeで拡張した要素にPersonオブジェクトもアクセスできるようになる(John, Bobもsingできるようになる)ため、間違えないように注意してください。
ということで、Personオブジェクトを継承したSingerオブジェクトを作ることができました!

もう一工夫

継承ができたところで、クラスベースのオブジェクト指向言語でいうところのstatic変数っぽいものを作ってみます。ここではPersonオブジェクトすべてに付与されるmyIdプロパティを作成し、Personオブジェクトが作成されるたびにインクリメントされるようにしてみます。

    function Person(){};
    Person.prototype.sayName = function(){
    	console.log("I am " + this.myName);
    }
    Person.prototype.sayId = function(){
    	console.log("My Id is " + this.myId);
    }

    // ファクトリ
    var PersonFactory = function(){
    	var nextId = 0; // static変数的なもの
	return function(name){
		nextId++;
		return Object.create(
		Person.prototype,{
			myName: {value: name},
			myId: {value: nextId}
	         }
	    );
	};
    };

    var pfactory = PersonFactory();

    var John = pfactory("John");
    var Bob = pfactory("Bob");
    var Mary = pfactory("Mary");

    John.sayName(); // I am John
    John.sayId(); // My Id is 1

    Bob.sayName(); // I am Bob
    Bob.sayId(); // My Id is 2

    Mary.sayName(); // I am Mary
    Mary.sayId(); // My Id is 3

オブジェクトやプロトタイプそのものにmyIdをもたせるのはうまくいかないので、ファクトリメソッドを作成してその中で管理するようにしました。pfactory関数が実行されるたびにnextIdがインクリメントされます。

気をつけるべきこと

継承に頼り過ぎない

オライリーJavaScriptパターン』でも述べられていますが、継承はコードを再利用するための一つの方法にすぎません。このコード再利用したいなーと思ったときに、どんな実装をすれば効果的に再利用できるのか、引き出しをたくさんもっていることが重要だと思います。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

パフォーマンスに気を配る

以下のリンク先でも触れられているように、プロトタイプを利用するのにはパフォーマンス改善という意味もあります。

例えば、新しくオブジェクト/クラスを作成する時、一般的にメソッドはオブジェクトのコンストラクタの中で定義するのではなく、オブジェクトのプロトタイプに結びつけるべきです。コンストラクタの中で定義してしまうと、コンストラクタが呼び出されるたびに (つまりオブジェクトが作成されるたびに) メソッドが再代入されてしまうことになるからです。


クロージャ - JavaScript | MDN

JavaScriptにクラスはありません - オブジェクトリテラルとコンストラクタパターン編 -

そもそも

今月頭にこの記事を見て、げっと思ったので……。

Class構文が実装された - JS.next

JavaScriptにクラスは存在しません!!!にもかかわらず、class構文なるものを用意するのは余計に初学者を混乱させるだけだと思います。個人的な復習も兼ねて、オライリーJavaScriptパターン』に載っているオブジェクト作成とコード再利用のパターンから、自分なりに内容を噛み砕いてまとめてみました。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

方法1: オブジェクトリテラルを使う

クラスはオブジェクトを作るひな形でありオブジェクトではありません。JavaScriptではクラスがないため、オブジェクトはすべて既存のオブジェクトのコピーになります。

    var Person = {
    	myName: "noname",
    	sayName: function(){
    		console.log("I am " + this.myName);
    	}
    };

    Person.sayName(); // "I am noname"
    var John = Person;
    var Bob = Person;    
    John.sayName(); // "I am noname"
    Bob.sayName();  // "I am noname"

Personはクラスではなくオブジェクトです。John、BobはPersonと同じプロパティ(myName, sayName)を持っていますが、これはPersonオブジェクトのコピーだからです。しかしこのやり方には問題があります。

    John.myName = "John";
    John.sayName(); // "I am John"
    Bob.sayName(); //"I am John"
    Person.sayName(); //"I am John"

Johnのプロパティを書き換えるとPersonやBobのオブジェクトも書き換わってしまいます。これは期待した動作ではないので工夫が必要になります。

方法2: コンストラクタパターンを使う

次はPersonにオブジェクトリテラルではなく関数(コンストラクタ)を代入して使ってみます。

    var Person = function Person(myName){
    	this.sayName = function(){
    		console.log("I am " + myName);
    	};
    	this.sayHo = function(){
    		console.log("Ho!");
    	}
    };

    var John = new Person("John");
    var Bob = new Person("Bob");
    John.sayName(); // "I am John"
    John.sayHo(); // "Ho!"
    Bob.sayName(); // "I am Bob"
    Bob.sayHo(); // "Ho!"

    John.sayHo = function(){
    	console.log("HOOOOOOOOOOOOO!!!!");
    };
    John.sayHo(); // "HOOOOOOOOOOOOO!!!!"
    Bob.sayHo(); // "Ho!"

    var LadyGaga = new Person("Gaga");
    LadyGaga.sing = function(){
    	console.log("lalalala lalalala");
    };
    LadyGaga.sayName(); // "I am Gaga"
    LadyGaga.sayHo(); // "Ho!
    LadyGaga.sing(); // "lalalala lalalala"

newキーワードでJohnとBobのオブジェクトを生成することができました。両者ともPersonで定義されているプロパティにアクセスでき、一方のプロパティを書き換えたとしても他方に影響をあたえることはありません。
では最後に出てきたLadyGagaのように、Personのプロパティを継承しつつ、新たなプロパティ(sing)を追加したオブジェクトを生成したい場合はどうすればよいでしょうか。この例ではLadyGagaオブジェクトを作った後にsingプロパティを追加していますが、これはなかなか見通しが悪いです。
といったときにでてくるのがプロトタイプなのですが、長くなりそうなので続きは次回。

Titanium.UI.WebViewのreload()が動かない時の対処法

そもそも

Titanium Studio で開発したiPhoneアプリにて、WebView.reload()がうまく動かないという不具合が発生した。画面をリロードしようとしても真っ白になってしまい、何も読み込まれない。時々はうまくいくようだが、なにぶん動作が不安定すぎる。

解決方法

なんだかなあ、て感じだがこれで解決した。

var url = $.webview.getUrl();
$.webview.setUrl(url);

urlプロパティが書き換えられた瞬間に画面が読み込まれるようなので、一度現在のURLを変数に保存して再度設定しなおしてやればよい。
reloadメソッドもおんなじことをやってるだけかと思ってたのだが違うのかな?まぁ解決したのでよしとします。

Windowsの%date%に足元すくわれた話

そもそも

個人的に運用している某所の出退勤管理システムにて、実績を取得したい期間を任意に指定できる機能を実装した。*1それなら毎月初に先月1ヶ月分の実績を取得してメール送信するバッチ処理もこの機能を呼ぶようにしたいよね、ということで、.batファイルの中で %date% 変数で日付を呼び出し、それをゴニョゴニョ処理するように変更をかけた。

そしたら

エラーでコケました。

原因

開発環境の%date%の出力結果は

2014/10/01

だったのだが、本番環境は海外のサーバだったため、

Wed 01-10-2014

と出力されてあえなく轟沈した次第……。がっくし。

解決

Change the display of dates, times, currency, and measurements - Windows Help
ここを参考にして本番サーバ側のdateフォーマットを変更して解決。うーん、本番環境でテストするのは難しいとはいえ、こういう環境依存の部分があることは認識しておかないとダメですね。反省。

*1:いや今まで無かったのかよ、というツッコミはやめてください。

BuddyPressを前提としたプラグインのPHPUnit環境を構築する

PHPUnit使うのも初めてなのに、WordPress環境やらBuddyPress環境やらを整備しないといけなくってだいぶ時間がかかってしまった。備忘録的に手順まとめ。OSはMax OS Xを前提としています。

Wordpress Command line interface をインストールする

以下のリンクを参照。
Command line interface for WordPress | WP-CLI

WordPressテスト環境、およびプラグインテストひな形ファイルを作成

以下のリンクの手順に従って、WordPressユニットテスト用の環境と、プラグインテストのひな形ファイルを作成する。
Plugin Unit Tests · wp-cli/wp-cli Wiki · GitHub
自分の環境では 3)のテスト環境作成の時に「データベース接続エラー」で怒られた。結局rootとは別に管理者ユーザを作成することで解決。なぜ怒られたか理由はわからない。
ここまでやると /tmp/wordpress 以下にテスト用のWordPress環境、/tmp/wordpress-tests-lib 以下にテスト用ライブラリ一式ができあがっているはず。

BuddyPressのユニットテスト用環境を作成

通常のBuddyPressプラグインにはテスト用ソースが含まれていないので、以下のリンクの手順に従ってダウンロードする。ダウンロード先は /tmp/wordpress/wp-content/plugins
Automated Testing · BuddyPress Codex

環境変数 WP_TESTS_DIR は tmp/wordpress-tests-lib に設定しておく。

プラグインユニットテストについてbootstrap.phpを編集

以下を参照しつつbootstrap.phpを書き換える。
Writing automated tests for BuddyPress-dependent plugins · BuddyPress Codex
が、開発環境のbuddypressフォルダ下にテスト用ソース一式を置くような感じになっているので気持ち悪い。ので一部編集。完成品がこちら。

<?php
$_tests_dir = getenv('WP_TESTS_DIR');
if ( !$_tests_dir ) $_tests_dir = '/tmp/wordpress-tests-lib';
require_once $_tests_dir . '/includes/functions.php';

// The BP_TESTS_DIR constant is a helpful shorthand for accessing assets later
// on. By defining in a constant, we allow for setups where the BuddyPress
// tests may be located in a non-standard location.
if ( ! defined( 'BP_TESTS_DIR' ) ) {
    define( 'BP_TESTS_DIR', '/tmp/wordpress/wp-content/plugins/buddypress/tests/phpunit' );
}

// Checking for the existence of tests/bootstrap.php ensures that your version
// of BuddyPress supports this kind of automated testing
if ( file_exists( BP_TESTS_DIR . '/bootstrap.php' ) ) {
    // The functions.php file from the WP test suite needs to be defined early,
    // because it gives us access to the tests_add_filter() function
    require_once $_tests_dir . '/includes/functions.php';
 
    // Hooked to muplugins_loaded, this function is responsible for bootstrapping
    // BuddyPress, as well as your own plugin
    function _bootstrap_plugins() {
        // loader.php will ensure that BP gets installed at the right time, and
        // that BP is initialized before your own plugin
        require BP_TESTS_DIR . '/includes/loader.php';
 
        // Change this path to point to your plugin's loader file
        require __DIR__ . '/../your-plugin.php';

   // Function called on plugin activation should be called here
    }
    tests_add_filter( 'muplugins_loaded', '_bootstrap_plugins' );
 
    // Start up the WP testing environment
    require $_tests_dir . '/includes/bootstrap.php';

    // Requiring this file gives you access to BP_UnitTestCase
    require BP_TESTS_DIR . '/includes/testcase.php';
 
    // Optional: If your plugin needs its own _UnitTestCase class, include it
    // here so that it's available when your testcases are loaded
    //require __DIR__ . '/bp-cli-testcase.php';
}

プラグインのファイルは読み込まれるだけで、内部的にプラグインが有効になるわけではないことに注意。有効化されたときに走るファンクションが定義されている場合は、ファイルがrequireされた後に直接実行してやる必要がある。

おつかれさまでした

あとはプラグインのディレクトリに移動して phpunit コマンドを叩けばテストコードが無事実施されるはずです。

Xcodeを6.0にアップデートしたらTitanium Studioが動かなくなったでござるの巻

散々な日曜日でした。

そもそも

iOS8の登場に伴い、Xcode6がリリースされましたね。
Mac App Store - Xcode
アップデートの通知も来ていたし、これはやらねばと思ってアップデートしたのです。そしたらTitanium Studioからアプリケーションのビルドが通らなくなった。

解決

結局、Titanium Studioをアンインストールし、3.4.0のバージョンを再インストールすることで万事解決。Xcodeをアンインストール->再インストールしたり、Time Machineからディスクまるごと復元したり、いらんことをやり過ぎた……。

まとめ

使っているツールのバージョンはちゃんと確認しよう。

Appストア リジェクトとの戦い(現在進行形)

久々の更新。

Appストアの申請が通らないよおおお

初めてAppストアへのアプリリリースに挑戦中なのですが、現在のところ2連敗中。心が折れそうや。
予想外の理由で怒られたりもしてるので、まとめておきます。

第1戦 "quit unexpectedly"

初めてのアプリリリース、うまくいくかな、どうかなーというどきどきを無残にかち割ってきたリジェクト理由がコレ。

We encountered the issue when selecting the application on the Home screen - the app displayed a launch image then quit unexpectedly. This may be because iOS 7.1.2 uses a watchdog timer for applications; if an application takes too long to complete its initial startup, the operating system terminates the application.

要するに 「ホームスクリーンから起動できません」と言われております。いやいやいや、さすがにそれくらい実機で確認してるし。ググってみると、テスト用ではうまく動いてもアドホック用では動かないケースがあるので両方試すべし、みたいなのが主な解決方法らしいのですが、ちゃんと両方でテストをしてるのでなぜリジェクトされたのかがよくわからない。

そんな中唯一「これが原因じゃない?」と見つけた不具合が、「プッシュ通知を許可する」のダイアログで「許可しない」を選択した際、処理が先に進まなくなるというバグ。このダイアログってアプリを再インストールしても再現されるわけじゃないので見つけられていなかった。再現には時計をいじる必要があってちょっとめんどくさい(下記リンク先参照)。

iphone - Reset push notification settings for app - Stack Overflow

これでまた同じ理由でリジェクトされたらどうにもならんなと思いつつ、バグ修正版を再申請。

第2戦 "Was not optimized to support the device screen size"

で、また無慈悲に突っ返された結果がこちら。

Was not optimized to support the device screen size and/or resolution on iPad; see screenshot for example.

添付されたスクリーンショットを見ると、内部ブラウザで表示されるウェブ画面がiPad用に最適化されてないからダメということらしい。内部ブラウザでは作成したウェブサービスを表示してるのだけど、確かにタブレット用レイアウトの対応後回しにしてたもんな……。でもこういう理由でもリジェクトされるのね。

まとめ

  • プッシュ通知のダイアログは「許可」「許可しない」両方チェックすること
  • 表示される画面はiPhoneでもiPadでもカッコよく見えるようにすべし