積極的後進

後ろ向きに全力ダッシュ

アプリケーションアーキテクチャ設計パターン読書メモ

一連の読書シリーズはこれで最後。アプリケーションアーキテクチャ設計パターンを読んだ。

本書はアプリケーション全体、システム全体の設計について具体例を添えて書かれている。Javaを中心に様々なパターンについて触れられており、全部に触れていくとキリが無い。気になったワードや解釈を中心に触れていく。

気になる

  • アプリケーションの構造と処理方式を決めるのがアプリケーション設計の目的。
  • アプリケーションサーバーの中身の内訳についてはあまり意識したことが無かった。
    • プレゼンテーション層
    • ビジネス層
    • インテグレーション層
  • セッション管理や認証系についても詳細な解説があるのは、サーバー側の実装や知識がない僕にとっては嬉しい
  • バッチ処理やデータアクセス層の解説はもう何度か読み直したい
  • クライアントの実装、SPA等についても触れられている

まとめ

内容が多岐にわたることもあって簡単なワードやらを羅列する形でまとめているが、サーバー側の知識を中心に増やすことができた。これまでWebクライアント一本で進んできたので、繰り返し読み返して身につけたい。もう少し個人でもサーバーサイドのコードを書こうかなー。

CleanArchitecture読書メモ

僕がプログラムを一番最初に書いたのは17歳の時だったが、これまでに自分が書いたソースコードを全て見直しても、おおよそ設計と呼べるものが無かった。

設計思想、設計とは何かを学ぶためにCleanArchitectureを読んだのでメモを書く。

例の図

clean_architecture

いつも見る例のアレ。既にネットにある種々の解説を読んでみてもピンとこなかったが、本を読むとそれなりにしっくりきた気がする。自分なりにまとめてみる。

内容物

  • Entities: 企業全体の最重要ビジネスルールを司る。ビジネスルールとは、例えばローンの計算など、コンピューターで行うかどうかを問わないビジネス上の重要なルールのことを指す。このビジネスルールには、例えば利子のレートや支払いスケジュールなどのデータが紐づく。このデータを最重要ビジネスデータとよぶ。この図で表現されているEntitiesは、この最重要ビジネスデータそのものと、それらにアクセス、操作する関数群で成り立っている。このEntitiesは、システムの他の要素に依存せず独立しているようにしておく必要がある。
  • UseCases: 自動化されたシステムを使用する方法を記述したものであり、アプリケーション固有のビジネスルールを記述したオブジェクトである。例えばユーザーの入力やそれに対するインタラクション、入力後の処理などが相当する。ユースケースでは、エンティティに含まれる最重要ビジネスルールをいつ、どのように呼び出して使用するかを規定したルールが含まれている。ここで、ユースケースにはUIに関する記述は無いことに注意する。インターフェースを問わず、インターフェースが使用する処理が書かれている。ユースケースはアプリケーション固有である一方、エンティティはたとえアプリケーションが違ってもそのまま使用できる。そのため、ユースケースはエンティティに依存し、下位に位置する。
  • Interface Adapters: この層は、エンティティやユースケースと外部を繋ぐ部分となる。図に示されているように、ControllerやGatewayがこの部分に相当する。DBやWebへの接続はこの層から行う。Interface層やUseCases層を中心に、それぞれの層をまたいだデータのやり取りが発生することになる。その場合は、常に内側の層に対して便利な構造にしておくと良い。
  • Frameworks and Drivers: この層が、アプリケーションにとって最も外側の層となる。DBやUIが相当する。肝要なのは、これらは円の内側に依存するが、円の内側であるEntitiesやUseCasesはこれらに依存しないことだ。

依存方向のコントロールコンポーネント粒度

この本では、依存方向のコントロールと適切なコンポーネント粒度の重要性について語り口を変えて幾度も説かれている。アプリケーションやソースコードに依存が発生するのは避けられないが、依存の方向はコントロールすることが出来る。それが出来れば、依存先に重大な変更が加わった場合でも、依存している側への影響は最小限で済む。そのために、例えば必ずインターフェースを定義、用意しておくことでコントロールが容易になるなどの例が繰り返し紹介されている。

また、依存方向をコントロールするためには、適切なコンポーネント粒度である必要がある。コンポーネントが密結合すぎると、アーキテクチャの変更等アプリケーションにとって大きな変更が加わった場合に対応できなくなる。コンポーネントの分割粒度や可変/普遍など詳しくは書籍の中で触れられているが、それぞれのコンポーネントがどういう位置づけで、他のコンポーネントとどのように関わるのかは常に意識したい。

個人的には、こういったことを意識することでテスタブルなコードを書けることも嬉しい。

まとめ

本書を読むまで、上記の図すら理解出来なかったがまずはそこをクリア出来てよかった。クリアするためにはその前提である依存方向のコントロールコンポーネント粒度を理解しておかねばならず、その理解が無かった僕にとっては多くの収穫があった。本の中でも触れられているが、あくまで円の図は一例であるので、実際のアーキテクチャを考えるにあたっては完成はなく常に見直し続けることが大事らしい。そういった考えに至れたり、少しずつ自分の書くコードがまともになっている気がするのはほんの少し進歩したと思う。思いたい。

超速本読書メモ

僕がWebフロントエンドの世界に飛び込んでから数年経った。しかし、恥ずかしながらこれまでアプリケーションのパフォーマンスについてあまり深く考えたことがなかった。僕がWebに飛び込む前から、そして飛び込んだ後もWebページの高速化は日に日にその重要性を増してきており、最低限の知識や考え方を持っていないと成果を出すどころか同僚との議論にすら全くついていけなくなる有様だ。

パフォーマンスの世界への入り口として 超速本 を読了したので自分なりにまとめてみることにした。

基本戦略

とにかく計測から入る

推測するな、計測せよ は様々な領域で言われていることだが、改めてこの言葉を噛みしめることになった。

考え方としては、

  • ボトルネックを探して対応することが成果への近道
  • 手段から入るのは間違い

の2つが特に重要そう。

高速化のためのポイント

高速化と言っても、ボトルネックの正体やそれに合わせた対応を考えると様々である。

しかし、主に軽量化(取り扱うデータ量を削減する)や最適化(実行の順番やデータ経路などのチューニング)を意識することになる。

具体的には、下記のようなことを考えることになる。

  • ネットワーク処理
    • データの転送量をなるべく小さくすること
    • データの転送回数をなるべく少なくすること
    • データの転送距離をなるべく短くすること
  • レンダリング処理
    • UIの応答速度
      • 100ms以内を目指す
    • FPS
      • テレビ: 30fps, アニメ・映画: 24fps
      • Webでは60fpsを目標とする
  • スクリプト処理
    • 重いスクリプト
      • Bottom-Up タブ
      • setTimeoutを使って擬似的に非同期化できる
    • メモリリークの調査
      • GCの発生を抑制したい
    • 未解放のイベントリスナーとタイマーの調査と改善
      • performanceパネルで確認できる
      • 特にSPAでは気にしたい

計測手段

計測に用いるツールについて

ここまでで、Webのパフォーマンスアップに対する考え方や着目点を知ることが出来たが、重要なのは計測することである。計測手段はいくつかあるので、それぞれ見ていく。

Chrome DevTool

おそらく最も多くのWebフロントエンジニアに馴染み深いツール。DevToolの各タブで出来ることを見ていく。

Networkタブ

個人的には、DevToolの中でも使用頻度が高いタブ。ページ内の関連リソースの通信状況を確認できる。

Chrome/Devtool/Network tab

Yahoo!JAPAN でnetworkタブを開いてみるとこんな感じ。

ページ上部

ページ上部にはページ読み込み完了までのリソース読み込み状態が表示されている。

ここで、青と赤の赤線に注目したい。

  • 青い縦線がDOMContentLoadedが完了したタイミング
    • HTMLのパースが完了してDOMが構築されるタイミング
      • HTMLが巨大であったり、扱うDOMノードが大量。DOM構築をブロックするスクリプトが存在する。
      • コンテンツ量を減らしたりしてHTMLを軽量化する。同期的に読み込まれるJSファイルや、ベタがきされているスクリプトを排除する。
  • 赤い縦線がLoadが完了したタイミング
    • 関連するリソースの取得と解析が終了するタイミング
      • JSや画像などサブリソースが大量に存在する(またはレスポンスが遅い)。実行されるスクリプト処理やレンダリング処理が遅い。
      • 読み込むリソースの量・サイズ・経路などを見直す。強引な実装や非効率な処理を見直す。

上記のリソース読み込み状態の下には、Waterfallが表示されている。ここでは、リソースがダウンロードされる順番や、各リソースをダウンロードするのにかかった時間がわかる。それぞれのリソースの詳細も見られる。

Perfomanceタブ

Webサイトのパフォーマンスを計測できる。これまであまり使ったことがなかった。

Chrome/Devtool/Performance tab

AbemaTVのもの

  • FPS

    この値が小さいほど、より速く画面が更新され動くことを示す。

  • CPU

    このグラフが示すのはCPUの負荷の大きさで、色によってそれぞれ意味が異なる。

    • 青/Loading: HTTPリクエスト、パース
    • 黄/Scripting: JavaScriptでの処理
    • 紫/Rendering: スタイル評価、レイアウト算出
    • 緑/Painting: ペイント処理、画像のラスタライズ
    • 灰色/Other: その他
  • NET

    ネットワーク通信状態を示す。優先度の髙い/低いリソースへのアクセスを表す。

PageSpeed Insights

Googleが提供しているツール。URLを入力することで、そのWebページのPC/SPでの表示速度を計測できる。

PageSpeed Insights

久しぶりに自分のWebページに対してチェックをかけてみたら一見良さそうな値が出てきた。一見早く見えるが、単純にコンテンツが無いだけ。

複雑なWebページであればあるほどスコアは落ちてくるが、改善項目をサジェストしてくれるのでその項目を精査、改善するだけでも結構違う。

WebPagetest

合成モニタリングを行うサービス。実際に特定のリージョンからアクセスを実行し、ChromeDeveloperToolsのNetworkタブに相当するデータを取得できる。

こちらに詳しい。

Webpagetest

自分のWebページに対して実行してみた結果。有料の合成モニタリングサービスとしては、 SpeedCurve が有名。

表示速度の指標

上記のツールを用いてパフォーマンスの改善を図る場合に、どのタイミング/段階の数値を改善するのかを追いかける方がより良い改善ができる。表示速度においては、ナビゲーションからのパフォーマンスを示す複数の指標がある。

具体的な指標の数々

  • First Paint
    • ナビゲーション後に、ページ内の何かが表示され始めたタイミングのこと
    • クリティカルレンダリングパスの改善が有効
  • First Contentful Paint
    • ナビゲーション後に、ページ内のコンテンツが表示され始めたタイミングのこと
    • これら2つの指標は、サブリソースのロード状況やクリティカルレンダリングパスの状態などを示す
      • パフォーマンスの指標となりうる
      • ユーザー体験に直接影響する数値ではない
      • Paint Timingで取得可能
  • First Meaningful Paint
    • ユーザーにとって意味のある表示になったタイミング
    • あいまいな指標
      • 標準化が難しい
      • User Timingを利用して計測もしくは計測ツールでスクリーンキャプチャを撮って画像比較で判定
  • Time to Interactive
    • ロードが完了し、ユーザー操作に確実に応答できるようになったとき
    • RAILモデルのIdleを満たす状態
    • SPAのようにJSの初期化やAPIとのやり取りに時間がかかるアプリケーションなどで有効な指標
    • こちらも、標準化には至っていない
  • Speed Index
    • ATF(Above the fold)
      • スクロールせずに閲覧可能な画面領域
    • ATFでの表示性能を数値化
      • ファーストビューにおける描画量のスコア

こういった指標が挙げられ、それぞれに効果的な改善を模索、実行していくことになる。ここで意識しておきたいのは、 最も重要視するべき指標はプロダクトによって変わってくる ことである。プロダクトでの各指標がどうなっているかを継続的に計測すること、改善するために何が出来るかをチームで議論して実践することが何より大事っぽい。

その他、取り組みたいキーワード

  • Resource Hints
  • 通信内容の軽量化
    • brotli
      • googleが開発した圧縮アルゴリズム
      • gzipに比べて高圧縮率を誇り、かつ圧縮処理時間も短い
      • IE以外のモダンブラウザでは対応している
  • キャッシュ戦略
    • ServiceWorkerを用いたキャッシュ戦略
    • 優先度の髙いリソースをキャッシュしたりする
  • 画像の圧縮
  • フォントのサブセット化
    • Unicodeのコードポイントで分割
    • ブラウザが必要なフォントファイルのみロードすることを補助する

まとめ

いろいろ考えることがあって混乱してしまうのが正直なところだが、まずは計測する仕組みする仕組みを整えること、それぞれの指標/フェーズにおいて何が出来るのかを一つずつ明らかにしていくのが良さそう。

計測による可視化がなければ闇雲かつ場当たり的な対策しか出来ず、パフォーマンスを改善したとは言えない。特にWebは多種多様な環境でのアクセスが当然のように起こる世界なので、チーム/プロダクトで協力して様々な環境を用意し、定期的なモニタリングと評価、改善が肝要なのだと知ることが出来た。

参考

セッション認証付きAPIサーバーをnode.js(express)で作る

f:id:heimusu:20180518115114j:plain プライベートプロジェクトのサーバー実装を一新するべく,セッション認証とパスワードハッシュ化を付けたのでメモ.

本当はユーザー情報をDBに持たないと意味が無いけど,ユーザーは僕しか居ないからまあいいやと妥協した.

実際のコード

const express = require('express')
const oauth = require('oauth')
const http = require('http')
const path = require('path')
const crypto = require('crypto') // 暗号化
const bodyParser = require('body-parser')
const session = require('express-session')

const app = express();
app.use(express.static(__dirname));
app.set('port', process.env.PORT || 3000);
[f:id:heimusu:20180518115114j:plain]

// Cross Originを有効化
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
  next();
});

// Optionsも
app.options('*', (req, res) => {
  res.sendStatus(200);
});


// urlencodeとjsonの初期化
app.use(bodyParser.urlencoded({
  extended: true
}));
app.use(bodyParser.json());


// セッションの生存時間(分で指定)
const maxage = 1;

// セッション管理設定
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: maxage * 60000 }}))



// セッション管理関数
const sessionCheck = (req, res, next) => {
  if (req.session.email) {
    next();
  } else {
    res.status(440).json({message: 'セッション切れです'})
  }
}


const email = 'email@email.com'
const password = 'hogehoge'

// sha-512で暗号化
const hashed = password => {
  let hash = crypto.createHmac('sha512', password)
  hash.update(password)
  const value = hash.digest('hex')
  return value;
}

// ログイン処理
app.post('/login', (req, res, next) => {
  const reqEmail = req.body.email
  const reqPass = req.body.password
  try {
    if(email === reqEmail && hashed(password) === hashed(reqPass)) {
      req.session.email = {name: req.body.email};
      res.send(200)
    }
    else {
      res.status(401).json({message: 'メールアドレス/パスワードが一致しません'})
    }
  }
  catch (error){
    res.status(500).json({message: 'error'})
  }
});

app.get('/', (req, res, next) => {
  sessionCheck(req, res, next)
  res.sendStatus(200)
})

app.listen(3000, function(){
  console.log('working');
});

リポジトリ

github.com

React.jsのsetStateが遅い問題

f:id:heimusu:20180510105852j:plain

散々言われ続けてきたことだが,自分の備忘録として残しておく.

this.stateを参照してDOMを書き換える

失敗する例

例えば下記のような場合

  constructor(props) {
    super(props);
    this.state = {
      isOpen: false
    }
  }

  componentDidMount() {
    this.setState({
      isOpen: true
    })
  }

  click(flg) {
    alert(flg)
  }


  render() {
    return({
      <div onClick={()=>this.click(this.state.isOpen)}>
    })
  }

DOMをクリックした場合に,1回目のクリックではfalse,2回目以降でtrueが表示される. 本当は1回目のクリックでtrueが表示されてほしいが…

成功する例

正しい対処法なのかどうかはさておいて,上記のコードを次のように変更すると良い.

  constructor(props) {
    super(props);
    this.state = {
      isOpen: false
    }
  }

  componentDidMount() {
    this.setState({
      isOpen: true
    })
  }

  click(flg) {
    alert(flg)
  }


  render() {
    const isOpen = this.state.isOpen

    return({
      <div onClick={()=>this.click(isOpen)}>
    })
  }

render()内でthis.state.isOpenを変数に外出しにして,その変数をDOMに渡す. こうすることで一回目のクリックでもtrueが表示されるはずだ.

私見

業務でReact.jsを扱う中でハマったので試行錯誤した結果この形になったが,原因や良い対処法についてはっきりしていない…

stateの変更タイミングが意図的に制御されているのだと思うが, そもそもreduxを使っていれば心配いらないのかもしれない.

原因やよりよい方法についてご存知であれば是非コメントにて.

Rx.jsでTinder風UIを実装する

f:id:heimusu:20180502125757j:plain

業務でTinder風UIを作るオーダーが来たので,勉強も兼ねてRx.jsで作ることにした.

コードと処理の流れ

下記のコードはRx.js ver5.8.8で動作を確認しています. v6.0.0では動作しないので適宜読み替えてください.

また,モバイルでの動作を想定して記述しています.

const tolerance = 200;
const windowWidth = window.innerWidth
const imageElem = document.querySelector('.characterImage') // dom

// 現在のスワイプ位置を取得
const touchstart$ = Rx.Observable.fromEvent(imageElem, 'touchstart')
  .switchMap(startEvent => 
    Rx.Observable.fromEvent(imageElem, "touchend")
    .map(e => e.changedTouches[0].pageX)
  )

touchstart$
  .do(val => {
    // スワイプ位置を判断して画像を振り分ける
    if(val > windowWidth - tolerance) {
      imageElem.classList.add('rotate-right')
    }
    else if(val < windowWidth / 2.0 - tolerance) {
      imageElem.classList.add('rotate-left')
    }
  })
  .delay(1000)
  .subscribe(val => {
    imageElem.classList.remove('rotate-right')
    imageElem.classList.remove('rotate-left')
  })

対象のDOMをfromEventで監視して,touchend時の座標を基に判定する. torelanceをいじることで動作をもう少し過敏にしたりできる.

subscribeではサンプル用に画像の復帰処理をしているが,画像の追加変更も任意で行える.

所感

まだRx.jsの勉強中なので掴めていないところが多々あるが,一旦は目標のものが出来上がった. Rx.jsは出来ることがたくさんあって使い所が分かっていなかったり,そもそも書き方がまだおぼつかなかったり… 使いながら覚えていくしかなさそう.

上記コードの指摘等あればプルリクを投げてもらえると嬉しいです.

サンプルプロジェクト

https://github.com/heimusu/rxtinder

参考

GulpでCloudFront + S3環境へのフロントエンドデプロイを自動化する

CloudFrontに紐付いたS3に静的ファイル一式を置いた構成はテッパンだと思う. この場合,静的ファイルを更新するためには 1. S3にファイルをアップロードする 2. CFをInvalidateする の手順が必要となる. 滅多に更新が発生しないならともかく,絶賛開発中のプロジェクトの場合にこの操作を何度も手動で繰り返したくない.

本来ならばCIを設定したりwebpackでbuildついでに実行するなどありそうだが,オーソドックスにgulpタスクを書いてみた.

パッケージインストール

$ yarn add gulp gulp-awspublish gulp-cloudfront-invalidate

gulpタスクの記述

const gulp = require('gulp');
const awspublish = require('gulp-awspublish');
const cloudfront = require('gulp-cloudfront-invalidate');

// S3にデプロイするタスク
// dev環境にデプロイする
gulp.task('deploy', function() {
  const key = {
    accessKeyId: '...',
    secretAccessKey: '...',
    region: "ap-northeast-1",
    params: {
      "Bucket": "bucket-name",
    }
  }
  const publisher = awspublish.create(key);

  // deployするもの
  gulp.src(['**/*', '!node_modules/**/*', '!gulpfile.js',  '!package.json', '!README.md', '!yarn.lock', '!.babelrc' ])
    .pipe(publisher.publish())
    .pipe(awspublish.reporter());
});

// CFをInvalidateする
const cfInvalidateSettings = {
  distribution: '...',
  paths: ['/*'], 
  accessKeyId: '...', 
  secretAccessKey: '...',
  wait: true   
}


gulp.task('invalidate', function() {
  return gulp.src('*')
    .pipe(cloudfront(cfInvalidateSettings));
})

実行

$ gulp deploy && gulp invalidate

これで,指定したS3に静的ファイルを設置(存在しないファイルは新規に設置,存在するファイルは差分があれば更新)した後に指定したCFをinvalidateしてくれる. あとは個々の案件に併せてタスクをカスタマイズして,package.jsonのscriptsに良い感じで書いておく. 上記の設定だとinvalidateの完了までwaitする設定になっているが,外すことも出来る.個人的にはしっかりinvalidateが完了したかを見届けたいのでwaitすることにしている.