Aliexpressおすすめ自転車関連商品を紹介

本日、Aliexpressでは日本時間17時から11.11セールが行れています。 そこで今回、筆者がAliexpressでこれまで実際に買って良かった自転車関連商品を紹介していきます。

そもそもAliexpressってどうなの

筆者は2016年にAliexpressに登録してから、これまでに312件の注文をしています。 金額にすると375,320円です。 購入した商品の内訳としては、自転車部品、電子部品、キャンプ用品、衣類、化粧品、日用品など多岐にわたって購入しています。

うちトラブルがあった注文は3件だけで、2件が不着、1件が商品間違いでした。 この3件の注文はすべてOpen Dispute(返金請求)して全額返金となっています。

f:id:emonkak:20201111185718p:plainf:id:emonkak:20201111185714p:plain

中国の通販は信頼できないと思われる方は多いかもれませんが、筆者の場合トラブルの比率でいうと国内通販と遜色ないレベルです。 一部に悪質なセラーがいて、そこから購入するとトラブルに繋がる場合があるのは否定できませんが、ストアの評価や開設日(昔からあるストアは信用できる)、商品の注文数(たくさん注文されていれば信用できる)を参考に注文すれば、そのようなセラーに当たる確率はかなり減らせます。 もしトラブルにあっても、ほとんどの場合はOpen Disputeで返金されるので、必要以上に恐れることはないです。

届くのに時間がかかるというデメリットはありますが、ここでしか買えない面白いものも色々ありますし、何より価格が圧倒的に安いです(最近は送料が上がっていますが、それでもまだまだ安い)。 是非、怪しい魅力溢れるAliexpressを楽しんでみて下さい。

おすすめ商品

以下に筆者が実際に買って良かったと思う自転車関連商品をご紹介しています。

リンクは基本的には実際に購入したストアの商品を貼っています。 もし品切れだった場合は、別のストアから適当なものを選んでいます。

購入金額は商品の代金と送料を足して、クーポンによる割引きを適用した実際の請求金額となります。

★★★ LTWOO R7 2X10ロードシフター

  • 購入金額: ¥6,378

MTBコンポで有名なLTWOOのロード用シフターです。 少し前までどこにも流通していませんでしたが、最近はAliexpressで購入できるようになったみたいです。

筆者はこのシフターがまだ流通していない時に、別の新しく出たSensahの10Sシフターを購入した所、品切れと言われ代わりにこのLTWOOのシフターを送ると勧められ偶然に入手できました。

公式サイトに詳しい仕様の記述はありませんが、実際試したみた所、シマノ4700系Tiagra互換の10Sで、ブレーキはNew Super SLR対応みたいです。

ブラケットの形状はカンパっぽい感じで、変速の方式もカンパと同じです。 いわゆるシマニョーロは色々と困難がありますが、このシフターが使えれば簡単にカンパの操作感が手に入るのでおすすめです(ただし10sのみ)。

追記:公式サイトにはまだありませんが、いつのまにか11sモデルも出てました。

ブラケットカバーの単品売りもして欲しいですが現在取り扱いはないようです。

f:id:emonkak:20201111193750j:plain
LTWOO R7

★★★ LTWOO A7 1x10 MTBシフター&ディレイラー

  • 購入金額: ¥ 2,679

LTWOOのMTB用のシフター&ディレイラーです。 SRAMっぽい感じの作りで、操作感は滑らかなシマノのとは違いますが全然悪くないです(使ったことないのでわかりませんがSRAMに近いのではないかと予想)。

ディレイラーにSSモデルがあるのがお気に入りのポイントです。 最近のMTBコンポは1xが主流になり、カセットがどんどん大きくなった影響で、ケージの長いモデルしかありません(シマノならGSとSGSのみ)。 しかし、LTWOO A7にはリア34Tまで対応のSSモデルが存在するので、ワイドカセット使わない人にはおすすめです。 このコンポを取り付けた筆者のバイクは前40T、後11-36Tにしてオンロードの平地とヒルクライムもこなせる仕様にしています。

★★★ Lumintop B01フロントライト

  • 購入金額: ¥3,905

Lumintopの850ルーメンのフロントライトです。 筆者がライトに求める以下の条件すべてを満たしたライトで、個人的には現状最高のライトです。

  • 800ルーメン以上
  • LED1灯のコンパクトデザイン
  • ブラケットを個別購入可能
  • 幻惑防止の光軸(ロービーム)
  • 21700/18650電池で稼動し、交換可能

注意点として、18650電池は保護回路なしの長さが短いものを使うと振動で消えてしまうことがあるので、ネオジム磁石(100均で買える)で長さを調整するといいです。

ブラケットは以下の商品が本体付属のものと同じで、個別かつ安価で買えます。

このブラケットは構造上使っている内にだんだん緩くなってしまうので、脱落防止のバンドを付けておくことをおすすめします(実際に一度ライト落としました)。 筆者は脱落防止のバンドとして以下の商品を使用しています。

★★★ テフロンコーティング ブレーキケーブル&シフトケーブル

  • 購入金額: ¥ 457(シフトケーブル2本、ロード用ブレーキケーブル2本)

テフロンコーティングのケーブルです。 一本約150円と激安ですが、BBBとかのテフロンコーティングケーブルと性能は遜色ないように思います。

注意点としてはリア用のブレーキケーブルが1700mmと若干短い(シマノは2050mm)ので、ディスクブレーキの場合は長さが足りないケースがあるのに注意して下さい。

★★★ ROCKBROSハーフグローブ(S106)

  • 購入金額: ¥746

中華ブランドとしては有名なROCKBROSのハーフグローブです。 マジックテープがないタイプなので、フィット感が良く、毛羽だって痛むことないのが良いです。 パッドは厚めで、グリップ感の強いタイプで耐久性もいいです。

★★ ASS SAVERS偽物簡易フェンダー

  • 購入金額: ¥102

ASS SAVERSの偽物の簡易リアフェンダーです。 1000円以上もする本家も持っていましたが、全く違いがわからないのでこれで十分です。

★★ Fouriers簡易フェンダー

  • 購入金額: ¥184

台湾のFouriersの簡易リアフェンダーです。 インシュロックで固定するタイプなので、ずれたりしません。 その代わり、取り外す度にインシュロックを切らないといけないので、必要な時だけ付ける使い方には向きません。

★★ Racmmerウィンドブレイカ

  • 購入金額: ¥1,399

ちょっと寒い時に着る軽量(100g)のウィンドブレイカー。 巾着付きでかなりコンパクトになるので気軽に持ち運べます。

耐水ということで防水性はあまりないので、雨だとすぐに浸水してきます。 防水性求めるなら防水モデルもあるのでそちらの方がいいかもしれません。

ジッパーが弱く、閉じた所が開いてくることがあるのが欠点。

★★ Giyoツールボトル

  • 購入金額: ¥813(Small) ¥901(Large)

空気入れで有名なGiyoのツールボトルです。 Smallは例のポンプが無理してギリギリ入る(Large推奨)サイズ感です。 両方購入してSmallはロード用、Largeはグラベル用に使い分けてます。

★★ Cairbullヘルメット

  • 購入金額: ¥2,498

Lサイズでカタログ重量240g(実測225g)というハイエンド並の軽量ヘルメットです。 モデルチェンジがあったのか筆者のモデルは入手困難になってしまったので、後継らしきものを貼っておきました。 こちらはLサイズでカタログはなんと重量195gらしいです。

f:id:emonkak:20201111191244j:plain
筆者所有の旧モデル

筆者がこのヘルメットの前に使ってたのはLazer Blade AFのLサイズで、調整ダイヤルを半分くらい閉めて使用していました。 このヘルメットの場合は、ダイヤルを少し閉めると丁度いいサイズ感です。 横幅が狭いという感じはありません。

仕上げは普通で所々粗はありますが、これは以前使用していたLazerのヘルメットでも同じだったので特に悪いということはないです。 見た目で気になる点と言えば、ロゴのデカールが著しくずれて貼られていたくらいですが、気になるなら剥してしまえばいいので問題はないです。

取り付けバンドがすぐ緩んでくるのが難点で、これはバンドを丁度いい長さにして固結びしてしまうことで解消しています。

★★ MicroShift BS-A11バーエンドシフター

  • 購入金額: ¥6,255

Microshiftの11速バーコンです。 国内の半額以下で買えます。

筆者はGevenalleで使うために購入しました。 ただしキャップが合わなかったので、キャップだけGevenalle付属のシフターのものに換装しています。

f:id:emonkak:20201111191528j:plain
Gevenalleのシフターとして使用

★★ Pillarニップルレンチ

  • 購入金額: ¥1,099

Pillarの4面掴みのニップルレンチです。 4面掴みのニップルレンチの中ではこれが一番使い易いのではないでしょうか。 3.2mmの方がDT等の海外メーカーのニップル用で、3.4mmの方がホシ等の国内メーカーのニップル用です。

このニップル使ってからはアルミニップルを潰してしまうことはなくなりました。 アルミニップルでホイール組みするなら必須の品です。

★★ Gineyea GXP BSA BB

  • 購入金額: ¥1541

SRAMGXP対応のBSA用BBです。 SRAMの公式のGXP BBの半額以下で買えますが、回転は滑らかで性能的には遜色ないです。

GXPのクランクに取り付ける場合は、反ドライブ側にシムを入れて22mmのシャフト径に対応させます。

注意点として、73mmのシェル幅対応のMTBクランクのみ対応で、68mm用のロードクランクはそのままでは使用できません。 ロードクランクに対応させるには、プラスチック製のスリーブをカッターナイフ等で5mm切断する必要があります。 筆者はこの改造をしてロード用のSram Rivalクランクとの組み合わせで使用しています。

★★ Chaoyang Fly Fish H-486タイヤ(700x25c)

  • 購入金額: ¥3,560(2本)

国内でも代理店があって流通しているChaoyangのレースタイヤです。

500km程度走行しましたが、乗り心地の良さに定評のあるコルサG+に比べても遜色ない振動吸収性でした。 グリップや転がり抵抗に関してはそんな攻めた走りをしないのでよくわかりませんが、ネガティブな印象はありません。

あとは耐久性ですが、既にサイドが若干ひび割れてきているのが気がかりではあります(長期保管品だったのかも?)。

f:id:emonkak:20201111185511j:plain
サイドのひび割れ

★★ Ansjs トリプルシールドベアリング フラットペダル

  • 購入金額: ¥1,646

薄型のピン付きのフラットペダルです。 筆者が購入したモデルはディスコンになってしまったので、似たモデルを貼っています(購入したモデルがディスコンになっていなければ★★★でした)。

この値段では考えられない程の綺麗な仕上げで高級感すらあります(貼ったモデルは軸のメッキが安っぽい感じ見えますが……)。 ピンは低めで靴をあまり傷めません。 トリプルシードベアリングで回転はめちゃくちゃ滑らかです。

f:id:emonkak:20201111192405j:plain
筆者購入のモデル

★★ Snail 100BCDナローワイドチェーンリング(40T)

  • 購入金額: ¥1,513

Aliexpressでもあまり流通していない110bcdのナローワイドチェーンリングです。 40T〜44Tまであるのでグラベルバイクにぴったりの歯数が選べます。

筆者はSRAM S350クランクの歯数をデフォルトの44Tから40Tに変更するために購入しました。

f:id:emonkak:20201111192440j:plain
SRAM S350クランクへの取り付け

★★ Fovno 104BCDスパイダーアーム(SRAMクランク用)/DECKAS 104 BCDチェーンリング

  • 購入金額: ¥2,352
    • Fovnoスパイダーアーム: ¥900
    • DECKASチェーンリング: ¥1,452

SRAMクランク用の104BCDのスパイダーアームと、104BCDのチェーンリングです。 SRAMクランクを1x化してグラベルバイクで丁度いい40〜44Tあたりの歯数で運用したい場合、この組み合わせが安価かつ歯数の組み合わせの自由度が高くおすすめです。

筆者はダブルのSRAM Rivalクランクの長期保管品を安く手に入れて、それとこのスパイダーアームとチェーンリングを組合せることで安価に1x化しました。 使わないダブルのチェーンリングは約3000円で売れたので収支はプラスになりました。

f:id:emonkak:20201111192601j:plain
SRAM Rivalクランクへの取り付け

★★ カーボンフラットバー/ライザーバー

  • 購入金額: ¥1,757

いわゆる中華カーボンのバーハンドルです。 バーにはRaceface Nextとプリントされていますが、もちろんRacefaceとは何の関係もなく勝手に名乗っているだけです。 他にも様々なロゴや色のパターンの亜種がありますが、恐らく出所は同じものだと思います。 本当は無地が良かったのですが、この製品が一番ロゴが目立たなかったので決めました。

さて、中華カーボンというと耐久性を気にする人が多いとは思いますが、この製品は断面を見るとかなり肉厚で、折れるということはまずないという感じがします。 中華カーボンのロード用のエアロハンドルなんかは破損報告がちょくちょくありますが、バーハンドルが折れたという話は聞いたことがないのでその点でも安心できるかなと思います。

長さのバリエーションが豊富なので最初から必要な長さが決まっている人にはカットの手間が省けて楽です。 筆者はツーリング用のバイクで使用しており、舗装路を中心にある程度未舗装路もいける640mmを選択して丁度いいと感じています。 最近ちょくちょく増えているフラットバーのグラベルバイクなんかも、完成車ではこのあたりの長さが採用されていることが多いです。

★★ チューブレスバルブ

  • 購入金額: ¥417(2本セット)

メーカー品だとそれなりに高いチューブレスバルブですが、これはめちゃくちゃ安いです。 リムとの相性はあるかと思いますが、Kinlin XR-22Tとの組み合わせで何の問題なくチューブレス化できました。

チューブレス用のリムテープもAliexpressでは多数出品されていますが、長さが短い(10mくらい)ので幅が合うなら3Mの仮固定用テープ(50m)を買った方がお得です(両方買いましたが、Aliで売ってる青テープは3Mのと恐らく同じものです)。

また、3Mの仮固定用テープでは幅が合わないリムの場合、ダクトテープを縦に切って幅を調整して貼るのがコスパいいです。 こちらでもチューブレス化できましたし、伸びがあって貼り易いのもメリットです。

★★ シマノハブフリーボディ分解工具(TL-FW03代替品)

  • 購入金額: ¥688

シマノハブのフリーボディは分解工具TL-FW03が廃盤になってしまったので、公式には分解整備ができません。 ソケットレンチで工具を自作している人もいましたが、今はこの工具で確実に分解できます。

筆者はこの工具を使って中古で買ったFH-7700のフリーボディを分解整備しました。 ただし、このハブはフリーボディ側のシールリングを外すのは非推奨となっており、実際やってみると外すのは困難でした。 結局シールを無理やり曲げて外して、新しい同等品のシールシールリングを買ってを取り付けました。 整備の結果、当初フリーの回転に抵抗感とムラがあったのが解消してスムーズになりました。 音に関しても、ただでさえ静かなシマノフリーが完全に無音になりました。

シマノハブのフリーボティでトラブルにあった人、無音化したい人におすすめの工具です。

★★ ロングプレッシャープラグ

  • 購入金額: ¥1,265

取り付け面がかなり長いタイプのプレッシャープラグです。 カーボンコラムの場合、短いタイプだとコラムを傷めてしまうことがあるので、これだけ長いと安心できます。

★ Pillar TB-2015スポーク

  • 購入金額: ¥4,186(34本)

Pillarのトリプルバテッドの超軽量スポークです。 首折れ部が2.2mmで、中間が1.5mm、ねじ切り部が2.0mmと、Pillar独自の形状になっています。

中間が1.8mmのTB-2018というモデルもありましたが、残念ながら今は販売しているセラーがありません。 筆者は、(リアの場合)テンションが高くなるフリー側をTB-2018、テンションが低くなる反フリー側をTB-2015(フロントの場合はディスク側をTB-2018、反対をTB-2015)のようにスポークを使い分けて32本組みでホイールを組みました。 エアロスポークだとかなり値段が上がってしまうので、安く軽量なホイールを組みたい場合に重宝するスポークです。

★ Tange LN-7922 JISスクエアテーパーBB

  • 購入金額: ¥2,042

TangeのJISのスクエアテーパーの用のBBです。 シマノ製に比べると回転が滑らかで、同価格帯のBBでは恐らく一番性能いいです。 何故か国内流通していない軸長103mmも売っているのでシングルスピードクランク用におすすめ。

★ UNO ハンドルバー/ステム/シートポスト

  • 購入金額: ¥4,206
    • ハンドルバー: ¥886
    • ステム: ¥1,329
    • シートポスト: ¥975
    • 送料: ¥1,016

台湾のKalloyのUNOブランドのハンドルバー/ステム/シートポストです。 デザインはシンプルでどんなバイクにも合わせ易くサイズも豊富です。 値段は安いですが、見た目は安っぽい感じはありません。

シートポストは取り付けボルトが長く、乗車するとサドルが沈んで底面に干渉してしまうので、5mmくらいボルトをカットしました。

★ 高グリップ薄手バーテープ

  • 購入金額: ¥695

高グリップで薄手(2.5mm)のバーテープ。 テープの端がガタガタであまり綺麗ではないですが、使い心地は悪くないです。

残念ながら今は値段が上がってしまっています(2020年11月現在)。

f:id:emonkak:20201111193525j:plain

Shimano NX-01チェーン

  • 購入金額: ¥977

シマノの廃盤になった厚歯のシングル用、通称アーモンドチェーン。 シマノのチェーンは偽物が出回っているとのことなので、これもその可能性がありますが品質的な問題はありませんでした(8000kmくらいは使いました)。

ただし、特殊な形状である故に、フリーギアで使用の場合にボディに干渉することがあるのに注意。 筆者の組み合わせだと走行抵抗が感じられるレベルの干渉ではありませんでしたが、ボディに擦れた跡が残っていました。

見た目が好きであえて選ぶということでなければ、IZUMIのチェーンが1000円ちょっとで買えるのでそちらの方がいいかもしれません。

PHP用ORM Emonkak\Ormの概要

背景

データベースを利用したWebアプリケーションをインフラストラクチャ(技術的機能)の観点から単純化すると、クライアントのリクエストを入力に、データベースに格納されたデータをCRUD(Create/Read/Update/Delete)して、その結果をレスポンスとして出力するものと定義できる。

この定義からいうと、リクエストとレスポンスの入出力を管理するのがWAF(Web Application Framework)で、データベースに格納されたデータをCRUDするのがORM(Object-relational mapping)となる。 両者はWebアプリケーションを構成するインフラストラクチャのコアであり、特にORMはドメインモデルの設計にも大きな影響を与えるのでその重要性はより高い。

そのような中、ORMといえばRuby On RailsのActive Recordに代表されるような重厚長大なものが主流だ。 一方で、ドメインモデルを特定のインフラストラクチャ(ORM)に依存することを避けるという観点からは、よりシンプルで依存性の少ないORMの方が望ましい。

ドメインの関心とインフラストラクチャの関心を適切に分離して、それぞれの関心に注力することでシンプルかつ明瞭な設計を実現する。 そのために新しいORMが必要だった。

コンセプト

Emonkak\Ormはフレームワークではなくライブラリとして設計した。

ここで言うフレームワークとライブラリの違いは以下の点にある:

ここでフレームワークとしてのORMの問題を考えてみる。

よくある例としては、ドメインモデル(エンティティ)の実装にORMが提供する基底クラスを継承しなければならないということがある。 これでは、ドメインとインフラストラクチャの関心が混ざり合い、適切に分離するのが難しい。 ORMが提供するモデルとドメインモデルを別個のものとして設計するという考えもあるが、冗長な実装になりがちでデメリットの方が大きいだろう。

制御の逆転を利用したフレームワークの難点としては、単純にわかりづらいという問題がある。 どのように機能が呼び出されるのかは、フレームワークの動作を知らなければわからないからだ。

そこで、Emonkak\Ormはあくまでインフラストラクチャの実装を助けるためのライブラリであると位置付け、ドメインの設計に影響を与えないようにした。 いわゆるリポジトリの実装に使うための一つのライブラリに過ぎないということだ。

特徴

Emonkak\Ormはクエリビルダー、リレーション、オブジェクトへのマッピングと基本的な機能しか提供しない。 データベースの抽象はEmonkak\Databaseが、結果セットのシーケンス処理はEmonkak\Enumerableと別のライブラリによって提供されるものを利用する。

  • SELECT, INSERT, UPDATE, DELETE文のクエリビルダー
  • カスタマイズ可能なマッピング動作
  • 一対一、一対多、多対多のリレーション
  • アクセスされるまで読み込みが遅延される遅延リレーション
  • PSR-6 Caching Interfaceを利用したキャッシュ付きのリレーション
  • ページネーション

使い方

※すべて開発中のものなので変更可能性がある

基本的な使い方としてはクエリビルダーを作成してクエリを構築、用途に合った実行メソッドを呼ぶ形になる。

SELECT文を発行して結果セットを取得する

クエリの実行結果を得るための結果セットを取得するには getResult() メソッドを呼び出す。 getResult() 実行時点ではまだクエリは実行されておらず、foreach で走査するなどして、実際に結果を得ようとして初めてクエリは実行される。 結果セットは Emonkak\Enumerable\EnumerableInterface を実装しているので、例えば first() メソッドを呼び出すことで一件だけ取り出すことができる。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\SelectBuilder;

$users = (new SelectBuilder())
    ->from('users')
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class));

$user = (new SelectBuilder())
    ->from('users')
    ->where('user_id', '=', 1)
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class))
    ->first();

ここで PopoFetcher というのが出てきたが、これは Emonkak\Orm\Fetcher\FetcherInterface の実装クラスで、 Emonkak\Database\PDOStatementInterface からどのように値を取得するのかを決めるものだ。 PopoFetcher の場合は指定されたクラスのインスタンスを作成して、それをPOPO(Plain Old PHP Object)としてプロパティに値を代入する。 POPOとはPOJO(Plain Old Java Object)PHP版で、マジックメソッドなどを利用しないごく普通のPHPオブジェクトを表す。

POPOの実装例:

<?php

class User
{
    private $user_id;
    private $first_name;
    private $last_name;

    public function getUserId()
    {
        return $this->user_id;
    }

    public function getFirstName()
    {
        return $this->first_name;
    }

    public function getLastName()
    {
        return $this->last_name;
    }
}

FetcherInterface の別の実装としては指定されたクラスのコンストラクター連想配列を与えてインスタンスを作成する ModelFetcher と、オブジェクトにマッピングせずに連想配列をそのまま返す ArrayFetcher の実装がある。 ModelFetcherActiveRecordタイプのORMにあるようなModelクラスを利用したい場合に役立つ。

ページネーションを利用する

指定された件数ごとにページ分けされた結果セットを取得するには paginate() メソッドを呼び出す。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\SelectBuilder;

$userPages = (new SelectBuilder())
    ->from('users')
    ->where('user_id', '=', 1)
    ->paginate($pdo /** @var PDOInterface */, new PopoFetcher(User::class), 100 /** @var integer ページ分けの単位 */);

$users = $userPages->at(1);  // 1ページ目を取得
$userPages->getNumItems();  // 総アイテム数を取得
$userPages->getNumPages();  // 総ページ数を取得

更新系のクエリを実行する

結果行がない更新系のクエリを実行する際は execute() メソッドを利用する。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\UpdateBuilder;

$stmt = (new InsertBuilder())
    ->into('users', ['first_name', 'last_name'])
    ->values(['foo', 'bar'])
    ->execute($pdo /** @var PDOInterface */);

$stmt = (new UpdateBuilder())
    ->table('users')
    ->set('first_name', 'foo')
    ->set('last_name', 'bar')
    ->execute($pdo /** @var PDOInterface */);

$stmt = (new DeleteBuilder())
    ->from('users')
    ->where('user_id', '=', 1)
    ->execute($pdo /** @var PDOInterface */);

簡易クエリビルダー

Emonkak\Orm\Sql クラスはSQL文字列と、プレースホルダーにバインドする値のペアを表すクラスだが、簡易的なクエリビルダーとしても機能する。 また、 Sql オブジェクトを各種クエリビルダーのメソッドに渡すことで、クエリビルダー単体では実現できないような複雑なクエリも表現できる。

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\Relation\Relations;
use Emonkak\Orm\SelectBuilder;
use Emonkak\Orm\Sql;

$users = (new Sql('SELECT'))
    ->append('*')
    ->append('FROM')
    ->append('users')
    ->append('WHERE')
    ->append('user_id IN')->appendValues([1, 2, 3])
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class));

// SQLの断片を条件式に指定することもできる
$users = (new SelectBuilder())
    ->from('users')
    ->where(new Sql('user_id = ?', [1]))
    ->getResult();

リレーションを利用する

リレーションは SelectBuilder あるいは Sql クラスの with() メソッドを利用することで行う。 with() に与えるリレーションは Emonkak\Orm\Relation\Relations が提供するファクトリメソッドで作成できる。

リレーションはネスト可能で、Emonkak\Orm\Relation\RelationInterfacewith() メソッドを使うことでネストできる。

作成できるリレーションの種類としては以下のものがある:

  • Relations::oneToOne(): 一対一
  • Relations::oneToMany(): 一対多
  • Relations::manyToMany(): 多対多
  • Relations::lazyOneToOne(): 遅延一対一
  • Relations::lazyOneToMany(): 遅延一対多
  • Relations::cachedOneToOne(): キャッシュ付きの一対一
  • Relations::cachedOneToMany(): キャッシュ付きの一対多

遅延リレーションについては別記事にて詳しい記述がある。

リレーション作成時に指定するパラメータは以下ものがある:

  • $tableName: string

    リレーション先のテーブル名。

  • $relationKey: string

    リレーション先のオブジェクトを格納するプロパティ名。

  • $outerKey: string

    リレーション元のテーブルの結合対象のカラム名

  • $innerKey: string

    リレーション先のテーブルの結合対象のカラム名

  • $connection: Emonkak\Database\PDOInterface

    リレーション先のテーブルのデータを取得するためのDBコネクション。 リレーションごとに指定するので、それぞれのリレーション先のテーブルが別のDBにあっても良い。

  • $fetcher: Emonkak\Orm\Fetcher\FetcherInterface

    リレーションのクエリの結果からどのように値を取得するかを決定する FetcherInterfaceインスタンス。 リレーションごとに指定するので、全く別のクラスか、あるいは連想配列をも混在してリレーションさせることもできる。

  • $proxyFactory: ProxyManager\Factory\LazyLoadingValueHolderFactory (Optional)

    遅延リレーションを利用する場合のプロキシオブジェクトを作成するためのファクトリ。

  • $cachePool: Psr\Cache\CacheItemPoolInterface

    キャッシュ付きのリレーションを利用する場合のキャッシュプール

  • $cachePrefix: string

    キャッシュ付きのリレーションを利用する場合のキャッシュキーのプレフィックス。

  • $lifetime: integer

    キャッシュ付きのリレーションを利用する場合のキャッシュの有効期限。

  • $builder: Emonkak\Orm\SelectBuilder (Optional)

    リレーション先のテーブルからデータを取得するためのクエリを生成するクエリビルダー。 リレーションする条件を特別に指定したい場合に利用する。

以下はユーザー(User)が投稿(Post)と、それに対するコメント(Comment)を持っているというリレーションの例だ:

<?php

use Emonkak\Database\PDOInterface;
use Emonkak\Orm\Fetcher\PopoFetcher;
use Emonkak\Orm\Relation\Relations;
use Emonkak\Orm\SelectBuilder;

$hasPosts = Relations::oneToMany(
    'posts',
    'posts',
    'user_id',
    'user_id',
    $pdo /** @var PDOInterface */,
    new PopoFetcher(Post::class)
);

$hasComments = Relations::oneToMany(
    'comments',
    'comments',
    'post_id',
    'post_id',
    $pdo /** @var PDOInterface */,
    new PopoFetcher(Comment::class)
);

$users = (new SelectBuilder())
    ->with($hasPosts->with($hasComments))
    ->from('users')
    ->where('user_id', '=', 1)
    ->getResult($pdo /** @var PDOInterface */, new PopoFetcher(User::class));

おわりに

Web開発の世界は長らくフルスタックのフレームワークが主流で、小さなライブラリを組合せてアプリケーションを構築するという文化があまりなかった。 しかし、PHPの世界ではPRR-7 HTTP message interfaceの登場以降、様々なソフトウェアが登場し、小さなものを組合せて大きなアプリケーションを作るというUnix哲学にも通ずる考えが徐々に普及してきた。

さらに近年はDDD(Domain-Driven Design)の考えが広く浸透つつあるのもあってか、アプリケーションのアーキテクチャについてさまざまな論考が出てきている。 アーキテクチャはアプケーションの構成部品をどう分割し、どう組み合せるかということで決まる。 そのような中、構成部品の組み合わせ方に制限を加えてしまうようなフレームワークは、アーキテクチャの決定にも制限を与えてしまうだろう。

そこで、アプリケーションの要件に適合するフレームワークを、小さなのライブラリの組み合わせで独自に構築するというのが一つの考えだ。 Emonkak\Ormはこの考えを元に開発されたライブラリである。

エンティティのライフサイクルに基いたクラス設計

エンティティとはその属性によってではなく識別子によって識別されるオブジェクトである。 属性が違っていても識別子が同一であればそのエンティティは同一のものと見なされる。 そのためエンティティは多くの場合、変更可能(Mutable)なオブジェクトとして表現される。 変更可能(Mutable)なオブジェクトは最初に生成されてから、それ以後に状態を変えることがある。 この状態遷移の流れがエンティティのライフサイクルである。

では、エンティティの状態はどのように変化するだろう。 それは、ドメインモデルを使って業務手順を記述したユースケースによって決まる。 ユースケースでは業務手順に基いてエンティティを作成し、状態を変更し、あるいは削除する。 これがエンティティのライフサイクルだ。

PHPにおけるエンティティの実装

エンティティはユースケースによってその状態を変えるが、ドメインモデルの一部であるエンティティをドメインのルールに反するような状態にしてはならない。 エンティティがドメインのルールに反する状態にあるというのは、バグに他ならないからだ。 実装レベルでエンティティがドメインのルールに反する状態にすることをできなくするのが望ましい。

基本的な方針としてエンティティの持つプロパティが、DBの列名と一致するように実装する。 プロパティは自由に参照・変更できないように private に隠蔽しておく。

class Person
{
    private $person_id;

    private $first_name;

    private $last_name;

    ...
}

コンストラクター

エンティティのコンストラクターprivate にして隠蔽しておく。 自由にインスタンスを作られては状態を適切に管理できないからだ。 インスタンスの生成は後述のFactory Methodによって行う。

private function __construct()
{
}

ゲッター

エンティティの属性値として、ただの文字列(String)や数値(Integer)ではドメインの関心を表現するオブジェクトして適切ではないことがある。 その場合はプロパティをそのまま返すのはでなく、ドメインモデル(値オブジェクト)を返すようにする。 そうでない場合はそのままプロパティを返却する。

public function getName()
{
    return new PersonName($this->first_name, $this->last_name);
}

セッター

単純なプロパティのセッターはすべて private にして隠蔽しておく。 自由に属性を設定できては状態を適切に管理できない。 外から状態の変更するメソッドはセッターではなくドメインに基いた振舞いとして別途適切に実装する。 セッターで設定される値を検証して値がエンティティの属性として適切ではなければ例外を投げるということもありうる。 この検証はあくまで属性レベルの検証なので、エンティティ全体の状態として正しいことを保証しているわけではない。

private function setFirstName($first_name)
{
    if ($first_name === null || $first_name === '') {
        throw new \InvalidArgumentException('名前は必ず設定する必要がある。');
    }
    $this->first_name = $first_name;
}

Factory Method

エンティティを生成する時は必ずFactory Methodを使用する。 コンストラターでいいと思うかもしれないが、PHPはメソッドのオーバーロードができないので別途Factory Methodを作った方が都合がいい。 このメソッドで作成されたエンティティの状態はドメインにおいて合法であることが保証される。

public static function create(Name $name)
{
    // first_name や last_name を持たない人間を作ることはできない
    $person = new Person();
    $person->setFirstName($name->firstName());
    $person->setLastName($name->lastName());
    return $person;
}

エンティティのライフライクルに基いた振舞い

これはドメインのルールに基いて、エンティティの状態を変更させるメソッドである。 これは対象とするドメインによって異なるので、その都度考える必要がある。 多くの場合ドメインの用語(ユビキタス言語)で動詞に対応するものが命令形で定義されることになる。

例:名前を変更する

public function changeName(Name $name)
{
    $this->setFirstName($name->firstName());
    $this->setLastName($name->lastName());
}

まとめ

業務手順を表すユースケースとは、エンティティのライフサイクルの記述に他ならない。 エンティティのライフサイクルはドメインに基いたものでなければならず、ユースケース実装に際してエンティティを不適切な状態にできるのは望ましくない。 これを回避するために、コンストラクターとセッターを隠蔽して、エンティティのライフサイクルに基いた振舞いだけを公開するといった手法を紹介した。 これによってエンティティを不適切な状態にすることができなくなり、ライフサイクルを適切に管理することができるようになった。

集約の実装とLazy Loading

エンティティの設計においてオブジェクト同士の関連(参照)は避けては通れないものだ。 エンティティ単体では不十分で、関連するエンティティ同士が結び付いて始めて意味を成す場面は往々にしてある。 そのような場合、エンティティを他のエンティティの参照を保持させる集約 として設計する。

しかし無闇に関連を持たせた集約を設計すればいいというわけではない。 関連をたくさん持った巨大な集約というのは扱いづらいし、双方向の関連は複雑さを生む。 必要最低限かつ、単方向の関連しか持たない集約が望ましい。 そのためには、エンティティの参照を保持するのではなく、その識別子(ID)を保持させることも検討すべきだ。 この時、エンティティの参照を得るには識別子をリポジトリに問い合わせればいい。

N + 1問題

集約とそれが参照する各エンティティがデータベース内に別々に格納されているとして、集約のエンティティを複数取得するケースではパフォーマンスの悪化が懸念される。 いわゆるN + 1問題がその典型的なケースだ。 N個の集約が与えられた時、集約自身を得るクエリ 1 つと、その関連を辿るクエリN を足したN + 1のクエリを発行してしまうという問題である。

この問題の解決方法は単純で、そもそもクエリを分けずに JOIN を利用して1 つのクエリにしてしまうか、IN句を利用して関連を辿るクエリを 1 つにまとめてしまえばいい。 いわゆるEager Loadingと呼ばれる方法だ。 N + 1問題が発生する方法が関連を参照するまで遅延読み込み(Lazy Loading)するのと対比して、この方法では即時読み込み(Eager Loading)するからだ。

Lazy Loadingと N + 1問題の回避

N + 1問題を回避するためにLazy Loadingは避けて常にEager Loadingすべきかというとそうとも言えない。 ユースケースによっては集約の一部の関連しか参照しない場合もあるかもしれないが、この時Lazy Loadingが利用できればクエリの発行自体を避けることができるのでパフォーマンスで有利になる。 しかしN + 1問題が起きると途端にパフォーマンスが悪化してしまうので、N + 1問題の起きないLazy Loadingの実装があれば使い勝手がいい。 つまり、エンティティごとに個別に読み込みを遅延するのではなく関連ごとに遅延すればいい。

emonkak/ormにはこの方式のLazy Loadingが実装されている。 ocramius/proxy-managerを利用した簡単な実装だ。

利用例:

<?php

use Emonkak\Orm\Relation\Relation;
use Emonkak\Orm\SelectQuery;

$users = (new SelectQuery())
    ->with(Relation::lazyOneToOne(
        Profile::class,
        'profiles',
        'profile',
        'user_id',
        'user_id',
        $pdo
    ))
    ->with(Relation::lazyOneToMany(
        Post::class,
        'posts',
        'posts',
        'user_id',
        'user_id',
        $pdo
    ))
    ->from('users')
    ->getResult($pdo, User::class);

foreach ($users as $user) {  // $users自体も実際に走査されるまでクエリの発行が遅延されている
    $users->posts;  // Postが格納されたArrayObjectのProxy Object
    $users->posts[0];  // 実際にアクセスするまでクエリの実行は遅延される
                       // 一度アクセスした時点ですべてのUserのPostが読み込まれる

    $users->profile;  // ProfileのProxyObject
    $users->profile->introduction;  // Post の時と同様にアクセスするまで読み込みは遅延される
}

おわりに

集約の読み込みにおいてLazy Loadingを利用することでクエリの発行自体を回避してパフォーマンスを向上させることができる場面があるが、N + 1問題が発生してしまうと逆にパフォーマンスが悪化してしまうので扱いづらい。 そこでN + 1問題の起きないLazy Loadingの実装があると使い勝手がいいのでemonkak/ormに実装した。

LINQ to ObjectのメソッドをJavaScriptの個別のモジュールとして使うためのライブラリ

LINQ to ObjectのメソッドをJavaScriptの個別のモジュールとして使うことのできるライブラリを作った。 これは元々Feedponで利用するために作ったのもので、現在はライブラリ単体として切り出している。

@emonkak/enumerable

なお、今回特に解説しないが本ライブラリはPHP版のものも存在する。

Emonkak\Enumerable

設計

この手のシーケンス処理のためのライブラリは膨大なメソッド(関数)を提供するので、そのライブラリに依存するスクリプトのサイズが大きくなりがちだ。 そのため、lodashRxJSなどのライブラリは個別のメソッドをモジュールとしても提供している。 個別に提供することで必要なものだけを読み込むようになるので、スクリプトのサイズ減らすことができるからだ。

今回作成した@emonkak/enumerableも同様で、メソッドを個別にモジュールとして提供している。 さらに、それらのモジュールが他のモジュールになるべく依存しないようにすることで、スクリプトサイズの増加を最小限に抑えている。

では、lodashとの違いはどこにあるのかというと、APILINQベースであるということがまずある。 LINQAPIは一般的なシーケンス処理の設計とは異なり、Select() Where() などSQL文を彷彿とさせる命名がなされている。 慣れていないと少々とっつきにくいが、APIとしての機能性はとても優れているように思う。 特に Join()GroupJoin() のような結合処理ができるのはLINQならではで、他のライブラリ等だとあまり提供されない。 実際に開発する動機となったのはこの2つのメソッドが必要になったからだ。

もう一つの違いとして、@emonkak/enumerableではGeneratorを使って実装されたIteratorをターゲットにしたAPIなので、シーケンス処理が遅延される。 そのため、返り値として配列が必要な場合は適宜変換が必要だ。 Generatorを使っているのでサポート環境が限られるが、モダンブラウザではサポートが進みつつある。 サポート環境を絞ればトランスパイルなしで利用できるだろう。

使用例

import select from '@emonkak/enumerable/select';

Array.from(select.call([1, 2, 3], (x) => x * 2));  // => [2, 4, 6]

This-Binding Syntaxを使うことでメソッドチェインをすることができる。

import select from '@emonkak/enumerable/select';
import where from '@emonkak/enumerable/where';
import toArray from '@emonkak/enumerable/toArray';

[1, 2, 3]
    ::where((x) => x % 2 === 0)
    ::select((x) => x * 2)
    ::toArray();  // => [4]

しかし、This-Binding Syntaxはまだ提案段階なのでトランスパイルなしでは使用することはできない。 代替手段として拡張モジュールを使用することができる。 拡張モジュールは読み込むと、Enumerable クラスにメソッドが追加される。

import Enumerable from '@emonkak/enumerable';

import '@emonkak/enumerable/extensions/select';
import '@emonkak/enumerable/extensions/where';
import '@emonkak/enumerable/extensions/toArray';

new Enumerable([1, 2, 3])
    .where((x) => x % 2 === 0)
    .select((x) => x * 2)
    .toArray();  // => [4]

@emonkak/enumerable

LDRに代わる新しいRSSリーダーFeedponを開発した

f:id:emonkak:20170929225749p:plain

はじめに

LDRが終了するということで、FeedlyをバックエンドにしたWebブラウザ上で動作する新しいRSSリーダーFeedponを開発した。 現在の所FirefoxのアドオンChromeの拡張として配布している。 他にモバイル版として、CordovaでパッケージングしたAndroidiOSアプリがあるものの、ストアでは配布していない(お金がかかるので)。 モバイル版を利用する場合は各自でビルドしてインストールする必要がある。

本稿は今回開発したFeedponの設計思想とその特徴を語るものである。

背景

自分が長年愛用していたRSSリーダーLDR(Live Dowango Reader)が2017年8月31日をもって終了と告知された。 終了の告知はLivedoor時代から数えると2度目で、今回はいよいよ本当に終了した。

実は、一度目の2014年10月の終了告知から、新しいRSSリーダーを作ることを構想し実装を進めていた。 LDRが終了するということで代替手段が必要になったが、他のサービスでは満足いくものが見付からなかったからだ。

しかし、この時はLDRの権利がドワンゴに譲渡されることになり、サービスの再開が決まった。 ここで一旦、新しいRSSリーダーの開発は止まっていた。 それを今年(2017年)に入ってから再開して、同年7月に一応の完成を迎えた。 その後すぐ、ある意味タイミング良くLDRの2度目の終了が告知され、公開に至るという流れだ。

バックエンドとフロントエンド

RSSリーダーの機能はバックエンドとフロントエンドに大別することができる。 フィードの巡回、購読や未読の状態を管理するのがバックエンド、対してフィードを読むためのUIを提供するのがフロントエンドだ。

バックエンドの機能についてはサーバーに実装されることが多い。 バックエンドがサーバー、フロントエンドがWebで提供されるのが、Webベースのサーバー・クライアント型のRSSリーダーだ。 サーバー・クライアント型は、購読・未読の情報をサーバー上に保存するので、それらの情報を複数の端末で共有できる。 また、フィードの巡回はサーバー上で自動的に行うので、ユーザーの端末に負担は掛からない。

一方、バックエンドとフロントエンドが一体となったスタンドアローン型のRSSリーダーも存在するが、現在の主流ではない。 複数端末で情報を共有できないし、巡回を自動で行なってはくれないので、使い勝手として劣るからだろう。

以上のことから、今回開発するFeedponについてはWebべースのサーバー・クライアント型を採用することにした。

バックエンドサーバー

Feedponでは外部サービスのAPI(Feedly)を利用しており、自前のバックエンドサーバーは用意していない。 ここではその理由について説明する。

ます、バックエンドサーバーの具体的機能を整理してみる。

  • 購読管理
  • 未読管理
  • フィードの巡回
  • エントリのお気に入り
  • フィードの検索

機能数は少なく、どの機能もシンプルで、実装上の工夫を挟む余地は少ない。 このことから、異なる実装のバックエンド同士を差別化するのは難しい。 差別化が難しいのであれば、サーバーを自前で用意するメリットも少ないだろう。

唯一差別化が容易な点としては、API応答時間や、フィードの巡回間隔などの、パフォーマンスに関する部分だろう。 パフォーマンスを最適化するのであれば、高スペックのサーバーと、効率的なサーバーアプリケーションの実装を用意するのはもちろんのことだが、サーバーにアクセスするユーザー数を制限するのが最も効率的だ。 今はコンテナ技術や各種自動化技術によりサーバー実装を配布することも容易なので、そのために自分専用のサーバーを立ち上げるというのはそう難しくないだろう。 しかしながら、難しくなはいといっても誰でもできるほど簡単とは言えないし、サーバーの稼働コストが掛かってしまうという難点もある。

現時点ではFeedlyをバックエンドサーバーとして利用しているが、特に不満はなく、サーバー実装を自前で用意するつもりはない。 しかし、LDRGoogle ReaderがそうであったようにFeedlyもいつかは終了してしまうかもしれない。 それに備えてバックエンドを交換可能にできるようにクライアントを実装している。 もしFeedlyのサービスが終了したとしても、別のサービスに移行してもいいし、自前で用意してもいい。 RSSリーダーのような日常的に使うツールは長く使えるべきだと考えているので、特定の外部サービスに密結合することは避けている。 LDRは10年近く使っていたので、Feedponについても同じくらいは使うことを考えている。

クライアント

クライアントはRSSリーダーの使い勝手に大きく影響するUIを提供するものである。 ではこのUIに表示するものは具体的に何かと言うと、RSSあるいはATOMの形式で配信されたXML文章である。 そして、このXML文章の中にはエントリの本文がHTML文章がとして含まれる。 このことから、RSSリーダーのUI上にはHTML文章を表示する必要があり、そのためにはブラウザエンジン(HTMLレンダラー)が必要である。

そのため、UIをネイティブ部品を使ってネイティブアプリケーションとして作成するよりも、ブラウザ上で動作するWebアプリケーションとして実装するのがシンプルだ。 ネイティブアプリケーションとして実装しても、本文の表示にはブラウザエンジンが必要になるので、ネイティブのUIにWebViewを載せる形になるからだ。 それよりも一貫してすべてWebベースで実装してしまうのがシンプルだろう。

もちろんパフォーマンスについてはネイティブの方がいくらかは優位だろうが、クロスプラットフォーム展開が容易という点からも、Webアプリケーションの方がいいと考えた。

Webアプリケーションとして実装するのであれば、LDRがそうであるように純粋なWebサービスとしてドメインを取って配信することがまず考えられる。 しかし、Feedponはバックエンド(Feedly)との通信の他、エントリの全文取得、ブックマーク数の取得など様々な箇所でクロスドメイン通信を利用するので、これは不可能だ。 これらすべての通信でCORS(Cross-Origin Resource Sharing)が利用できれば良いが、当然そうはいかない。 そのため、Webサービスとしてドメインを取って運用するという手段は取れない。

では、Webアプリケーションを動かすためのプラットフォームが必要となる。 まず最近流行しているElectronが思い付くが、これは選択肢として入れることはなかった。 何故かというと、自分の使い方だとRSSリーダーでエントリを読んでいる最中に大量のリンクを開くからだ。 リンクを開く度にElectronとブラウザを行き来するのではとても使いづらい。 同じブラウザウィンドウのタブ上で開いて欲しいのだ。 そのために、普段使っているブラウザ上で動作するのが望ましいだろう。

ブラウザ上で動作するWebアプリケーションであれば、ブラウザ拡張(Extension)がある。 幸いにもブラウザ拡張はGoogle ChromeAPIをベースに標準化されつつあるので、Chromeの拡張を作れば対応ブラウザでであればそのまま動作させることができるようになりつつある。 Feedponは始めChorme拡張として開発していたが、実際にコード改変なしでChrome拡張をFirefox拡張として動作させることができた。

モバイル対応

前述のクライアントの項はPCを対象としたものだったが、今の時代モバイル対応も必須だろう。 LDRには使いやすいスマホアプリがなく、レスポンシブの考え方がなかった時代のものなので直接Web経由で見るのも困難で、スマホ利用がしづらかったのが不満だった。

Feedponは、モバイル版もPCと全く同様のWebベースの実装にして、レスポンシブデザインで表示を切り替えるようにした。 実装が同じなのでメンテンスコストが抑えられるとともに、手間を掛けずとも同じ操作感を実現できるものもメリットだ。 もちろんモバイルならではの工夫でより使いやすくすることもできるだろうが、違和感なくモバイル版とPC版を使い分けたかったので、同じ操作感を実現する方向に舵を切った。

モバイルであれば、ネイティブアプリケーションでの実装はパフォーマンスの観点では圧倒的に有利だろう。 しかし、実装に大きな手間が掛かってしまうのであえて避けた。 また、モバイルのネイティブアプリケーションは開発環境の変化がとても大きいので、メンテナンスコストも馬鹿にできない。 対して、Web標準技術で実装すれば大きくメンテナンスの頻度を抑えることができる。 現にLDRは10年以上も稼動していたが、サービスの終了さえなければまだまだ使うことができた。 長く使うツールだからこそ、メンテンナンスの頻度は極力抑えたい。 そのためにもモバイル版もWebアプリケーションとして実装している。

モバイル版のアプリケーションのパッケージングにはApache Cordovaを利用した。 CordovaでiOSAndroid、Window Phone等様々なプラットフォームに対応しているので、マルチプラットフォーム対応も難なく可能だ。 現在の所FeedponはiOSAndroidに対応している。

UIデザイン

f:id:emonkak:20170929224156p:plain

UIデザインはLDRのものを参考にして、モダンでシンプルなデザインを心掛けた。 コンセプトとしては印刷物としても成り立つデザインということで、Vertical Rhythmにも気を使っている。

印刷物としても成り立つデザインというのは、いわゆるフラットデザインにも通ずる所がある。 フラットデザインというのはWebデザインが紙媒体のデザインに近付いていく仮定だと考えている。 これは高DPIのディスプレイが普及してきたことと無関係ではない。 DPIが限りなく印刷物に近づけばWebデザインも当然のことながら紙媒体のデザインに近付いて行くという訳だ。 そうであれば、これからのWebデザインはDTPのノウハウを活かす機会が多くなるだろう。

フラットデザインについてよく議論の対象となるのは、ボタンなどの操作可能なUI部品のデザインをどうするのかというものがある。 操作可能なUIかどうかが分かりづらいという批判がまさにそれだ。

ここでレイヤーという概念を導入して、操作可能な部品には影を付けるというアイディアがマテリアルデザインだろう。 操作可能な部品に影を付けることで、そうではない部分との違いが明確になって分かり易くなる。

Feedponの場合、RSSリーダーという日常的に使うツールなので、操作可能な部品がそう分かり易い必要はないと考えている。 少々分かりづらくても日常的に使っている内に操作を覚えて慣れてしまうからだ。 それよりも、デザインとしての一貫性と可読性を重視して、一貫してフラットなデザインにした。

レスポンシブデザイン

モバイル向けのデザインについてはレスポンシブにして、共通化を図っている。 デザインはモバイルのものがベースで、そこから微調整をしてデスクトップ向けのデザインが存在する。 デスクトップのデザインをベースにしてしまうと、ボタンなどの操作可能な部品が小さくなってしまい、ディスプレイの小さなモバイル端末でタップすることが困難になってしまうからだ。 そのため、モバイル端末での表示を考慮したデザインでは余白を大く取る傾向にある。 余白を多く取ることでモバイルでの操作性や、可読性も向上するが、一方で一画面に表示できる情報量については劣ってしまう。 デザインリニューアルで一画面に表示される情報量が減ってしまって不評を買うというのは度々ある話だ。

Feedponでは余白を比較的広く取ってはいるが、情報量が減りすぎないように微妙な所で調整している。

特徴

ここではLDRにはないFeedponならではの特徴的な機能について述べる。

文展

f:id:emonkak:20170929224219g:plain

フィードに含まれるエントリの情報にエントリの全文が含まれいないことが度々ある。 これでは使い勝手が悪いので、様々なRSSリーダーでエントリの全文を取得できるようにするユーザースクリプトが往々にして提供されている。 LDRで言えばLDR Full FeedFeedlyであればFeedly Full Feedだ。

Feedponではこの機能を初めから本体に内蔵している。 仕組みとしては前述のユーザースクリプトと同様にWedata上で管理されているSiteinfo情報を利用している。 この仕組みは古くからあるもので、今となってはWebではあまり使われないXPathを使ってエントリ本文のノードを特定している。 それこそ今だと流行りのAIを使って本文を抽出するというアプローチもありうるのかもしれないが、この古い仕組みでも十分に機能する。 むしろこの仕組みだと直接本文のあるウェブサイトに直接クライアントからアクセスするので、外部APIに本文の抽出を依頼するのに比べ、ラウンドトリップタイムを最小に抑えられるというのがメリットだ(APIでキャッシュが効いていればその限りではないが)。

キーボードショートカット

f:id:emonkak:20170929224218p:plain

Feedponでは多数のキーボードショートカットが提供される。 そしてそのすべてをカスタマイズすることが可能だ。

現時点ではデフォルトのキーボードショートカットは以下のようになっている。 マッピングVimライクな記法で行い、マルチキーストロークマッピングも可能だ。

Key Command
/ Search subscriptions
<Escape> Close sidebar
<S-Space> Scroll page up
<Space> Scroll page down
? Show help
A Select previous category
G Go to last line
R Reload stream
S Select next category
V Visit website
a Select previous subscription
b Toggle comments
c Toggle stream view
f Fetch full content
gc Clear read entries
gg Go to first line
gm Mark all entries as read
h Close entry
i Open URL
j Select next entry
k Select previous entry
l Expand entry
p Pin/Unpin entry
r Reload subscriptions
s Select next subscription
v Visit website
z Toggle sidebar

はてなブックマーク連携

f:id:emonkak:20170929224217p:plain

RSSリーダーには人それぞれ様々な使い方があると思うが、配信されるすべてのエントリを読むという人は少ないだろう。 自分の場合、エントリを読むかどうかはタイトルと、そのエントリのはてブの被ブックマーク件数で判断している。

Feedponではエントリごとにはてぶの件数が表示され、件数の多い場合は件数の表示が目立つようになるので、注目されているエントリが一目瞭然だ。 また、エントリのパーマリンクRSS用のトラッキングURLが設定されている場合、はてブの件数を正常に取得できないので、トラッキングURLを展開する機能も内蔵している。 さらに、はてブのコメントを表示する機能も内蔵しているので、コメントもシームレスに読むことができる。

もちろん、はてブを使わない人もいるだろうし、そんな人にとっては不要な機能だろう。 しかし、そもそものコンセプトはLDRの代替として「自分のために作るRSSリーダー」ということなので、この機能を入れている。 はてブの件数を、Facebookのいいねの件数にするだとか、はてブのコメントをエントリに関するツイートの一覧にするなど考えられるが、自分が使わないという理由で、はてブ決め打ちとなっている。 もしかすると、今後そのようにカスタマイズできるようにするかもしれないが今の所その必要を感じていない。

おわりに

今回LDRに代わる新しいRSSリーダーFeedponを開発した。 個人的にはとても便利に使っていて、もうこれなしではいられないというくらい毎日使っている。 そもそも自分が使うために作ったプロダクトなので万人に使い易いとは限らないものの、自分と同じように気に入る人がいればいいとの思いで公開した。

もし、何か要望や問題があった場合はGithubIssuesにて受け付けている(日本人ユーザーしかいないと思うので日本語OK)。

旅部まとめ

ライフログチャンネル旅部チャンネルにおける旅部の放送まとめ。

# Date URL Place Cast
1 2014/01/26 http://live.nicovideo.jp/watch/lv170879155 ドライブ 横山緑、力也、14
2 2014/02/28 http://live.nicovideo.jp/watch/lv170879155 名古屋 横山緑、本間智則、14、ふかみん
3 2014/05/21 http://live.nicovideo.jp/watch/lv176706778 福井 横山緑、石川典行、えりりか、力也、本間真、野田総理なっちゃん
4 2014/06/07 http://live.nicovideo.jp/watch/lv181013988 キャンプ 横山緑、石川典行、本間真、その他
5 2014/07/01 http://live.nicovideo.jp/watch/lv183430282 江ノ島 横山緑、石川典行、力也、本間真、あっきーなしゃま、野田総理、ももえり、その他
6 2014/08/07 http://live.nicovideo.jp/watch/lv185681203 韓国 横山緑、石川典行、力也
7 2014/09/27 http://live.nicovideo.jp/watch/lv192086983 沖縄 横山緑、力也、NER
8 2014/10/24 http://live.nicovideo.jp/watch/lv196711673 草津 横山緑、らみあ、力也
9 2014/11/25 http://live.nicovideo.jp/watch/lv198668560 大阪 ティロ・フィナーレ加川、本間すなお、14
10 2014/12/06 http://live.nicovideo.jp/watch/lv199282132 初島 横山緑、NER、力也
11 2015/06/19 http://live.nicovideo.jp/watch/lv223372925 浜名湖 横山緑、よっさん、力也
12 2015/07/18 http://live.nicovideo.jp/watch/lv227571405 大阪 横山緑、力也、団長
13 2015/10/03 http://live.nicovideo.jp/watch/lv236123298 福岡 横山緑、NER
14 2016/02/09 http://live.nicovideo.jp/watch/lv251659007 北海道 横山緑・石川典行
15 2016/05/22 http://live.nicovideo.jp/watch/lv260920055 鎌倉 横山緑、ハニー大木、シケキノコ
16 2016/12/20 http://live.nicovideo.jp/watch/lv284160287 沖縄 横山緑、大島薫
17 2017/03/13 http://live.nicovideo.jp/watch/lv292820296 九州・佐賀 横山緑、堀田みなみ
18 2017/04/09 http://live.nicovideo.jp/watch/lv295143445 キャンプ 横山緑、まりにゃん、AFちゃん、ロハコ、jan発進
19 2017/07/01 http://live.nicovideo.jp/watch/lv301795761 北海道 横山緑、野田草履、高田健志
20 2017/09/16 http://live.nicovideo.jp/watch/lv306534663 大阪 横山緑、NER、ティロ・フィナーレ加川、野田草履