僕とDDDとClean ArchitectureとやっぱりDDD

2022/04/21更新 ふりかえってみて、この記事は手段と目的をごっちゃにしちゃった自分がよくわかる記事です。 DDDは「どうやってコードを書くか」が問題ではありません。その点を勘違いしちゃってるエンジニアの話として、続きを読みたい人は読んでください🙏

DDD(Domain Driven Design)って難しいですよね。難しい難しいとばかり考えていた僕もようやく最近になって少しずつわかってきた気がします。そのきっかけとなった書籍と僕のストーリーを本記事で紹介できたらと思います。

TL;DR

  • Clean Architectureはなんとなくわかる
  • DDDは難しい

と感じている人は「Domain-Driven Design in PHP」を読むと道が拓けるかもしれない。

leanpub.com

僕とDDD

DDDといえばEvansのドメイン駆動設計:

そしてVernonの実践ドメイン駆動設計:

が有名です。「ドメイン駆動設計」の内容をより実際の設計に当てはめている「実践ドメイン駆動設計」の方がわかりやすいという意見を聞いたりしますが、僕にとっては両方ともなかなかに難易度の高い書籍です。少しずつ読んでは「なんとなく言ってることはわかる」と思いながらも今ひとつ身についた気がしない類のものでした。このように感じている人はそれなりにいるんじゃないかなと思ってます。

僕とClean Architecture

DDD難しいし、実際のプロジェクトに適用しようと思っても手が止まる。。。

ということでしばらく本棚に戻していたのですが、次に見つけたのがClean Architectureなるものでした。Qiitaの記事で見つけたのがきっかけだったのですが、実際に自分の馴染みのあるコード(Rest APIだったりAndroidのアプリだったり)で書かれていたこともあって「これは実際のプロジェクトでも書けそう」と思えたのが「僕とDDD」の出会いとの大きな違いかなと思います。

実際にはQiitaの記事だけでは心もとないので、以下のような記事や講演で学びながら実案件でも適用していきました。

Uncle BobのClean Architectureの記事

blog.cleancoder.com

Uncle BobによるClean Architectureの講演

www.youtube.com

(いくつかありますが、最初にみて面白かったのがこれです)

Uncle Bobと息子のMicah MartinがペアプロしながらWebシステムを作る講義

learning.oreilly.com

GitHub上のソースコード

github.com

Clean Architecture(書籍)

僕がClean Architecture学び始めたころはまだ無かったのですが、正式に書籍も発売されています。

Clean Architectureでモヤッとしたこと

Clean Architectureを採用したプロジェクトでは、オレオレClean Architectureでありながらもそれなりにルールを守った自信はあります(特に依存関係やScreaming Architecture部分)。が、いざ実装するとなったときに「ロジックをどこに置くのか」というところで結構悩むことがありました。

  • ○○ロジックはUseCase(Interactor)に入れる?
  • △△ロジックはEntityに入れる?
  • 同じようなロジック(例えばValidation)が複数箇所にあるけどこれは誰の役割?
  • などなど

Clean Architectureの書籍に「第20章 ビジネスルール」という章があるのですが、

  • エンティティは最重要ビジネスルールを含む
  • ユースケースアプリケーション固有のビジネスルールを含む

と言われてもなかなかエモくて直感的にわかりにくいと感じました。そして結構こういうところがコードレビューでチーム内で意見が分かれたりして変に時間がかかっちゃったりしたのを覚えています。「Clean Architecture良い!」という思いとともに「まだすっきりしない点が残る」という状態が続きました。「やっぱりDDDなマインドが足りていないからなのか?」ということでDDD学習に戻ることにしたのがここ1年くらいの話です。

DDDとClean Architecture

「実践ドメイン駆動設計」を再び読み直してみたのですが、不思議なことにClean Architectureを経てから読み直してみると見えてくるものが変わりました。たぶんですけど、

馴染みのあるコードからClean Architectureを学んだ
↓ 
Clean Architecture(書籍)を読んだら答え合わせのように腑に落ちる点が多かった
↓
この書籍内にドメイン駆動的な話も入っていた(んじゃないかと思う)
↓
無意識のうちにDDDの概念が少し増えた状態で改めて「実践ドメイン駆動設計」を読み始めた
↓
書籍内に出てくる単語や概念に馴染みが増していたので以前より想像しやすくなった

ということだと思います。

ただ、以前より理解がしやすくなったとは言え、モヤッとは残ったままで、以下のトピックを理解したいと思いつつもいまいちピンとこない感じでした。

  • Application Service
  • Domain Service
  • Aggregate
  • Entity
  • Value Object
  • Domain Event

結局繰り返しになるんですけど、「なんとなく言ってることはわかる」で止まってしまって、いざ自分のコードに入れようとすると「どうしたら良いんだろ?」になっちゃうんですね。(つまりわかっていない)

今にして思うと「わかろうとするモチベーションが足りなかった」だけな気もします。

そんな中最近「Domain-Driven-Design in PHP」という本に出会いました。

Domain-Driven-Design in PHP

leanpub.com

O'Reillyのサブスクリプションがある方はこちらのリンクからも読むことができます!

この書籍は結構前から知っていたんですけど、PHPというだけで避けてました。(ごめんなさい。でもそう思った人は僕だけじゃないはず!)

最近は毎日のようにPHP触ってますし、PHPへの抵抗も昔よりだいぶ無くなったので読んで見ることにしました。

久しぶりにこんなにワクワクする技術書を読んだ。

というのが素直な感想です。僕がちょうどモヤッとしているところとジャストミートな内容だった、というのもあるんだと思いますが:

  • ちょうど良いボリューム感
  • ドメイン駆動設計」, 「実践ドメイン駆動設計」, そして「Clean Architecture」にも言及している
  • DDDの概念とリンクする豊富なサンプルコード
  • 書籍用に作ったシステムがGitHub上に公開してある

という4つのポイントが僕の中で高評価です。

ちょうど良いボリューム感

ページ数は394ページとそれなりにあるのですが、コードが占めている比率が高いので思ったよりすぐ読めます(僕は3,4日で読めました)。

目次は以下の通り。

* Getting Started with Domain-Driven Design
* Architectural Styles
* Value Objects
* Entities
* Services
* Domain Events
* Modules
* Aggregates
* Factories
* Repositories
* Application
* Integrating Bounded Contexts
* Appendix: Hexagonal Architecture with PHP

「実践ドメイン駆動設計」と比べてみても、概ねトピックはカバーできていると言えます。

* Getting Started with DDD
* Domains, Subdomains, and Bounded Contexts
* Context Maps
* Architecture
* Entities
* Value Objects
* Services
* Domain Events
* Modules
* Aggregates
* Factories
* Repositories
* Integrating Bounded Contexts
* Application
* Aggregates and Event Sourcing: A+ES

ドメイン駆動設計」, 「実践ドメイン駆動設計」, そして「Clean Architecture」にも言及している

要所要所でEvansの「ドメイン駆動設計」とVernonの「実践ドメイン駆動設計」の引用を使って説明をしてくれます。

例えばDomain Eventについて「実践ドメイン駆動設計」からは:

Vaughn Vernon defines a Domain Event as:

An occurrence of something that happened in the domain.

そして「ドメイン駆動設計」からは:

Eric Evans defines a Domain Event as:

A full-fledged part of the Domain Model, a representation of something that happened in the Domain. Ignore irrelevant Domain activity while making explicit the events that the Domain Experts want to track or be notified of, or which are associated with state change in the other Model objects.

というように両者を引用で出してくれます。書籍内でのわかりやすさも増すのですが、この引用をもとに「ドメイン駆動設計」と「実践ドメイン駆動設計」をリファランスとして要所要所で読み直していける点もすごく良いです。

また、さらにうれしいのはUncle BobのClean Architectureにも言及している点です。(この書籍が発売したときには「Clean Architecture」はまだ出版されていないので、あくまでUncle Bob(Robert C. Martin)について言及しています)

As Robert C. Martin says: The Web is a delivery mechanism [...] Your system architecture should be as ignorant as possible about how it is to be delivered. You should be able to deliver it as a console app, a web app, or even a web service app, without undue complication or any change to the fundamental architecture.

DDDの概念とリンクする豊富なサンプルコード

(ちょっと大げさですけど)文章よりコードが多いんじゃないかってくらいサンプルコードが都度紹介されています。そして、コードの要所要所に関して丁寧にわかりやすく説明がされているので、DDDの概念とコードがどう結びついていくのかが想像しやすいです。

書籍用に作ったシステムがGitHub上に公開してある

書籍用に作られた「Last Wishes」というシステムのコードがGitHub上に公開されています。

Last Wishesトップ画面

コンセプトは「ユーザーが亡くなった場合に、あらかじめ登録されていたお願い(Wish)を、指定したメールアドレス宛に送る」というものです。お願い(Wish)を登録した場合にはゲーム要素もあって、一定ポイントたまるとバッジが付与されるようになっています。

このシステムは大きく2つのアプリケーションに分かれています。

  • ユーザーのWishを管理し、メールを送信するコア部分のアプリ
  • ゲーム要素を提供する、Wishに応じたポイントを換算し、ユーザー毎に管理するアプリ

それが各々以下で公開されています。

github.com

github.com

システム構成は下の図のようになっていて、それなりにまともなメンツが揃ったシステムになっています。

Last Wishesの概要図

サンプルアプリではあまり見られない:

  • CQRSとEvent Sourcing
  • Bounded Context(Last WishesとLast Wishes Gamify)

というのも学ぶことができるのでかなり貴重な素材だと思います。このシステムと書籍を照らし合わせていくことで答え合わせもしていけるので、理解が進みやすいという印象もあります。

まとめ

  • 「Domain-Driven-Design in PHP」を読んだ
  • 馴染みやすいコードでDDDを体系的に学べる良書だった
  • ドメイン駆動設計」「実践ドメイン駆動設計」「Clean Architecture」をリンクさせてくれる1冊だと思った

と、思いをつらつらと書きましたが、同じように迷い、悩んでいる方がもしいるのであれば「Domain-Driven-Design in PHP」はかなりおすすめできる1冊なのでぜひ読んでみてください!ということを伝えたかったです。

この記事が何かしら一歩先に進めるきっかけになれば幸いです。

今後の予定

  • Vernonの「実践ドメイン駆動設計」を読み直す
  • Evansの「ドメイン駆動設計」を読み直す
  • Uncle Bobの「Clean Architecture」を読み直す
  • Last WishesをLumenに移行してみる
  • ↑ができたらgoにも移行してみる
  • 自分でScratchからアプリを作ってみる

という流れで精進していけたらいいなと思います!

Zoomで読書会「アーキ部#2」に参加しました!

会社のSlackで流れていたのを見たのがきっかけで「アーキ部」なるものに初参加しました!

connpassでのイベントはこちら↓

architect-club.connpass.com

概要

connpassにも記載されてますが、Release It!: Design and Deploy Production-Ready Software (Second Edition) のオンライン読書会です!

Release It!: Design and Deploy Production-Ready Software

Release It!: Design and Deploy Production-Ready Software

目次

  • Ch.4 Stability Antipatterns(安定性のアンチパターン)
    • Integration Points(統合点)
    • Chain Reactions(連鎖反応)
    • Cascading Failures(カスケード障害)
    • Users(ユーザ) ←ここの途中まで

参加するにあたって準備するもの

Zoomのクライアント

zoom.us

勉強会はオンラインで開かれていて、Zoomを使います。今回は20時ごろにconnpass経由でZoomのURLが送られてきました。Zoomのクライアントはあらかじめ用意しておくとスムーズに参加できそうです。

参加人数は今回40人を超えていましたが、主催者の共有画面を参照するだけですし、参加者もみなビデオはOFFにしてるので回線速度が気になることは無かったです!

f:id:kenev:20190726231814p:plain
参加中のZoomの画面

Release It!: Design and Deploy Production-Ready Software (Second Edition)

本は無くてもOKですが、あればより理解を深めることができるんじゃないかなと思います。Second Editionは現時点(2019-07-26)では英語版しかありません。(間違えて初版の日本語版を買わないように注意!)

ちなみにSafari Books Onlineを購読している人であれば下記にあります。

learning.oreilly.com

ドリンク、軽食

オンライン勉強会なのでご自由に、というところですね!マイクONにしてボリボリ何か食べちゃわないようには気をつけたほうがいいですね(笑)

参加してみた感想まとめ

  • 司会(@kawasimaさん) が本の内容とともに実体験を話してくれるのですごく良い
  • 本の内容とともにたまに関連記事を紹介してくれるのうれしい
  • ハッシュタグ#アーキ部)で一緒に参加している人のツイートも見れるの良い
  • オンラインなので、勉強会が普段遠隔にしかない人(例えば山に住んでるような僕)にとっては本当にうれしい
  • オンライン勉強会に懇親会が最後に無いのが寂しいですが、この課題は難しい

本の内容に関してピックアップ

  • 統合点はシステムにとって殺し屋No.1
  • (ダイアグラムを見て)新米アーキテクトは箱にフォーカスするが、ベテランアーキテクトは、矢印により着目する
  • ぐっすり寝れるかどうかが安定性のKPI
  • 明らかに遅いレスポンスはレスポンスが無いことよりも悪い
  • ディフェンシブなコードになってないと、道連れに自システムも障害してしまうことになる
  • バグのあるアプリケーションをオートスケールすると、札束がとんでいく

次回!

たぶん日程はまだ決まっていないのですが、connpassのメンバーになっておけば通知がとんできますので、興味がある方はぜひ!

architect-club.connpass.com

このイベントをきっかけに「Release It!」読み始めましたが、かなり面白いです。が、リアルなので胃も痛くなってきます。書評にも挑戦したいですね。

関連記事

t.co

t.co

「CircleCIのconfig.ymlを守ろうとした話」を発表しました

「【大阪】CircleCI ユーザーコミュニティミートアップ #1」に参加し、「CircleCIのconfig.ymlを守ろうとした話」という題名でLTをしてきました。

circleci.connpass.com

発表資料

ストーリー

組織の中でCircleCIを多数のリポジトリで使い始めたとき、config.ymlに一定のルールを課したくなりませんか?セキュリティ的な意味と、Lint的な意味の両方で僕は欲しくなってきます。

ということでconfig.ymlにどのようにガバナンスを効かせることができるのか、という試みについて話しました。主にconftestでCIにルールを課す方法について述べています。

興味がある方はぜひ下の記事も読んでみてください!

kenfdev.hateblo.jp

また、ルールを書くときに用いているRegoについては、Open Policy Agentとともに以下の記事で紹介しています!

kenfdev.hateblo.jp

僕の発表内容には答えがなく、「みなさんはどのようにガバナンス効かせていますか?」と問うて終わりました! いろんな人のconfig.yml話聞きたいですねー!

おまけ

質疑応答コーナーがあったのですが、「config.ymlへのガバナンスの効かせ方で悩んでいる方いますか?」という質問に誰も反応しなかったので、僕が勝手に悩んでいるだけなのかもしれない、ということはそれとなくわかりました(笑)

そして、イベントでは「ワタシハサークルシーアイチョットデキル」Tシャツいただきましたー!感謝感謝です! f:id:kenev:20190717002821j:plain

Netlifyへのデプロイがタイムアウト

最近は仕事から趣味まで作ったもの(主にフロントエンド関連)のデプロイが簡単にNetlifyにできてしまうので多用しています。

www.netlify.com

直近ではVue.jsのコンポーネントをStorybookで公開しようとしたのですが、単純なタスクなはずが思いの外ハマってしまったので対処法をシェアしようと思います。

TL;DR

問題

npm run build のようなビルドタスクが30分以上かかってしまってNetlify側でタイムアウトしてしまう

解決方法

ビルド時のログの出力量に注意!特にwebpackのようにビルド時に大量にログを出力するツールを使う場合は、出力しないオプションを付加しましょう

Netlifyがタイムアウトする

発生していた問題とは、Netlifyがタイムアウトするというものです。

f:id:kenev:20190708214549p:plain

上の画面はVue.jsのStorybookをビルドしている最中のものです。静止画だけで伝えられないのですが、ビルドの進捗がものすごく遅いんです。このときは開始したのが12:45ごろだったので、およそ30分くらいこの調子でちょっとずつ%が上がるだけで、そのあと静かにデプロイが失敗します

サポートに問い合わせ

Netlifyはエラー文言も無しに静かに失敗し、ローカルではなんの問題もなくサクッとStorybookを開始できるのでお手上げでした。ということでサポートに問い合わせしてみることにしました。デプロイに失敗すると簡単にサポートに連絡できるようにリンクが用意されています。

f:id:kenev:20190708215150p:plain
問い合わせ

ここから問い合わせフォームが下のように入力できるようになっていて、Netlify側もデバッグしやすいようにリンクも自動で貼り付けてあるので、送信するだけです。

f:id:kenev:20190708215808p:plain

僕の場合、送信してから半日くらいでサポートから返事がきました。

内容を要約すると

「ビルドのログが多すぎて、Netlifyの Log Serviceが問題を起こしていた。Storybookのビルドするなら —quiet オプション付加してみて」

ってことでした。

なるほど、確かにログの表示が変に空白が多かったりしてましたね。

ログ出力量に注意!

ということでbuild-storybook—quietオプションをつけることにしました。公式サイトでもドキュメントされています。

https://storybook.js.org/docs/configurations/cli-options/

更新して再度デプロイしてみると、サクッと終わってサクッとNetlify上でStorybookが公開されました!

変に長時間悩み続けることなく、サポートに問い合わせてみてよかったです!

まとめ

  • Netlifyのビルドタスクのログ出力量には要注意
  • 可能であればログ出力が抑えられるオプションを付加しておくこと
  • NetlifyのサポートのUX最高でした

調べても同じ問題に遭遇している人を見つけられなかったので、この記事が誰かの助けになればと思います。

PHPの`$`と`->`がつらくなったらマクロを使えば良いことに気づいた

最近PHPを書くことが多いのですが、変数の前につける$(ドル記号)とメソッド呼び出しやプロパティ呼び出しに使う->(矢印)の指の動きの効率悪さに耐えられなくなりました。。。

PHPStormを使っているといい感じに$を入力しなくても変換してくれたりするのですが、その動きもまた気持ち悪く感じてしまうのでどうしたものかと思っていました。

f:id:kenev:20190624210413g:plain
慣れないと気持ち悪い

そこで今更ながら発見したのがエディタの「マクロ」機能です!

pleiades.io

マクロ作成

f:id:kenev:20190624210909p:plain

PHPStormであれば上のキャプチャのように「Edit > Macros > Start Macro Recording」を実行することでマクロの記録を始めることができます。

記録中は下図のようなインジケーターが表示されます。

f:id:kenev:20190624210939p:plain
マクロ記録中

ここでマクロとして入力したいキーを押します。$用のマクロを作りたいので$を入力すると、記録されているのがわかります。

f:id:kenev:20190624211226p:plain

そして赤い■をクリックするとマクロの記録が終わるので、このマクロに名前をつけてあげます。わかりやすく$にしておきます。

f:id:kenev:20190624211317p:plain

これでマクロは完成です!

同じ要領で->も作っておきましょう。

マクロのショートカット作成

マクロが完成したら、今度は自分の好きなショートカットでこれが入力されるようにします。

「Preferences > Keymap」に進んで、虫眼鏡のところでmacroと入力しましょう。下のように絞り込まれるはずです。

f:id:kenev:20190624211651p:plain

ここに先程作成した$->があるのがわかります。あとはここに自分の好きなキーの組み合わせを設定することで、以後このキーを入力すれば$->が入力できるようになります!

ちなみに僕は

  • $ctrl+s
  • ->ctrl+.

という設定にしました。ちょっとですけど快適になった気がします!

他の皆様がどうしているのか気になったりします(むしろ気にしてないですかね。。。)

まとめ

  • PHPStormのマクロを設定した
  • マクロならコードスニペットとは違ったショートカットを作ることができる

VSCodeであれば下記拡張機能を使えばできそうな気がします!

github.com

explainshell.comはシェルの強力な助っ人

f:id:kenev:20190616075801p:plain
explainshell.com

最近explainshell.comなるものを今更ながら知ることができたので紹介します!

explainshell.com

概要

シェルコマンドを全部覚えるのって大変です。なんとなく覚えてても、オプションがどんな意味をもっていたかまではなかなか思い出せないです。おまけにシェルは強力なのでパイプ(|)でどんどん繋げていい感じにコマンドを書いていくことができます。そんな芸術ともいうコマンドを見ては、manを見たりググったりして、分解しながらコマンドを理解していっているのはきっと僕だけじゃないはず。

そこで便利なのがこのexplainshell.comというサイトです!

使い方

使い方はいたって簡単で、インプットに知りたいコマンドを入力するだけ。

試しに最近覗いてみた下のk3sのインストールスクリプトから抜粋してみます。

https://get.k3s.io

lsof | sed -e 's/^[^0-9]*//g; s/  */\t/g' | grep -w 'k3s/data/[^/]*/bin/containerd-shim' | cut -f1 | sort -n -u

f:id:kenev:20190616080716p:plain

すごい!全部のコマンドに丁寧な説明がいい感じにつくんです!

そして「←→」で順番にコマンドの解説を表示していくことも可能。

f:id:kenev:20190616081012g:plain

とってもシンプルですけど、ブックマークしておけば困ったときにサクッと使えて重宝しそうです!

おまけ

おまけですが、僕の場合はAlfredにショートカットを作ってすぐに調べられるようにしてます。

www.alfredapp.com

参考までにWeb Searchの設定は下のようにしています。

f:id:kenev:20190616085456p:plain

そうすると下のようにすぐに調べることができます!

f:id:kenev:20190616091822g:plain

まとめ

  • explainshell.comを試した
  • シェルコマンド調べるときに便利
  • Alfredなどツールと連携すればさらに強力!

Node.jsではてなの自分の記事一覧を取得する

はてなの自分の記事一覧をサクッと取得できるようにしたかったのですが、意外と

  • Node.jsで書かれている
  • 結果をJSONにする

ものが見当たらなかったので結局自分で作ることにしました!

ツールの流れ

はてなから自分の記事一覧を取得する流れは下のようになります。

  1. はてなのAtomPubを使う(認証はこの記事ではBasic認証を使います)
  2. エンドポイントをGETすると最新の記事が10件とれる
  3. 結果はXMLなのでJSONに変換する
  4. 2のレスポンスに次の10件のURLが記載されているので、全部取得終わるまで2, 3を繰り返す

はてなのAtomPub

はてなのAtomPubの仕様については以下Hatena Developer Centerに記載されています。

developer.hatena.ne.jp

認証のところに書かれていますが、取得時に使える認証は

の3種類です。個人的にしか使わないし、簡単な方法でとりたいので今回はBasic認証を使うことにしました!

Basic認証に必要なユーザー名ははてなIDで、パスワードはAPIキーを使用します。

APIキーははてなブログの管理画面から設定>詳細設定に行った場所にあります!

f:id:kenev:20190614063328p:plain
設定>詳細設定

下の方を見るとAtomPubの項目があり、そこにAPIキーがあるのでメモっておきましょう!

f:id:kenev:20190614063417p:plain
APIキー

ブラウザからGETしてみる

それでは、試しに記事一覧をブラウザから取得してみます。

ドキュメントにも記載がありますが、以下のURLで取得できます。

GET https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry

僕の場合はてなID=kenevブログID=kenfdev.hateblo.jpなので、以下になります。

https://blog.hatena.ne.jp/kenev/kenfdev.hateblo.jp/atom/entry

これをブラウザに入れると、下のようにBasic認証が聞かれるはずです。

f:id:kenev:20190614063856p:plain

ここにユーザー名APIキーを入力すると、下のようなXMLが表示されます。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:app="http://www.w3.org/2007/app">
  <link rel="first" href="https://blog.hatena.ne.jp/{はてなID}}/{ブログID}/atom/entry" />
  <link rel="next" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=1377584217" />
  <title>ブログタイトル</title>
  <link rel="alternate" href="http://{ブログID}/"/>
  <updated>2013-08-27T15:17:06+09:00</updated>
  <author>
    <name>{はてなID}</name>
  </author>
  <generator uri="http://blog.hatena.ne.jp/" version="100000000">Hatena::Blog</generator>
  <id>hatenablog://blog/2000000000000</id>
  
  <entry>
    <id>tag:blog.hatena.ne.jp,2013:blog-{はてなID}-20000000000000-3000000000000000</id>
    <link rel="edit" href="https://blog.hatena.ne.jp/{はてなID}/
    ブログID}/atom/edit/2500000000"/>
    <link rel="alternate" type="text/html" href="http://{ブログID}/entry/2013/09/02/112823"/>
    <author><name>{はてなID}</name></author>
    <title>記事タイトル</title>
    <updated>2013-09-02T11:28:23+09:00</updated>
    <published>2013-09-02T11:28:23+09:00</published>
    <app:edited>2013-09-02T11:28:23+09:00</app:edited>
    <summary type="text"> 記事本文 リスト1 リスト2 内容 </summary>
    <content type="text/x-hatena-syntax">
      ** 記事本文
      - リスト1
      - リスト2
      内容
    </content>
    <hatena:formatted-content type="text/html" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#">
      &lt;div class=&quot;section&quot;&gt;
      &lt;h4&gt;記事本文&lt;/h4&gt;
      &lt;ul&gt;
      &lt;li&gt;リスト1&lt;/li&gt;
      &lt;li&gt;リスト2&lt;/li&gt;
      &lt;/ul&gt;&lt;p&gt;内容&lt;/p&gt;
      &lt;/div&gt;
    </hatena:formatted-content>
    <app:control>
      <app:draft>no</app:draft>
    </app:control>
  </entry>
  <entry>
  ...
  </entry>  
  ...
</feed>

XMLが取得できそうだとわかったところで、Node.jsで実装してみます!

Node.jsで取得する

まずはソースを載せちゃってからポイントを解説します。

const axios = require('axios');
const parseString = require('xml2js').parseString;

const hatenaAPIUser = '<USER_ID>';
const hatenaAPIPassword = '<API_KEY>';
const entryUrl = `https://blog.hatena.ne.jp/<USER_ID>/<BLOG_ID>/atom/entry`;

const creds = `${hatenaAPIUser}:${hatenaAPIPassword}`;
const encoded = Buffer.from(creds).toString('base64');
const authorizationHeader = `Basic ${encoded}`;

/**
 * Promise based timeout
 * @param {number} msec wait milliseconds
 */
function wait(msec) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, msec);
  });
}

/**
 * Promise based XML->JSON Parser
 * @param {object} data XML Data
 */
function parseStringPromise(data) {
  return new Promise((resolve, reject) => {
    parseString(data, (err, result) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(result);
    });
  });
}

/**
 * Get all Hatena entries recursively
 * @param {string} url Entry URL
 */
async function getAllEntries(url) {
  console.log('fetching:', url);
  const entries = await axios.get(url, {
    headers: {
      Authorization: authorizationHeader,
    },
  });

  const {
    feed: { entry, link },
  } = await parseStringPromise(entries.data);

  const next = link.find(l => l.$.rel === 'next');

  const parsedEntries = entry
    .filter(e => {
      const {
        'app:control': [
          {
            'app:draft': [isDraft],
          },
        ],
      } = e;
      return !(isDraft === 'yes');
    })
    .map(e => {
      const categories = e.category ? e.category : [];

      return {
        id: e.id[0],
        title: e.title[0],
        url: e.link[1].$.href,
        published: e.published[0],
        publishedAt: new Date(e.published[0]).getTime(),
        updated: e.updated[0],
        updatedAt: new Date(e.updated[0]).getTime(),
        summary: e.summary[0]._,
        categories: categories.map(c => c.$.term),
      };
    });

  if (next) {
    // recursively fetch if 'next' exists
    const nextHref = next.$.href;
    await wait(500);
    const nextEntries = await getAllEntries(nextHref);
    return [...parsedEntries, ...nextEntries];
  } else {
    return parsedEntries;
  }
}

getAllEntries(entryUrl).then(console.log);

ポイントとしては

  • 外部リクエストにはaxiosを使い、BASIC認証用のHTTPヘッダーを作成している
  • XMLからJSONにはxml2jsというライブラリを使っている
  • 無限ループで負荷かけちゃうの怖いのでリクエスト間に500ミリ秒待つ
  • 最終的なJSONの形は自分で決める

となります。順番に説明します!

外部リクエストにはaxiosを使い、BASIC認証用のHTTPヘッダーを作成している

最近axiosを使う機会が多かったので、なんとなく外部リクエストのライブラリはaxiosにしました。 ライブラリは割とどうでもいいのですが、ポイントはBASIC認証のHTTPヘッダーを作っている部分です。

const creds = `${hatenaAPIUser}:${hatenaAPIPassword}`;
const encoded = Buffer.from(creds).toString('base64');
const authorizationHeader = `Basic ${encoded}`;

...

  const entries = await axios.get(url, {
    headers: {
      Authorization: authorizationHeader,
    },
  });

...

BASIC認証を実現するためにはHTTPヘッダーに Authorization というものを作って、中身はBasic <ユーザー名:パスワードをbase64エンコードした値>となります。

XMLからJSONにはxml2jsというライブラリを使っている

XMLからJSONにするために下のライブラリを使っています。

github.com

axiosで取得したXMLの中身をこのライブラリに通すことで、JSONを取得することができます。

const parseString = require('xml2js').parseString;

/**
 * Promise based XML->JSON Parser
 * @param {object} data XML Data
 */
function parseStringPromise(data) {
  return new Promise((resolve, reject) => {
    parseString(data, (err, result) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(result);
    });
  });
}

...

  const {
    feed: { entry, link },
  } = await parseStringPromise(entries.data);

...

個人的にCallbackがあまり好きじゃないので、Promiseにわざわざ変換したfunction書いてますけど、これは別に必要ないです。Promiseにしてawaitしたかっただけです。

無限ループで負荷かけちゃうの怖いのでリクエスト間に500ミリ秒待つ

コードがバグっていてはてなさんのサーバーに変な攻撃を起こしてしまうのが怖いので、リクエスト間に待ちを発生させるようにしてます。

/**
 * Promise based timeout
 * @param {number} msec wait milliseconds
 */
function wait(msec) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, msec);
  });
}

...

  if (next) {
    // recursively fetch if 'next' exists
    const nextHref = next.$.href;
    await wait(500);
    const nextEntries = await getAllEntries(nextHref);
    return [...parsedEntries, ...nextEntries];
  } else {
    return parsedEntries;
  }

...

先程と同じく、setTimeoutをPromiseにしてawait使いたかったのでわざわざfunction作っています。あとは再帰呼び出しをする直前にawait wait(500)と書いて500ミリ秒待つようにしています。

最終的なJSONの形は自分で決める

XMLからJSONに変換したものは、そのままではものすごく使いづらいので、適宜興味のある項目だけ抜き取って最終的な形に整形します。

...
      const categories = e.category ? e.category : [];

      return {
        id: e.id[0],
        title: e.title[0],
        url: e.link[1].$.href,
        published: e.published[0],
        publishedAt: new Date(e.published[0]).getTime(),
        updated: e.updated[0],
        updatedAt: new Date(e.updated[0]).getTime(),
        summary: e.summary[0]._,
        categories: categories.map(c => c.$.term),
      };
...

こうすると、下のようなJSONになります。

{
  "id": "xxxxxxxxxxxxxxxxxxx",
  "title": "AWS SAA再認定のために5日間頑張ったこと",
  "url": "https://kenfdev.hateblo.jp/entry/saa-recertification",
  "published": "2019-03-08T20:42:58+09:00",
  "publishedAt": 1552045378000,
  "updated": "2019-03-08T20:42:58+09:00",
  "updatedAt": 1552045378000,
  "summary": "昨日AWSソリューションアーキテクト・アソシエイト(以下SAA)に再認定されました。3月中に取得しないといけないというミッションがあったので慌てて先週申し込みました。試験会場の関係で試験日まで5日しか無かったのですが、合格できたので備忘録を残しておきます。 TL;DR(まとめ) …",
  "categories": ["AWS"]
}

まとめ

以上ではてなの記事一覧の取得ができます!最後はfsを使ったりしてファイルにJSONを保存すれば何か他の用途に使うことができるかと思います。

以下リポジトリにコードを格納していますので、興味がある方は使ってみてください!

github.com