おじゃまぷよ系エンジニアメモ

主にスマホネイティブ関連Tips。たまにWebも書きたい。お腹はぷよぷよ

技術書典5をMarkdownのみで執筆してもなんとか黒字になった話

技術同人誌 Advent Calendar 2018 14日目の記事です。

本当はなぜこの本を書いたのかの深掘りとかしようと思いましたけど、本についての想いはこちらのブログに込めておきました。
熱い思いを込めておきましたので読んでください。

www.wantedly.com

もう技術書典については書くこともなくなってきたので総まとめ的な感じで執筆環境から入稿作業や売上どうだったか的なところを備忘録を込めて書いていきます。

執筆環境

VSCodeMarkdownで執筆していました。 文章の校正にredpenを最初使ってたんですが、途中でVSCodeプラグインテキスト校正くんというものを発見してからこちらを使ってました。

基本的にはVSCode上でMarkdownpreview見ながらがーっと書いて
ある程度書けたらmd2reviewでREView形式に変換してpdfを出力していました。
Re:Viewのstyleをいい感じに変更したりする技術がなかったので、デフォルトのstyleのままRe:View記法を自分で書くことなくMarkdownのみで書き続けました。 (marginだけいじったかも?) 今回書いてある程度Re:View記法も覚えてきたので、次回は最初はMarkdownを使わずに書いてみようかと考え中。
本のレイアウトとかもいじれるようになりたい…

わからないことだらけの入稿作業

ねこのしっぽさんを利用して最終的に出来上がったpdfや表紙画像を入稿しました。
正直なにもわからなかったので、求められるがままに本文pdfや表紙画像(psdファイル)をアップロードしました。
ただその後のフォローが手厚くて、pdf内で貼り付けていた画像がおかしいところや、表紙の画像のおかしいところなど細かい部分まで目を通してくれていてわざわざ電話にて教えてくれました。
最終的にはMarkdown->review記法に変換したもので、style.cssも特にいじらない本当にデフォルトのままでも無事に入稿できました。 ので最初はレイアウトにさえこだわらなければ最初から最後までMarkdownで書くことも可能です。
お値段は早期割引ありで150部 66ページで40,320円でした。思ったより安かったです。
入稿するときに表紙や印刷方法など何から何までわからなかったので、ほぼデフォルトのまま依頼したんですが、ちゃんと思ったとおりの本になってましたので、そのときのオプションも参考までに載せておきます。
出来上がった本は当日現地に直接届ける方式だったので、完成品が当日になってみないとわからないという恐怖と戦いました。

f:id:masahide318:20181213112825j:plain f:id:masahide318:20181213112838p:plain

被チェック数と売上

最終的にイベント時の売上は被チェック数が76で当日の売上部数は90部ほどでした。 他の方のブログを見てると被チェック数が150ぐらいあっても100部も売れなかったパターンなどあるらしいので、自分の場合は本当に運が良かったなと思います。
「友人に100部ぐらいは結構イケるよ」って言われたのを鵜呑みにして調子に乗って150部刷ってしまったので本当に危なかった。
爆死を避けられた理由は恐らく、「表紙で目を引く」と「声をかける」の2つだと思います。
まず表紙は小菅かつきさんに圧倒的美少女を描いてほしいとお願いしてめちゃくちゃ良い表紙を作ってもらいました。緑ベースにしたのはドロイド君がバ美肉したという設定で、周りには過去のAndroidバージョンのお菓子たちが散りばめられています。
そして現場ではとりあえず手にとって試し読みしてくれてる人にひたすら「これはこういう本です」というテンプレ説明を繰り返すマシーンと化して熱い思いを伝えていました。軽く会話してどういうことに困ってるかなど聞いたりして「だったらこの本参考になるかもしれないです!」みたいな流れで必死にアピールしてました。1年分のコミュ力を使い果たした場面。
なので手にとってから購入までのコンバージョン率は高かったように思います。
現在BOOTH上でオンラインで販売しておりますが、かれこれイベントが終わってからも20冊ほど売れています。
在庫抱えてるので買ってもらえると助かります。markdownのみで書いたものがどんな感じか見たい方もぜひ… ! t-masahide.booth.pm

というわけで生々しい売上話でした。
発行部数さえミスらなければ大赤字にはならないと思います。
中には基本電子版のみ販売してリアル本は見本分だけ用意するという方法をとってるサークルもありましたので、経費を抑えたい場合はそういうやり方もアリですね。

さいごに

途中にも書いてますがレイアウトにこだわらなければMarkdownで書き通すことも可能なので
あまり躊躇せずにとりあえずやってみることが大事ですね。 個人的には入稿作業が最もハードル高かったです…完全に未知の世界だったので。
執筆は余裕を持って終わらせて入稿しましょう。

PHPで学ぶChain of Responsibilityパターン

この記事はPHPで学ぶデザインパターン Advent Calendar 2018の4日目の記事です。

qiita.com

Chain of Responsibilityパターンとは

よく言われるのがたらい回しパターンですね。
自分では処理できない場合次のオブジェクトに処理をお願いするパターンです。

サンプルコード

とりあえずサンプルコードです。
RPGを想定して、勇者がモンスターと戦いますがそのモンスターのレベルが高かったり強い種族の場合は自分より強い勇者に倒すのをお願いするものを想定しています。

/**
 * モンスターです。名前とレベルだけ持ってます
 */
class Monster
{
    private $name;
    private $level;

    public function getName()
    {
        return $this->name;
    }

    public function getLevel()
    {
        return $this->level;
    }

    public function __construct($name, $level)
    {
        $this->name = $name;
        $this->level = $level;
    }
}
/**
 * 抽象的な勇者です。
 * canDefeat()で倒せるかどうかの判定は具体的なサブクラスに自由に決めてもらいます
 */
abstract class Braver
{
    private $name;

    /**
     * @var Braver
     */
    private $next;

    public function __construct(String $name)
    {
        $this->name = $name;
    }

    abstract function canDefeat(Monster $monster);

    /**
     * たらい回し先をセットします
     */
    public function setNext(Braver $next)
    {
        $this->next = $next;
        return $this->next;
    }

    /**
     * モンスターと戦います。
     * 自分が倒せない相手だと次の勇者に任せます
     */
    public function fight(Monster $monster)
    {
        if ($this->canDefeat($monster)) {
            echo $this->name . " が " . $monster->getName() . " を倒した\n";
        } else if ($this->next != null) {
            $this->next->fight($monster);
        } else {
            echo "全滅した\n";
        }
    }
}

3種類の勇者を用意しました。

/**
 * 新米勇者
 */
class NewcomerBraver extends Braver
{

    /**
     * レベル10未満のスライムなら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getName() == 'スライム' && $monster->getLevel() < 10;
    }
}

/**
 * 中級勇者
 */
class IntermediateBraver extends Braver
{

    /**
     * どんなモンスターもレベル15未満なら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getLevel() < 30;
    }
}

/**
 * ベテラン勇者
 */
class ExpertBraver extends Braver
{
    /**
     * どんなモンスターもレベル50未満なら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getLevel() < 50;
    }

}
$newComerBraver = new NewcomerBraver("新米勇者");
$intermediateBraver = new IntermediateBraver("中級勇者");
$expertBraver = new ExpertBraver("ベテラン勇者");
//新米勇者->中級勇者->ベテラン勇者の順にたらい回しする
$newComerBraver->setNext($intermediateBraver)->setNext($expertBraver);

$newComerBraver->fight(new Monster("スライム", 9));//新米勇者 が スライム を倒した
$newComerBraver->fight(new Monster("ドラキー", 9));//中級勇者 が ドラキー を倒した
$newComerBraver->fight(new Monster("スライム", 20));//中級勇者 が スライム を倒した
$newComerBraver->fight(new Monster("スライム", 40));//ベテラン勇者 が スライム を倒した
$newComerBraver->fight(new Monster("りゅうおう", 99));//全滅した

どういう時につかうの?

複雑なif-elseやswitch文を見た時に、これはポリモーフィズムで解決できないか?と少し立ち止まってみます。 今回のサンプルを愚直に実装してみると(正確にはちょっと違うけど…)

$monster = new Monster("スライム", 10);
$newComerBraver = new NewcomerBraver("新米勇者");
$intermediateBraver = new IntermediateBraver("中級勇者");
$expertBraver = new ExpertBraver("ベテラン勇者");

if ($monster->getName() == 'スライム' && $monster->getLevel() < 10) {
    $newComerBraver->fight($monster);
} else if ($monster->getLevel() < 30) {
    $intermediateBraver->fight($monster);
} else if ($monster->getLevel() < 50) {
    $expertBraver->fight($monster);
} else {
    echo "全滅した";
}

だいたいこのような感じになります。
これがぐらいなら愚直に実装した方がわかりやすいかもしれませんがもう少し条件が複雑になったり if-elseのパターンが追加されると見通しが悪くなります。そして

 if ($monster->getLevel() < 50) {
    $newComerBraver->fight($monster);
} 

みたいに条件と内部の処理が一致しないミスなんかも起こるかもしれません。 CORの良いところの1つに、条件と処理がひとつのクラスにまとまっているところですね。

もう少しシンプルに

CORをデザインパターンそのまま組むのはちょっとしんどいのでもう少しシンプルにした形で
配列に入れてforeachで回して書くCORパターンもどきを好んで使います。
これだと条件が増えても新たにBraverをextendsしたクラスを用意して配列にaddするだけでいいので楽です。

$braverParty = [
    new NewcomerBraver("新米勇者"),
    new IntermediateBraver("中級勇者"),
    new ExpertBraver("ベテラン勇者")];

$monster = new Monster("スライム", 10);
foreach ($braverParty as $braver) {
    if ($braver->canDefeat($monster)) {
        $braver->fight($monster);
        return;
    }
}
echo "全滅した";

かなりざっくりした説明になりましたが、if-elseやswitch文を見た時にポリモーフィズムでシンプルに書きたいという欲求に駆られたときに思い出してみるのもいいかもしれませんね。

参考資料

www.dezapatan.com

isucon8本戦に出たものの惨敗しました

ものすごくありがたいことにisucon8の本戦まで出場することできました。 ぶっちゃけチームメンバーが優秀すぎて自分はほとんど何もできなかったけど!

本戦出場の証。アイコンは先日の技術書典5で出した本のキャラクター。 f:id:masahide318:20181022194854j:plain

本戦でやったこと

クエリ改善

SELECT ORDER BYでフルスキャンするクエリをLIMIT 1つけて改善する。 しかしPHPのソース的にパフォーマンスにはそこまで影響しなかった。

諸々キャッシュしようとする

とりあえずredisで最高買値と最高売値をキャッシュしようとする
キャッシュしたと思ったらベンチマークコケてうまくいかない…キャッシュ削除のタイミングとかいろいろ悪かったかもしれない。 泥沼にハマりそうだったので途中で諦める (ほんと時間を無駄にした…申し訳ない)

N+1問題に対応

for文ぶんまわしでパフォーマンス悪いところがあったので
SQLのクエリ修正したり、INDEX貼り直したりする。プログラム側のデータの詰替えも修正する。 これでちょっとスコアあがる。2200ぐらいになった。

他のメンバーの修正をマージする

ぶっちゃけ自分が出来たのはこれぐらいで、あとは他のメンバーの修正をマージしてフィニッシュしました。 DB専用サーバーを1個別サーバーに設けたり、nginx側でWebサーバー負荷分散したりしましたが最終的に4200ほどに落ち着きました。 最後ベンチマークうまく動かず超ギリギリで最後動いたw f:id:masahide318:20181022200901p:plain

反省点

反省が多すぎて困る…

  • インフラはチームメンバーにお任せしていたが僕はdockerになれてないのですでに厳しかった。
  • キャッシュは早々に諦めるべきだったなぁと…そもそも効果うすそうだったし。
  • logのbulk_sendの存在に終了30分前ぐらいに気づいた…辛い
  • enable_shareの存在に気づかず最後まで放置してた
  • 途中でDataGripからisuconサーバーのmysqlに繋がらなくなりパニック

感想

  • イベントは予選は本戦もめっちゃ楽しかったです。休日潰してでも行く価値あり!
  • 本戦振り返ってみるともっと色々できただろ!って反省が多くてしばらくずっと悔しい思いしてましたw
  • でも予選通過は17倍ぐらいの倍率だったらしいので、初めてのisucon挑戦で本戦まで行けただけでも良かったとしよう!
  • dockerを捨ててるチームがあって、なるほどな…って思った。めっちゃ良い判断だと思った
  • ちゃんとソースとドキュメントは読もう!思わぬ落とし穴が潜んでるぞ!

カップケーキ美味しかったです。その他のケータリングもどれも美味しくて運営さまには感謝です。 f:id:masahide318:20181022204555j:plain

技術書典5に「テストが書けない人のためのAndroid MVP」を出展してきました

技術書典5無事に終わりました。足を運んでくださった皆様ありがとうございます。
「テストが書けない人のためのAndroid MVP」という本を出展していました。
f:id:masahide318:20181008200153j:plain
f:id:masahide318:20181008203441j:plain

そもそもなぜこの本を書こうとしたのか

事の発端はDroidKaigiに行った時に、なんとなくチームメンバーとテストコード書きたいなぁとざっくりした話になって
クリーンアーキテクチャやMVVMやMVPがある中で最終的に特別なライブラリを必要とせず、設計上やりすぎ感なく丁度いいであろうという理由でMVPに決まりました。
とはいえ実際にMVPっぽく書いてみたものの結構人によってMVPの認識の違いがあったり、元々存在する便利なシングルトンやUtilクラスが邪魔でテスト書きたいけどうまくいかない部分が出てきたのでそのあたりを解決するにはどうすればいいのかと考え、やってみた結果をこの本にまとめました。
DIとか使えばいいじゃんっていう安直な考えをせず、普通にプログラムを書いていかにJUnitでテストを書けるようにするかがコンセプトになってます。
レガシーコード改善ガイドを参考にしたものが多いのですが、Androidのあるあるな依存を排除するのにかなり参考になる部分が多かったので助かりました。

本書で書ききれなかったこと

本書ではマッチョなActivityをまずはMVPに置き換えてみるということを主軸にしたので、その後の発展としてMVPで依存を切り離したものMVVMに移行したいときはどうするかなど、その後の発展部分については書けなかったのでどこかで余裕があれば電子版などに追加していきたいと考えております……

出展してみての感想

今更MVPは若干古いのであまり多くの人にウケるものではないですが、話を聞いているとやはりテストコードを書けない人やActivityにがっつりソースコード書いてあるプロジェクトはまだまだありそうだなぁという印象を受けました。
ただ自分の書きたいものが書けたので大変満足しております。

オンライン販売もしております

BOOTHさんにて電子版と物理本も販売しておりますので、レガシーAndroidコードと戦っていて気になった方はどうぞこちらからお買い求めください。

t-masahide.booth.pm

ktlintをpre-commitに引っ掛けてコードフォーマットする

元々php書いてて、php-cs-fixerをpre-commitでフックして整形してたなぁ…と思って最近Androidをkotlinでソース書いてるときに、同じようなやつkotlin版ないのかなぁと調べたらちゃんとありましたね。

github.com

ということでphp-cs-fixerのパターンと同様にktlintでフォーマットかけます pre-commitのscriptはこちらを参考にしました

blog.manaten.net

結果以下のようなpre-commitにしました

#!/bin/sh

if git rev-parse --verify HEAD >/dev/null 2>&1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# Redirect output to stderr.
exec 1>&2


IS_ERROR=0
# コミットされるファイルのうち、.ktで終わるもの
for FILE in `git diff-index --name-status $against -- | grep -E '^[AUM].*\.kt$'| cut -c3-`; do
    MESSAGES=(`/usr/local/bin/ktlint -F $FILE `)
    if [ "${#MESSAGES[@]}" -gt 0 ]
    then
       IS_ERROR=1
       echo ${MESSAGES[@]}
    fi
done
exit $IS_ERROR

普通に「ktlint」でlint違反があるものに関してはexitProcess(1)で終わるのですが、「ktlint -F」でフォーマットかけるとexitProcess(0)で終了しちゃうのでシンプルに

if /usr/local/bin/ktlint -F $FILE; then
     git add $FILE
else
     IS_ERROR=1
fi

こんな感じで書けなかったので、ktlint -Fが何かlint違反の文字列を出力していたらコミットしないというような感じに変えました。

クローディアにはエンジニアとしてお世話になりました

クローディアとは

2012年4月6日から2017年12月30日の期間。372文字投稿できる国産ミニブログなど言われていましたが、実際のユーザーの利用方法としては小規模なTwitterでした。 そこのサービスのユーザーとの思い出も色々とあるのですが、アプリ開発の思い出などを少し…

アプリ開発の思い出

ここのサードパーティーAndroidアプリの「クローイド」とiOSの「Icors」を開発してかれこれ多くのユーザーに?使っていただきました。(現在はストアから掲載を落としております)
ネイティブアプリ開発者としてここのアプリを作るのは非常に良い教材でした。
auth認証の実装やらアクセストークンの管理、トークンは1時間で切れるので適宜リフレッシュする必要がったり…
システム的にもほぼTwitterと似たような機能を持ち合わせているので、自然とTwitterライクなUIを作ることになります。
しかしSDKなど一切存在しないので、すべて自分で実装しなければならないところが勉強に非常に適しているところでした。
Androidで新しい機能や便利そうなライブラリが出たら、クローイドに入れて実際の業務で使えそうかどうか実験していたことも多々あります。
またマテリアルデザインが出始めの頃はFABを入れたりReceyclerView入れたり、BottomSheetBehavior入れてみたり最新のUIのキャッチアップにも使わせてもらいました。
kotlinが流行りだしたときはkotlinでリプレースをし、MVPやMVVMなどクラス設計が話題になり始めたときにも実際に導入してどんな雰囲気になるか試したり、会社の新卒エンジニアでアプリ開発に配属された人にはクローディアのアプリを作ってもらって、Android開発のいろはを学んでもらったりしました。
Icrosは自分の初めてのiOSアプリ開発で、中のソースコードもUIも出来栄えとかはクソみたいなものでした…そしてアップルとの審査との戦いそのあたりは以前のブログにでも… masahide318.hatenablog.jp

このように、現在アプリ開発者としてそこそこマシな人間になれているのも何かあれば実際に試せる環境があったからです。
ユーザーのフィードバックもダイレクトに来ます。嬉しいことを言ってくれる人もいたり、厳しいことを言ってくる人もいましたが、全部参考にさせていただいてました。
広告や課金などの要素は一切入れてなかったのですが、Amazonのほしいものリストを公開するとみんな色々と送ってくれたので実際の現物支給の額でいえばなかなかのものになってました。(あまりクローディアの住民には言えない)

最後に

大晦日に完全ポエムの投稿でしたが、開発者としてはちょうどいいAPIがそろった絶妙なサービスだったのでこれから何かアプリの動作を試したいときはどうしようかなぁと思っているところです。やっぱりマストドンかな
クローディアの新サービスも開発中らしいのでまたAPIが公開されたらアプリを作りたいと思います。
長い間実験場としてありがとうございました。

社内でGoogle Home使ってハッカソンしました

タイトル通り1日エンジニアの業務をストップしてハッカソンしました
作ったものは大したことないんですが、3人チームでメンバーの一人がGoogle Homeを持っていたのでそれを使っての開発が楽しかったので振り返りを

作ったもの

「我々には仕事より大切なものがある」をテーマにGoogle Homeでアイドルやアーティストのライブに遅刻しないように、逐一Google Homeに喋ってもらおうと頑張りました。
ひたすらググって出てきた参考ソースをほぼコピペしてだけなのですが…

喋ってもらうもの

  • 公式ブログ
  • 公式アカウントのツイート
  • 特定ワードのツイート
  • 「残り時間教えて!」と聞くと、ライブまでの残り時間教えてくれる
  • etc…

使用したライブラリなど

Google Homeに指定した文言を喋ってもらうのには

github.com ツイートの取得には

www.npmjs.com

以上2つお世話になりました。 また独自の音声コマンド「残り時間教えて」を実行するのにIFTTTを使いました
これらの3つはGoogle Homeでググるといっぱい先人の経験があるのでおかげさまで楽できました

ifttt.com

それらを組み合わせてこんな感じになりました f:id:masahide318:20171216005632p:plain

基本的にツイートを喋ってもらう分には問題無い作りです。StreamingAPIで流れてくるテキストをgoogle-home-notifierにcurlを投げつければいいだけなので
ただ独自の音声コマンド追加して何かさせるというのがGoogle Homeあまり向いてないんですかね…こんなめんどくさいことするとは思わなかったです。

  1. 「残り時間教えて」って言うとGoogleHomeが反応
  2. IFTTTのApplet実行し、特定のワードをTwitterに投稿
  3. 特定のワードがStreamingAPIで流れてきて、ローカルPCで取得
  4. 残り時間計算して、google-home-notifierにcurlなげて計算結果しゃべってもらう

なんじゃこりゃ…ハッカソンなので許してください

ハマったところ

google-home-notifierを使うためにGoogle Homeの固定IPを取得する必要があったのですが、スマホの端末とGoogle Homeとの接続が同一ネットワーク内にあるにも関わらずうまく接続できないということがありました。
結果社内のwifiが悪いということが判明して手持ちのiPhoneテザリングして繋いだらあっさりセットアップできたのでなんとかなりました。

改善点

  • やはりgoogle-home-notifierを常駐しておく用のラズパイなり何かが欲しかった
  • もっと別のいいやり方や連携サービス使えたかもしれないが知識が足りなかった

まとめ

  • 初めて使う言語やデバイスに触れる機会ができて楽しかった
  • 接続うまくいかないときはテザリングする
  • Google Home実際に使ってみておもったより色々なことできそうなのでおもちゃとしてほしくなった
  • もう少し普段からサービス間の連携を考えたり、何ができるかを意識しておく必要がある
  • 修行がまだまだたりないと改めて実感
  • AmazonEcho早く届いてほしい