jsでリファクタリングを学びたい(Nullオブジェクト編)

リファクタリングについて学びたく、「Java言語で学ぶリファクタリング」を読んでる。タイトル通り、Javaで記述されているが、js(ES6) + flowで書いてみて、リファクタリングの技術とflowの理解を深めようと思う.

今回取り上げるリファクタリング内容は、4章のNullオブジェクトについてである。

書籍にはNullオブジェクトの良いたとえ話が記載されている。一日おきに飲まなければならない薬があるとき、本日飲むべき日かどうか考えないといけない。その判断を避けるために、本来の薬とダミーの薬を交互に飲み続ければよい。これをプログラミングでも行おうというものが、Nullオブジェクトの目的である。

では、実際null判定の入った次のソースをリファクタリングしていこう。

リファクタリング

// Person.js
// @flow
import Label from './Label';

export default class Person {
  _name: Label;
  _mail: Label;

  constructor(name: Label, mail: Label) {
    this._name = name;
    this._mail = mail;
  }

  display() {
    // nullチェック
    if (this._name !== null) {
      this._name.display();
    }

    // nullチェック
    if (this._mail !== null) {
      this._mail.display();
    }
  }

  toString(): string {
    let result: string = '[ Person:';
    result += ' name=';

    // nullチェック
    if (this._name === null) {
      result += '(none)';
    } else {
      result += this._name;
    }
    result += " mail=";
    if (this._mail === null) {
      result += '(none)';
    } else {
      result += this._mail;
    }
    result += ' ]';
    return result;
  }
}

nullかどうかでdisplay処理を行うかどうかを分けてるが、Nullオブジェクトを導入すると、nullかどうかを意識せずメソッドを呼び出せばよいのだ。

リファクタリング

// Person.js 
// @flow
import Label from './Label';

export default class Person {
  _name: Label;
  _mail: Label;

  // Nullオブジェクトの導入
  constructor(name: Label, mail: Label = Label.newNull()) {
    this._name = name;
    this._mail = mail;
  }

  display() {
    this._name.display();
    this._mail.display();
  }

  toString(): string {
    return `[Person: name=${this._name.toString()} mail=${this._mail.toString()}]`;
  }
}

コンストラクタの第二引数に、mailアドレスが与えれなかった時、Label.newNull()によりNullオブジェクトが代わりに与えられる。
このNullオブジェクトは、Label型のサブクラスとして実装されるため、型一致している。 そして、NullオブジェクトはLabelオブジェクト同様displayメソッドをもつが、何も影響与えないよう設計されている。

以上の要件からNullオブジェクトに要求されるものが、
- 本来のクラスを継承する
- 本来のクラスのもつメソッドを無効化する
という2点にある。

実装は次の通り。

Nullオブジェクト

// Label.js
// @flow
export default class Label {
  _label: string;

  constructor(label: string) {
    this._label = label;
  }

  // 本来行いたい処理
  display() {
    console.log(`display: ${this._label}`);
  }

  toString(): string {
    return this._label;
  }

  // Nullオブジェクトを返すfactoryメソッド --- (a)
  static newNull(): Label {
    return NullLabel.getInstance();
  }
}

// Nullオブジェクトクラス
class NullLabel extends Label { // --- (1)
  // シングルトンの実装 --- (b)
  static singleton = new NullLabel();
  static getInstance(): NullLabel {
    return NullLabel.singleton;
  }

  constructor() {
    super('(none)');
  }

  // オーバーライドして何も影響を与えないようにする --- (2)
  // @override
  display() {

  }
}

本来のLabelクラスを継承するNullLabelサブクラスがある(1)
またサブクラス内でメソッドを無効化するためにオーバーライドを利用している(2)
そして、メモリ節約のためのシングルトン(b)や、new演算子をさけるためのFactoryメソッド(a)を利用して、LabelクラスからNullオブジェクトを呼び出している。

まとめ

今回は、リファクタリングとしてNullオブジェクトについて学んだことを残してみた。Nullオブジェクトを一言でいうと、本物と同じ動きをするが影響を及ぼさないダミーオブジェクトのことを指すのであろう。
それを、継承とオーバライドによる無効化によって実現する技術なんだなと理解できた。

気になったこと

javaオーバーロードが実装できるので、nullオブジェクトの検討がわかりやすいが、jsではオーバーロードが実装できない。
代わりに、デフォルト引数を利用すれば補えるのではないかと気づいた!

public class Person {
  private Label _name;
  private Label _mail;

  public Person(Label name, Label mail) {
    _name = name;
    _mail = mail;
  }

  public Person(Label name) {
    this(name, Label.newNull());
  }

  ...
}
export default class Person {
  _name: Label;
  _mail: Label;

  constructor(name: Label, mail: Label = Label.newNull()) {
    this._name = name;
    this._mail = mail;
  }

}

Java言語で学ぶリファクタリング入門

Java言語で学ぶリファクタリング入門

electronでキャプチャーソフトつくってみるpart2

キャプチャーソフトを作成中に、録画経過時間を表示する機能が必要になった。フラグのON/OFFでタイマーの開始・リセット出来るならば、他のアプリにも活かせるかもと思い、タイマーコンポーネントを作ってみた。

要件

  1. タイマーつくる
  2. propsであるisRecordを切り返ると、タイマーが開始・リセットされる

propsTypeの代わりに、flowで型チェックを行っている

// @flow
import * as React from 'react';

type PropType = {
  isRecord: boolean
};

type StateType = {
  time: number,
  timerId: number | null
};

export default class Timer extends React.Component<PropType, StateType> {
  constructor(props: PropType) {
    super(props);
    this.state = {
      time: 0,
      timerId: null,
    };
  }

  shouldComponentUpdate(nextProps: PropType): boolean {
    if (nextProps.isRecord !== this.props.isRecord) {
      (nextProps.isRecord) ? this.clearTimer() : this.setTimer();
    }
    return true;
  }

  setTimer() {
    const timerId = setInterval(() => {
      const { time } = this.state;
      this.setState({ time: time + 1 });
    }, 1000);
    this.setState({ timerId });
  }

  clearTimer() {
    const { timerId } = this.state;
    timerId && clearInterval(timerId);
    this.setState({ time: 0, timerId: null });
  }

  formatTime(time: number): string {
    const zeroPad = (num: number): string | number => {
      if (num < 10) { return `0${num}`; }
      return num;
    };

    const hour = zeroPad(Math.floor(time / 60 / 60));
    const minuite = zeroPad(Math.floor(time / 60));
    const second = zeroPad(time % 60);

    return `${hour}:${minuite}:${second}`;
  }

  render(): React.Node {
    const { time } = this.state;
    return (
        <span>
          {this.formatTime(time)}
        </span>
    );
  }
}

timerIdについて、eslint/flowでエラーが吐かれてしまう。 なんだこれ。あとで調べよう。。。

electronでキャプチャーソフトつくってみるpart1

electronに最近はまってます(笑)。キャプチャーソフトも作れるということなので、
reactとmaterial-uiを練習かねて、つくりました。

f:id:poppon555:20180415005255p:plain


electronのキャプチャー機能と動画保存機能を実装してみたかっただけなので、
ものすごく単純です。この調子で仕上げていきたい~

参考サイト

Electronでデスクトップを録画するアプリが簡単に作れました - なになれ
デスクトップを録画するアプリを書いた - Qiita

electronでReactを使うためのwebpackの設定

はじめに

electron。javascriptでデスクトップアプリケーションを作れるフレームワーク
使ってみたかったけど、なかなかよい作りたいものも思いつかずsample demo appで遊ぶくらいでした。 最近ちょっと良いアイデアも思いついたので、思い切ってelectronを触ってみました。 また合わせてreactの勉強もしてみたかったので、React x electronの環境構築の備忘録を残しておこうと思います。 reduxは使いません。まだ扱えきれるレベルじゃないないので。。。

ポイント

  • node_modulesを呼び出せるelectronのjsをどうやってバンドルするの?
  • main・rendererプロセス用の2種類のjsが必要

electronのjsはfsなどnodeで使えるモジュールを呼び出せるので、
webのReactと同様にimportできるのか。。。

またelectronには、mainプロセスとrendererプロセスが存在し、それぞれにjsファイルがあるのに対して、多分普通のReactのプロジェクトでは、htmlで読み込むのは、一つのjsだけ。
この違いをどうやって、対応するか。。。

結論急げば、webpackの設定でどちらの問題も解決できます。まじ優秀!!

流れ

  1. electronのインストール
  2. electron起動
  3. reactの環境構築
  4. build失敗の確認
  5. webpackの設定(nodeモジュールをimportさせる)
  6. webpackの設定(2つのjsファイルを作成)
  7. buildしてみる

1. electronのインストール

これだけ。はは、便利

npm install -g electron 

2. electron起動

プロジェクトを作って、mainプロセス用のjsと、rendererプロセスで必要なhtmlとそれが呼び出すrenderer.jsを作成します。 js,htmlが用意できたら、electronコマンドでmain.jsを指定します

mkdir sampleApp
cd sampleApp
npm init -y
touch main.js     
touch index.html   
electron main.js

作成したhtml,jsは、tutorialのWriting Your First Electron Appからコピペしてます。

f:id:poppon555:20180402195318p:plain

こんな感じ。

3. reactの環境構築

他の色々なサイトに導入方法は書いてあるが、自分は【Reactではじめるフロントエンド開発入門】1 を参考にさせてもらいました。npm, yarn両方の構築方法が丁寧に書かれてあるので、勉強になりました。

// react
npm install react react-dom

// webpack
npm install -D webpack webpack-cli

// babel
npm i babel-core -D
npm i babel-preset-es2015 -D
npm i babel-preset-react -D
npm i babel-loader -D

webpackの設定も最低限

// webpack.config.js
const webpack = require("webpack");
const path = require('path');

const config = {
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'app.js'
  },
  module: {
    rules: [{
      test: /\.js$/,
      exclude: path.resolve(__dirname, 'node_modules'),
      loader: 'babel-loader',
      query:{
        presets: ['react', 'es2015'],
      }
    }]
  }
};

module.exports = config;

npm buildでapp.jsができると、後はindex.htmlでロードすればおしまい。

f:id:poppon555:20180402195006p:plain

4. build失敗の確認

では、webのプロジェクト同じやり方でjsファイルを作成しトランスパイルさせてみます

nodeモジュールfsを利用した以下のファイルを作成する。

import React, { Component } from 'react';
import ReactDom from 'react-dom';
// 追加: nodeのモジュールfsをimportする
import fs from 'fs'

export default class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      file : ''
    }
  }

  componentWillMount() {
    // fsモジュールを使用する
    this.setState({file:fs.readFileSync('index.html','utf8')})
  }

  render() {
    return <h1>{this.state.file}</h1>
  }
}

ReactDom.render(
  <App />,
  document.getElementById('root')
);

npm buildするとこちら。

f:id:poppon555:20180402195010p:plain

はい、失敗します! fsが読み込めませんと。 なので、これの対応がwebのときと異なり必要となってきます。

5. webpackの設定(nodeモジュールをimportさせる)

google先生に聞いてみると、ここに electron + webpack + react + sass ほとんど書いてました。targetプロパティなるものが、重要ということ。 webpack公式targetによると、targetには,electron-mainとelectron-rendererを 指定できるようなので、これを使いましょう。

では、targetプロパティを追加して、electron用であることを指定します。

// webpack.config.js
const webpack = require("webpack");
const path = require('path');

const config = {
  entry: './src/app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'app.js'
  },
  target:'electron-renderer',   // 追加 renderer用
  module: {
    rules: [{
      test: /\.js$/,
      exclude: path.resolve(__dirname, 'node_modules'),
      loader: 'babel-loader',
      query:{
        presets: ['react', 'es2015'],
      }
    }]
  }  
};

module.exports = config;

6. buildしてみる

f:id:poppon555:20180402195016p:plain

build成功

f:id:poppon555:20180402195021p:plain

electron起動

7. webpackの設定(2つのjsファイルを作成)

もう一つ課題が残っていました。
mainプロセスとrendererプロセス用の二つのjsをトランスパイルさせましょう。 renderer用のみトランスパイルするなら特に不要な作業だが、main.jsだけはcommonjs記法を採用するという
気持ちの悪い感じがするので、自分はmain.jsもトランスパイルさせました。

設定方法は実に簡単。
main用とrenderer用の設定を記述して、配列にしたものをconfigに格納するだけになります。

// webpack.config.js
const config = [
  {
    // mainプロセス用
    entry: './src/main/main.js',
    output: {
      path: path.resolve(__dirname, 'dist/main'),
      filename: 'main.js'
    },
    target: 'electron-main',
    module: {
      rules:[
      {
        test: /\.js$/,
        exclude: path.resolve(__dirname, 'node_modules'),
        loader: 'babel-loader',
        query: {
          presets: ['react', 'es2015']
        }
      }
      ]
    }
  },
  {
    // renderer用
    entry: './src/renderer/renderer.js',
    output: {
      path: path.resolve(__dirname, 'dist/renderer'),
      filename: 'renderer.js'
    },
    target: 'electron-renderer',
    module: {
      rules:[
      {
        test: /\.js$/,
        exclude: path.resolve(__dirname, 'node_modules'),
        loader: 'babel-loader',
        query: {
          presets: ['react', 'es2015'],
          plugins:['transform-class-properties']
        },
      }
      ]
    }
  }
]

その結果、main用とrenderer用の二つのjsファイルを作成されます。

dist
├─main
│      main.js
│
└─renderer
        index.html
        renderer.js

最後に

webpackの設定は奥が深い...。
フロントエンドの勉強は学習範囲が多すぎて、どこか不安になってしまう。
これが仕事となると、本当に効率よい学習が必要なんだろうな~と思う。 けど、jqueryも使う気がしないのは確かだ。

power-assertとmochaを使ったnodejsのテスト

昨年decodeではじめて和田卓人さんの講演を聞いて、テスト駆動開発というものを知りとても興味が湧いた。テスト環境づくりは多くのサイトで紹介されているのだけど、自分の備忘録として環境構築手順を残しておこうと思う。もちろんアサーションツールには、power-assertを使用する。

大きな流れ

  1. npmで必要なモジュールのインストール
  2. npm scriptsでテスト実行コマンド登録
  3. テストコードを書く
  4. テスト実行

1 npmで必要なモジュールのインストール

// mochaのインストール
npm install --save-dev mocha

// power-assertのインストール
npm install --save-dev power-assert intelli-espower-loader

2 npm scriptsでテスト実行コマンド登録

testフォルダを作成して、このフォルダをテスト対象として実行するようにnpm scriptsに記述する。

// package.json
  ...
  "scripts": {
    "test": "mocha --require intelli-espower-loader ./test/"
  },
  ...

3 テストコードを書く

テスト対象のコードはただの計算クラスで、2つの引数を足したり引いたりするだけ。

class Calc {

  static add(x, y) {
    return x + y;
  }

  static sub(x, y) {
    return x - y;
  }

}
exports.Calc = Calc;

テストコードでは、power-assertと上記テスト対象のコードをロードして、あとはテストコードの記法通り。本筋とはずれるけど、オブジェクトの分割代入便利ですね。

const assert = require('power-assert');
const {Calc} = require('../module/Calc');  // 分割代入

describe('Calcクラス', () => {

  it('addメソッド', () => {
    assert( Calc.add(1,2) === 3 );
  });


  it('subメソッド', () => {
    assert( Calc.sub(1,2) === -1 );
  });

});

4 テスト実行

あとは、npm testでテスト実行できる。
実行結果はこんな感じ

 Calcクラス
    ✓ addメソッド
    ✓ subメソッド

  2 passing (9ms)

疑問

  • DBアクセスするコードのテストはどうすればよいのだろうか?
  • テストごとにDBを初期化しテスト終了後も初期化する作業が必要なのか?

expressのルーティング共通処理

nodejsでHTTPサーバーを立てるときに利用するexpress。
色々なルーティングを記述していくと、いずれのルーティングでも共通処理を噛ませたいという気持ちになる。

共通処理というのは、セッションが切れていたときにログイン画面に戻す処理のことで、今までは下記のように個々のルーティングごとに記述するアホなことをやっていた。

app.post('/select',  ( req, res ) => {
  
  // ①共通処理:セッション情報の存在判定
  if ( !req.session.pass ) {
     res.json(440, {result:'expired', message:'セッションが切れました。ログインからやり直してください。'});
     return;
   }

  // ②ルーティング固有の処理
  res.header("Content-Type", "application/json; charset=utf-8");
  res.json( something );

}

Nodeクックブックを読んでると、next()を使えばよいことがわかった。

// ①共通処理:セッション情報の存在判定
const checkSession = ( req, res, next) => {
  if ( !req.session.pass ) {
     res.json(440, {result:'expired', message:'セッションが切れました。ログインからやり直してください。'});
     return;
   }
  next()
} ;

app.post('/select', checkSession,  ( req, res ) => {
  
  // ②ルーティング固有の処理
  res.header("Content-Type", "application/json; charset=utf-8");
  res.json( something );

}

expressのpost, getなどのHTTPメソッドには、第二引数以降に複数のcallbackを指定でき、next()を使うことで順番に処理を行ってくれる。
今回では、checkSession(共通処理)がまず実行されsession状態が切れていなければ、next()により次のcallback(固有の処理)を行ってくれる。

Nodeクックブック

Nodeクックブック

Promiseでループ処理

javascriptで通信処理を記述するときに使用するPromiseオブジェクト。
だいぶ慣れてきたが、ループ処理についてはよくわからず立ち止まってしまったので解決を考えてみたいと思う。

満たしたい要件

  1. ループ内の処理はPromiseを返却する
  2. ループ回数を指定できる
  3. ループ内処理に、異なるパラメータを渡せる(1)
  4. ループ内でエラーが起きた場合、catchできる

単純にループを

まず1,2を満たすコードを書いてみる。

// ループ内処理
// @param {Number} i - ループカウンター
function action(i) {
  return new Promise( (res, rej) => {
    setTimeout( () => {
      console.log(i + '回目です')
      res();
    },500)
  })
}

// ループ関数
// @param {Function} fn - 個々の処理
// @param {Number} i - カウント初期値
// @param {Number} end - カウント終了値
function loop( fn, i, end) {

  return fn(i)
  .then( () => {
    if ( i < end ) {
      return loop( fn, i+1, end)
    }
    else {
      return Promise.resolve('end');
    }
  })

}

// 実行
loop(action, 0, 3 );

// console
< 1回目です
< 2回目です
< 3回目です
< 4回目です
< end

ループ内処理では、setTimeoutを使用して擬似的に遅延させてPromiseを返却している。
Promiseでのループ関数のポイントは再帰処理だと思ってる。ループ内処理を呼び出して、thenでPromiseをチェーンさせる2。then内で引数の値からloopを終了させるかどうか判定し、続けるならばloopを再帰的に呼び出し、終了ならばPromiseを返すようにした。

エラー処理の追加

続いて、ループない処理でエラーが生じた場合それ以降の処理を中断するように改良する。

function action(i) {
  return new Promise( (res, rej) => {
    setTimeout( () => {
      // 修正
      if ( i === 5 ) {
        rej('5は処理できません');
      }
      else {
        console.log(i + '回目です')
        res();
      }
    },500)
  })
}

function loop( fn, i, end) {

  return fn(i)
  .then( () => {
    if ( i < end ) {
      return loop( fn, i+1, end)
    }
    else {
      return Promise.resolve('end');
    }
  })
  // 追加
  .catch( (err) => {
    console.log(err);
  })

}

// 実行
loop( action, 0, 6);
// console
< 0回目です
< 1回目です
< 2回目です
< 3回目です
< 4回目です
< 5回は処理できません

ループ内処理では、5だとrejectするように擬似的に設定。 ループ関数では、最後にcatchを追加するだけでよい。

任意のパラメータ指定

最後に3.の指定した任意のパラメータを実行させるように改良してみる

// ループ内処理
// 引数を追加
function action(i, second) {
  return new Promise( (res, rej) => {
    setTimeout( () => {
      if ( i === 5 ) {
        rej('5回は処理できません');
      }
      else {
      console.log(i + '回目です');
      // 追加
      console.log('パラメータ合計:' +  Number(i + second) ); 
      res();
      }
    },500)
  })
}

// ループ関数
// params引数を追加
function loop( fn, params, i, end) {

  // applyを使って、個々のparamを渡す
  return fn.apply(null, params[i])
  .then( () => {
    if ( i < end ) {
      return loop( fn, params, i+1, end)
    }
    else {
      return Promise.resolve('end');
    }
  })
  .catch( (err) => {
    console.log(err);
  })

}

// 個々のパラメータを用意
var params = [
  [1,10],
  [2,20],
  [3,30],
  [4,40]
]

// 実行
loop(action, params, 0, 3 )
// console
< 1回目です
< パラメータ合計:11
< 2回目です
< パラメータ合計:22
< 3回目です
< パラメータ合計:33
< 4回目です
< パラメータ合計:44
< end

修正はループ関数だけでよい。
むしろ、ループを回すために内部処理に微修正させるとかしたくない。 fnを直接呼び出すのではなく、applyメソッドでparamsをfnの引数として 利用するように指定する。applyめっちゃ便利。


  1. ループ内処理は同じ処理である

  2. チェーンさせて処理をつなげるため、whileやfor文ではループ処理ができないのだと思っている。