msawady’s learning memo

ただのJavaエンジニアが、学んだことをログっていくところ

【Javascript】【DataTables】DataTables を利用した Bootstrap テーブルのフィルタ機能

テーブルのフィルタリングを実装

  • DataTables を使ってフィルタリング機能を実装しました

DataTables | Table plug-in for jQuery

  • フッターにセレクトボックスを利用したエクセル的なフィルタリングを出来るようになっています。

ソース

build.sbtに依存関係を追加

webJarsを利用してインポートを行います。

libraryDependencies += "org.webjars.bower" % "datatables" % "1.10.15"

html の headの中でインポート

    @Html(webJarsUtil.css("jquery.dataTables.min.css"))
    @Html(webJarsUtil.css("dataTables.bootstrap4.min.css"))
    @Html(webJarsUtil.script("jquery.dataTables.min.js"))
    @Html(webJarsUtil.script("dataTables.bootstrap4.min.js"))

DataTables を読み込み

    var todoTable = $('#todoTable').DataTable({
         initComplete: function () {
             this.api().columns().every( function () {
                 var column = this;
                 var select = $('<select><option value=""></option></select>')
                     .appendTo( $(column.footer()).empty() )
                     .on( 'change', function () {
                         var val = $.fn.dataTable.util.escapeRegex(
                             $(this).val()
                         );

                         column
                             .search( val ? '^'+val+'$' : '', true, false )
                             .draw();
                     } );

                 column.data().unique().sort().each( function ( d, j ) {
                     select.append( '<option value="'+d+'">'+d+'</option>' )
                 } );
             } );
         }
     } );

DataTables example - Individual column searching (select inputs) からコピペしています。

var todoTable = $('#todoTable').DataTable(

とすることで、テーブルをDataTableにすることができます。

                 var select = $('<select><option value=""></option></select>')
                     .appendTo( $(column.footer()).empty() )
                     .on( 'change', function () {
                         var val = $.fn.dataTable.util.escapeRegex(
                             $(this).val()
                         );

                         column
                             .search( val ? '^'+val+'$' : '', true, false )
                             .draw();
                     } );

              

html に 空のtfootタグを追加し上記スクリプトを読み込むと、各カラムのフッターにセレクトボックスを追加し、on('change')で選ばれた値と同一の値を持つレコードでフィルタ(検索→再描画)をすることが出来ます。

 column.data().unique().sort().each( function ( d, j ) {
                     select.append( '<option value="'+d+'">'+d+'</option>' )
                 } );

セレクトボックスの中身を作っているところです。各カラムのデータをuniqして、セレクトボックスを作っています。

結果

f:id:msawady:20170919220930p:plain “DOOING” を選ぶと、DOINGのTODOをフィルタすることが出来ます。 f:id:msawady:20170919221048p:plain

感想

  • なんともいえない、「これじゃない」感がありますね….
  • header 側に上手くつけたかったんですが、力及ばず…
  • kendo grid のほうが自分のイメージに合ってるので、次回はこっちへの差し替えをやってみようかなと思います。

demos.telerik.com

  • ライブラリを使いやすいようにハックする力がないと、思ったようにフロントエンドの実装を進められないなぁという小並感。
  • いやまぁ力不足以外のなにものでもないんですが….

以上です。なんとも歯がゆい一週間でした。

障害対応はエンジニアの地力である「問題解決能力」が試される

最近、障害対応で思ったことをポエムしておく

  • 自分は開発か要件定義・設計を担当することが多く、保守の仕事をあまりしてきませんでした
  • 最近、障害対応が増えたので、感想というかポエムをつぶやいておきます

障害対応は、エンジニアとしての地力が試される

障害対応のとき、エンジニアには以下のことが求められます。

  • 冷静さを保つ
  • 問題を正確に切り分ける
  • 過不足なくオペレーションを行う

冷静さを保つ

当然ですが、障害が発生したら焦ります。

場合によってはタフな時間に対応を行うことが求められます。

そのようなときでも、決してヤケにならず、悲観的になりすぎず、

  • 問題を正確に切り分ける
  • 過不足なくオペレーションを行う

の2点を遂行し続ける精神力が必要になります。若干フィジカルかつメンタルな話では有りますが、これはエンジニアのポータブルな能力の一つだと思います。

常に落ち着いている人が1人いるだけで、チームとしての落ち着きが変わります。

全体のパフォーマンス向上という面から見ても、「常に冷静」というのはとても大事なエンジニアの能力なのではないかと思います。

問題を正確に切り分ける

障害というのは当然イレギュラーな状況です。その状況においてファクトを正確に収集することが、障害対応では必要不可欠です。

  • 何がイレギュラーな状態なのか
  • 何をきっかけにイレギュラーな状態になっているか
  • 逆にどこまでがマトモな状態なのか

そしてファクトを分析し、事実関係・根本原因を推察する能力も必要です。

  • 何が根本原因なのか、何がボトルネックなのか
  • 再発するのか、するとしたら次はいつになるか
  • 何がトリガーになったのか、なぜ今まで発生しなかったのか

ファクトの収集・分析はシステムの知識を持っているエンジニアにしか出来ないことです。また、正確に仮説検証を繰り返しながら根本原因を探るという能力も試されます。

過不足なくオペレーションを行う

調査段階においても、実際の対応においても、「過不足なく」オペレーションを行うことが重要です。

オペレーションが不足してはいけないのはもちろん、過剰なオペレーションも避けなくてはいけません。

システムがイレギュラーな状態になっているので、思いもよらぬ副作用をもたらし複雑骨折する可能性が有るからです。

だからといって、何もしないわけには行きません。慎重かつ迅速に調査・復旧を行う必要があります。

  • 目的に適うオペレーションを行うこと
  • オペレーションの前提条件を確認すること
  • オペレーションのリスク、副作用を把握すること
    • リスクを軽減するための方策は無いか確認すること

上記のようなオペレーションを行うためには、問題の切り分けが出来ることが不可欠です。加えて、漏れなく影響を把握する視野の広さ、システム全般に対する広く深い理解が求められます。

まとめ

障害を対応するためには、「冷静に状況と向き合い、正確に問題を切り分け、必要十分なオペレーションをする」ことが求められます。

「プログラミング能力とは別に物凄い勢いでキーボードを叩いてプログラムを完成させてしまう能力のことではない」というツイートが話題になりました。私も同感でエンジニアの能力はあくまで「問題解決能力」だと考えています。

togetter.com

障害対応は、まさしく問題解決能力を試されるものです。勿論、障害なんて無い方が良いに決まってるんです。ただ起こってしまったとしても「これは俺のエンジニアとしての力が試されている...!」と思うと、ちょっと気分が上がって、辛さが薄れるんではないかなぁ、と思ったのでこの記事を書きました。

以上です。皆さんが今夜も?ゆっくりお休み出来ることを心よりお祈りしています。おやすみなさいませ。

【Scala】【Play Framework】Bootstrap のモーダルダイアログに書いた内容をDBにinsertする

TODOの新規追加を出来るようにする

Bootstrap を利用した Modal Dialog の追加

依存関係(tether.js)の追加

bootstrap.jsを動かすにあたり、jQueryだけでなくtether.jsも必要なので、webJarsを利用してインポート

libraryDependencies += "org.webjars.bower" % "tether" % "1.4.0"
    @Html(webJarsUtil.script("jquery.min.js"))
    @Html(webJarsUtil.script("tether.min.js"))
    @Html(webJarsUtil.script("bootstrap.min.js"))

bootstrap.js の前に jQuery, tether.js のimport を行う必要があるので注意。

Modal Dialog の追加

公式ドキュメントのサンプルコードをコピペしています。

v4-alpha.getbootstrap.com

<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addTodoModal">
   Add TODO
</button>

<div class="modal fade" id="addTodoModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLabel">Input TODO</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <label for="new-todo" class="form-control-label">Title:</label>
                    <input type="text" class="form-control" id="new-todo">
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" id="add-todo">Add TODO!!</button>
            </div>
        </div>
    </div>
</div>

コピペにあたっての注意点としては、button のdata-target属性とmodal-dialog の idは揃える必要があるよ..くらいですね^^;

サーバーへの永続化リクエストは以下のようにJavascriptを書きます.

    $('#add-todo').click(function() {
        var todo_title = $('#new-todo').val()
        if(todo_title){
            $.get('todo/add',
            {
                title: todo_title
            },
            function(){
                $('#myModal').modal('hide');
                location.reload()
            })
        }
    });

コールバック処理で、ダイアログを閉じてリロードするようにしています。Javascriptからモーダルダイアログを閉じるには以下のように書きます。

$('#myModal').modal('hide');

サーバーでの永続化処理の実装

めちゃめちゃ簡単です。Play Framework, slickともに素晴らしい…!

ルーティングの追加

conf/routes に以下を追加します。

GET     /todo/add                 controllers.TodoController.add(title)

Controller, Service, Repositoryの実装

処理をRepositoryまで持っていくだけなのであっさりと。

Controller
  def add(title: String) = Action {
    todoManager.add(title)
    Ok(todoView(todoManager.list))
  }
Service
def add(title: String): Unit = todoRepository.addTodo(new Todo(0, TodoStatus.UNDONE, title))

AUTO_INCREMENT なフィールドを持つレコード(今回はidがAUTO_INCREMENT)をinsertするときはデフォルト値として0をセットしておくと、INSERTのタイミングでよしなに採番されます。

Repository
 def addTodo(todo: Todo) = {
    database.run(Todos += todo)
  }

これで永続化できます。slick素晴らしい。

結果

Add TODOボタンを押すと f:id:msawady:20170910154958p:plain こんな感じのダイアログが出るので、Todoを入力 f:id:msawady:20170910155105p:plain ちゃんと採番されて追加されます f:id:msawady:20170910155151p:plain

感想と次にやること

  • めちゃめちゃあっさり行けたなぁという印象です。特にサーバー側はほとんど工夫してないですね。
    • Play Framework, slick, bootstrap が使いやすくて助かりました。
  • あとはDelete(論理削除), フィルタリング, メモ機能の追加あたりをやってみたい。

【Scala】【Play Framework】Slick を使ってMySQLのDBに接続する

やっとデータベースへの接続を

  • ファイルの読み書きでやっていたデータストアをデータベースにする
  • MySQLデータベースへの接続
  • ORMとしてSlickを使う

MySQLデータベースへの接続

事前にテーブル作成、初期データ挿入

create table todo(
    id bigint(20) NOT NULL AUTO_INCREMENT,
    status varchar(31) NOT NULL,
    title varchar(255) NOT NULL,
    PRIMARY KEY (ID)) AUTO_INCREMENT=10000;

insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の要件定義');
insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の設計');
insert into todo(id, status, title) values(null, 'UNDONE', 'すごい機能の実装');

10000始まりのauto increment にしたい場合はcreate table の末尾にAUTO_INCREMENT=10000 をつけます。 また、insertの際はidをnullで入れることでよしなにインクリメントしてくれます。

依存ライブラリの定義

build.sbtにjdbc, slick, mysql-connectorの依存関係を追加します。

libraryDependencies += jdbc
libraryDependencies += "com.typesafe.slick" %% "slick" % "3.2.1"
libraryDependencies += "mysql" % "mysql-connector-java" % "6.0.6"

データベースの設定書き込み

conf/application.conf に接続先データベースを定義

todo-db = {
  url = "jdbc:mysql://localhost/todo_play?characterEncoding=UTF8&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC"
  driver = com.mysql.jdbc.Driver
  user = "user"
  password = "password"
  connectionPool = disabled
}

url のパラメータには呪文が書かれていますが、、、

  • characterEncoding=UTF8&useUnicode=true : これは全角文字の文字化け防止
  • `useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC これをいれないとTimezoneのパースでエラーになります。
SQLException: The server time zone value '���� (�W����)' is unrecognized or represents more than one time zone.
You must configure either the server or JDBC driver (via the serverTimezone configuration property) 
to use a more specifc time zone value if you want to utilize time zone support.]

まったく理解してないですが、サーバーのtimezoneをちゃんと設定してあげる必要があるんだけど、呪文によってJDBCの設定で動くようにしているんじゃないかなーくらいに予想してます。 stackoverflow.com

RepositoryクラスでDatabaseの読み込み

import slick.driver.MySQLDriver.api._
.
.
private val database = Database.forConfig("todo-db")

MySQLDriverをインポートして、Database.forConfig("DB設定")をすることでセッションをはれます。

ORMとしてSlick利用

Entity クラスを編集

Slick によるマッピングができるよう、Domain Entityを編集します。

import slick.driver.MySQLDriver.api._

/**
  * entity class of todo
  */
case class Todo(id: Int, var status: TodoStatus, title: String)

class Todos(tag: Tag) extends Table[Todo](tag, "todo") {
  def id = column[Int]("id", O.PrimaryKey, O.AutoInc)

  def status = column[TodoStatus]("status")

  def title = column[String]("title")

  def * = (id, status, title) <> (Todo.tupled, Todo.unapply)

}
object Todos extends TableQuery(new Todos(_))

case classに実装されるtupled, unapply を利用してデータの展開を行いますが、自然体だと

 def status = column[TodoStatus]("status")

自作したsealed traitのTodoStatusによってunapplyができません。

sealed traitにマッパーを実装

import slick.driver.MySQLDriver.api._

sealed trait TodoStatus

/**
  * todo status
  */
object TodoStatus {

  case object UNDONE extends TodoStatus

  case object DOING extends TodoStatus

  case object DONE extends TodoStatus

  implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String](
    _.toString, TodoStatus.withName(_)
  )

  def withName(s: String) = s.toLowerCase match {
    case "undone" => UNDONE
    case "doing" => DOING
    case "done" => DONE
  }

}

こんな感じでimplicit val としてマッパーを実装してあげると、unapplyが出来るようになります。

  implicit val todoStatusMapper = MappedColumnType.base[TodoStatus, String](
    _.toString, TodoStatus.withName(_)
  )

Repository クラスの実装

Slick の力を借りることで、とてもスッキリとした形でORマッピングが可能になっています。 可読性も高く、Repository クラスがビジネスロジックの実装に集中できる形ですね。

package services.repository

import com.google.inject.Singleton
import services.domain.{Todo, TodoStatus, Todos}
import slick.driver.MySQLDriver.api._

import scala.concurrent.Await
import scala.concurrent.duration.Duration

/**
  * repository class for todo management
  *
  */
@Singleton
class TodoRepository {

  private val database = Database.forConfig("todo-db")

  def getTodoList(): List[Todo] = {
    val f = database.run(Todos.sortBy(_.id).result)
    Await.result(f, Duration.Inf).toList
  }

  def updateTodoList(ids: List[Int], status: TodoStatus) = {
    val toBeUpdated = Todos.filter(row => row.id inSetBind ids)
    database.run(toBeUpdated.map(_.status).update(status))
  }

}

Slickを利用すると、クエリの結果は全てFutureに包まれて帰ってきます。 結果を待ってからレスポンスを返すか、非同期にレスポンスを返すかは実装者の側で選択する必要があります。 ここでは、同期的に結果を待ってから返すようにしています。

    val f = database.run(Todos.sortBy(_.id).result)
    Await.result(f, Duration.Inf).toList

感想

  • あっちこっちの設定で色々とハマりましたが、なんとかデータベースとの接続が出来ました。
  • Slick は設定さえできれば、Repositoryクラスをかなり綺麗に書けて便利だなぁという印象です。
    • DDL の生成とかも出来るようなので、先にテーブルを作ってしまったのはちょっと後悔。
  • Scalaapply, unapply, implicit など、Scala特有の?暗黙的なクラスやオブジェクトの仕組みも少しずつ分かってきた気がします。
  • 次は画面からのaddを実装したいですね。

以上です。なかなか苦しみましたが、非常に勉強になりました。

【Java】abstract と interface の使い分け 〜「オブジェクト指向でなぜ作るのか」から学ぶ〜

オブジェクト指向でなぜ作るのか」の読書メモとして

あたりは全然分かっていなかったなと本書を読むことで実感しました。なので

  • オブジェクト指向は何を解決したかったのか
  • abstract class は何を解決したのか
  • interface は何を解決したのか
  • abstract class と interface の使い分け

について書きたいと思います。対象読者はJava初級〜中級者になります。

オブジェクト指向は何を解決したかったのか

機械語アセンブリ高級言語(e.g:FORTRAN)→構造化言語(e.g: C言語)とプログラミング言語は進化しましたが、以前として解決されない問題が有りました。

そして問題を加速させたのが、以下の問題でした。

  • システム要件の複雑化(本格的な業務利用)
  • コードベースの巨大化

それらの問題を解決するために以下のような方針で解決策が取られました。こういった方針を具体化したプログラミング手法がオブジェクト指向プログラミングであると本書では述べられています。

方針 解決したかったこと
変数、サブルーティンの整理・集約
重複するサブルーティンの集約
コードベースの巨大化
システム要件の複雑化
共通メインルーティンの作成 モジュールの再利用が困難
変数の隠蔽、ローカル変数→インスタンス変数 グローバル変数の管理

abstract class は何を解決したのか

方針 解決したかったこと
変数、サブルーティンの整理・集約
重複するサブルーティンの集約
コードベースの巨大化
システム要件の複雑化

この問題を解決するためのキーワードは「継承」です。まず、「クラス」という仕組みを導入すことで、変数、サブルーティンの整理・集約が行いやすくなりました。 さらに、「abstract class の継承」を導入することで、「整理されたクラス間で重複するサブルーティンの集約(脱コピペ!)」を直感的に行えるようになり、システム開発の生産性・保守性を大きく上げました。

実装コストと言う面から見ると、以下のようなメリットが有ります。

  • 重複する変数・サブルーティンを共通化することによる、コードベースの縮小
  • コードを整理することによる、ソースコード管理の効率化(=保守性の向上)
  • 修正の分散を抑えることによる、保守性の向上

また、設計の質を向上させる効果も有りました。

  • 共通するサブルーティンを集約するための、外部・内部設計のブラッシュアップ
    • 共通のサブルーティンを持つ=似たような機能/属性である、という抽象化

これにより、複雑なシステム要件を「現実世界に則した」形で表現し、直感的に設計することが可能になりました。

サンプルとしては以下のような形になります。定番ではありますが、DogクラスとHumanクラスを実装する例を考えます。

before

abstract クラスを利用しない場合は、重複するコードが発生します。

public class Dog{
    private int legCount;
    
    public Dog(){
       this.legCount = 4; 
    }

    public boolean isMammal(){
        return true;
    }
    
    public boolean isFourLeg(){
        return true;
    }
}

public class Human{
    private int legCount;

    public Human(){
        this.legCount = 2;
    }
    
    public boolean isMammal(){
        return true;
    }
    
    public boolean isFourLeg(){
        return false;
    }
}

after

abstract なMammalクラスを実装し、それを継承することで、コードの重複を減らすことが出来ます。 また、isFourLegメソッドについては「犬はtrue, 人間はfalse」という定義から、「4本足かどうか」という抽象的かつ直感的な再定義を行うことが出来ています。

public abstract class Mammal {
    private int legCount;

    public Mammal(int legCount) {
        this.legCount = legCount;
    }

    public boolean isMammal() {
        return true;
    }

    public boolean isFourLeg() {
        return this.legCount == 4;
    }
}

public class Dog extends Mammal {
    public Dog() {
        super(4);
    }
}

public class Human extends Mammal {
    public Human() {
        super(2);
    }
}

interface は何を解決したのか

方針 解決したかったこと
共通メインルーティンの作成 モジュールの再利用が困難

この問題を解決するためのキーワードは「ポリモーフィズム」です。「ポリモーフィズム」を実現することで、「共通化されたメインルーティン」を作成しやすくなり、「再利用可能なモジュール」を作成することが可能になりました。

ちょっと分かりづらいかと思うのでサンプルを。雑なサンプルですが、伝えやすくはなるかと。

before

もし、Listというinterfaceが存在しない世界で、「ArrayList のすべての要素をnew Element()に変更する」メソッドと「LinkedList のすべての要素をnew Element()に変更する」メソッドを実装しようとすると、以下のようになります。

public class HogeService {

    public void replaceWithNewElement(ArrayList<Element> list) {
        int size = list.size();
        list.removeAll();
        for (int i = 0; i < size; i++) {
            list.add(new Element());
        }
    }

    public void replaceWithNewElement(LinkedList<Element> list) {
        int size = list.size();
        list.removeAll();
        for (int i = 0; i < size; i++) {
            list.add(new Element());
        }
    }
}

まったく同じことをやっているのに、ArrayListLinkedListという2つのクラスを処理するためにreplaceWithNewElementというメインルーティンを2つ用意する必要が有ります。言い方を変えると、replaceWithNewElement(ArrayList list)replaceWithNewElement(LinkedList list)として再利用できません。

after

Listというsize(), removeAll(), add()といったメソッドを持つ interface を作成します。

public interface List<E> {

    public int size();

    public boolean removeAll();

    public boolean add(E element);
    
}

そして、interface は実装を提供せず、仕様を提供します。「実現の仕方は約束しないけど、このメソッドはこういうことをする」という仕様の約束だけを行います。上記3メソッドについては以下のように仕様が定められます。

  • size(): このリスト内にある要素の数を返します。
  • removeAll(): このリストから、指定されたコレクションに含まれる要素をすべて削除します。
  • add(): 指定された要素をこのリストの最後に追加します。

List (Java Platform SE 8) *1

そして、ArrayListLinkedListという実装クラスはListという interface の仕様に対して、実装を提供します。

public class ArrayList<E> implements List<E> {
    public int size(){
        // 具体的な実装
    }

    public boolean removeAll(){
        // 具体的な実装
    }

    public boolean add(E element){
        // 具体的な実装
    }
}

public class LinkedList<E> implements List<E> {
    public int size(){
        // 具体的な実装
    }

    public boolean removeAll(){
        // 具体的な実装
    }

    public boolean add(E element){
        // 具体的な実装
    }
}

そして、replaceWithNewElement の引数をListという interface の仕様に従ったクラスと定義します。

public class HogeService {

    public void replaceWithNewElement(List<Element> list) {
        int size = list.size();
        list.removeAll();
        for (int i = 0; i < size; i++) {
            list.add(new Element());
        }
    }
}

こうすることにより、replaceWithNewElementというメインルーティンは、ArrayListでもLinkedListでも利用できるようになりました。つまり、モジュールの再利用性が高まったのです。

  1. 仕様(interface)を定義
  2. 仕様に対する実装(implement)を定義
  3. interface を処理するメインルーティンを定義する

とすることで、「interface を implement するクラスすべてを扱うことに出来る、再利用性が高いメインルーティン」を作成することができます。

abstract class と interface の使い分け

ここまで書くと、なんとなく伝わってくるでしょうか。abstract class と interface という2つの仕組みは似たような特徴を持ちますが、そもそも解決したい問題が違うのです。

仕組み 目的
abstract class 重複するメソッドの集約
実装における抽象度の整理
interface メインルーティンの再利用性の向上
仕様と実装の分離

ということですね。この目的の違いを意識してあげると、abstract class と interface の使い分けを上手く出来るのではないかと思います。

おわりに

オブジェクト指向でなぜ作るのか」は今回書いたようなプログラミング手法以外にも、設計やプロジェクト運営についても幅広く書かれており、新人〜若手におすすめできる良書です。

特に「継承」「interface」はオブジェクト指向プログラミングにおいて重要な要素である一方で、勉強を進める中でのカベになりやすいところです。「そもそも何を解決したかったのか」から学ぶことで、スッキリ理解することが出来るのではないかと思います。

以上です。長文にお付き合い下さり、ありがとうございました。

*1:java8からは replaceAllが有るので、今回のサンプルのイケてなさが…

【レビュー】【新人向け】レビュイーの心得 ~レビューを通すための準備~

今日のテーマはレビュー

新人だとレビューがどういうものかという実感が湧かず、「レビューに向けてどのようにタスクを進めるか」を意識しづらいのかな、、、と思い今回の記事を書きました。

ここでいう「レビュー」とは

  • 対面
  • スケジュールされている
  • レビュイーが成果物を用意し、レビュワーが指摘をする

ような、いわゆる「レビュー会」を指しています。基本的には名詞で使うことを意識していますが、意味が若干ぶれている部分も有ります。ご容赦ください。

前提: レビューは通すべくして通すものである

まず、これが大前提です。成果物をつくり、「オーケーかな、ドキドキ」じゃないです。 事前に相談、調整をして、万全の状況にしてから望むものです。

指摘の修正コストは未知数

ただ成果物を作って持って行くだけでは、レビューでの指摘事項の量やその修正コストが読めないですよね。

まして、「レビュー会」が行われる時点で、成果物に求められるクオリティは、質・量ともに低くないことがほとんどです。 そのため、指摘事項の量や修正コストの振れ幅は大きくなります。

事前に準備をして、レビューでの指摘事項が出来るだけ出ないようにしておく必要が有ります。

再レビューのコスト・リスクが大きい

「レビューでの指摘事項の反映」に対してもレビュー(対面、書面を問わず)が必要ですが、これについてはレビュワーとしてもコストが大きいです。

書面で瞬殺できる範囲であれば良いのですが、再度の対面レビューを行うようなレベルであると、レビュワーにとっても大きなコストになります。 また、書面で対応できる範囲である場合でも、修正量が増加すれば見落としリスクも増加します。

レビューコスト・レビュー品質という2つの観点からも、指摘事項は少なくする必要があります。

どうやってタスクを進めるか

早めに成果物イメージをすり合わせる

どうすれば100点なの?というイメージが無い状態でレビューに望むのは、ただのギャンブルです。 あらかじめレビュワーに相談し、成果物イメージを早めにすり合わせましょう。

とはいえ、何も叩き台が無い状態で持っていってもレビュワーを困らせるだけです。 まずは作業時間の1割くらいで大枠を作って、大枠が間違っていないかを確認しに行きましょう。

そして大枠について確認する中で、その中身についても少しずつ話を掘って行きます。 出来るだけヒントを引き出し、レビュワーの求める成果物を具体化して行きましょう。

そうすることで、以下のようなメリットがあります。

  • 「大枠から間違ってるんだけど…」という死刑宣告の回避
  • 自信を持って詳細を詰めることができる
  • レビュワーがレビューしやすくなる

1点目は、言わずもがな。

2点目はレビュイーにとってのメリットです。方針が間違ってないことを保証されていれば、迷うことなく作業を進めることが出来るので作業スピードが上がります。
そして、早く終わればセルフレビューの時間を取ることも出来るので、レビューに出す成果物のレベルを上げることに繋がります。

3点目はレビュワーにとってのメリットです。そして結構重要です。レビューをする側としても、「成果物の品質が十分かどうか」を確認するよりも、 「成果物が自分の伝えたイメージ通りになっているか」 を確認するほうが断然ラクで、効率的です。

困ったら早めに相談する

レビューは通すべくして通すものなので、「困っている」状態でレビューに望むのはレビュイーとして論外です。
一方で、問題が解決されないままレビューに持ち込まれるのはレビュワーとしても避けたいので、(状況次第ですが)真面目に解決方法を考えます。 仮に、即座に助けることが出来なくとも、予め連携されていれば他の時間で解決策を考えることも出来ます。

そうしてレビュワーの力を借りることで、問題の解決をグッと進めることが出来ます。
また「困っているときあるある」の、「困らなくていいことに困っている」状態を早めに抜け出すことが出来ます。
タスクを実行していると視野が狭くなり、ハマらなくて良いことにハマることも多いですが、 冷静な第三者の意見を聴くことで、そもそもの方針が間違っていることに気づくことも出来ます。

レビュワーの時間や協力を上手く使いながら、効率よく、品質の高い(= レビューを通りやすい)成果物を作ることが重要です。

終わりに

まだまだ、言い足りない部分もありますが、以下の3点はどんなレビューを受けるにしても基本だと思うので、徹底して欲しいなと思います。

  • 「完璧な成果物」を持ってレビューに望むこと
  • 早めに成果物イメージをすり合わせること
  • 困ったら早めに相談すること

そして、タスク管理やレビューについて記事を書くと、自分の日頃の行いを見直す良い機会になりますね。 今後とも、積極的に記事を書いていきたいと思います。

以上です。長文にお付き合いくださり、ありがとうございました。

【Scala】【Play Framework】ブラウザからのサーバー処理の呼び出し、ファイルの書き込み

ブラウザからのステータス更新処理を実装しました

  • Play Frameworkを用いた Todo 管理アプリの実装を進め、サーバーの更新処理を実装しました
  • あちこち触ることになり記事の内容が若干散漫ですが、ご容赦ください
  • やったこと
    • 前回作成した右クリックメニューにjQueryでのサーバー呼び出しを追加
    • 更新メソッドのルーティングを追加
    • Repository クラス分離、ファイルの書き込み処理を実装
    • Enum の書きっぷりを変更

jQuery でのサーバー呼び出し

$.getを利用する、以下のメソッドを追加します。

    function reqUpdateTodoStatus(status){
        $.get('/todo/update',
            {
                ids: getSelectedIds,
                status: status
            },
            function(){location.reload()}
        )
    }

第1引数はリクエスト先のURL, 第2引数はリクエストのパラメータ、第3引数はコールバック処理です。 コールバック処理で画面の更新(location.reload())を行うことで、即座に画面に変更内容が反映されます。

更新メソッドのルーティングを追加

config/routes に以下のrouteを追加

引数の型がStringだったら、型は省略できます。

GET     /todo/update              controllers.TodoController.update(ids, status)

Controller

  def update(ids: String, status: String) = Action {
    todoManager.update(ids.split(",").map(_.toInt).toList, status)
    Ok(todoView(todoManager.list))
  }

困っているところ

自分としては、Int、百歩譲ってStringの配列で引数を回したかったんですが、どうも"10001, 10002"のようなカンマ区切りのStringで来てしまいます。
ココらへんの記事を参考にして色々とやってみたのですが上手く行かず,,, リクエストの投げ方が悪いのか、受け取り方が悪いのか、もうちょっと試行錯誤したいと思います。

Repository クラス分離、ファイルの書き込み処理を実装

Service クラスと Repository クラスを分離し、Repository クラスにファイルの書き込み処理を実装しました。

Service

package services

import javax.inject.Inject

import com.google.inject.Singleton
import services.domain.TodoStatus
import services.repository.TodoRepository

/**
  * service class for todo management
  */
@Singleton
class TodoManager @Inject()(todoRepository: TodoRepository) {

  /**
    * get Todo List
    *
    * @return todo-list order by id
    */
  def list = todoRepository.getTodoList()

  /**
    * update todo status
    *
    * @param ids    id list of target TODO
    * @param status TodoStatus change to
    */
  def update(ids: List[Int], status: String): Unit = todoRepository.updateTodoList(ids, TodoStatus.withName(status))

}

かなりスッキリしました。基本的に、Inject している repository のメソッドをコールするだけになっています。

Repository

package services.repository

import java.io.PrintWriter

import com.google.inject.Singleton
import play.Environment
import services.domain.{Todo, TodoStatus}

import scala.collection.mutable
import scala.io.Source

/**
  * repository class for todo management
  *
  */
@Singleton
class TodoRepository() {

  private val filePath = "data/TODO.txt"
  private val encoding = "UTF-8"

  def getTodoList(): List[Todo] = {
    sortById(readTodoFile())
  }

  def updateTodoList(ids: List[Int], status: TodoStatus) = {
    val modified = readTodoFile()
    ids.foreach(id => {
      val todo: Todo = modified.get(id).get
      todo.update(status)
      modified.put(todo.getId, todo)
    })
    writeTodoFile(sortById(modified))
  }

  private def readTodoFile(): mutable.Map[Int, Todo] = {
    val map: mutable.Map[Int, Todo] = new mutable.HashMap[Int, Todo]
    val s = Source.fromFile(Environment.simple().getFile(filePath), encoding)
    val strList: List[String] = try s.getLines.toList finally s.close()
    strList.foreach(str => {
      val data: Array[String] = str split "\t"
      val todo: Todo = new Todo(data(0).toInt, TodoStatus.withName(data(1)), data(2))
      map.put(todo.getId, todo)
    })
    return map
  }

  private def writeTodoFile(list: List[Todo]) = {
    val pw = new PrintWriter(filePath, encoding)
    try list.foreach(todo => pw.write(todo.toTsvString + "\n")) finally pw.close()
  }

  private def sortById(map: mutable.Map[Int, Todo]): List[Todo] = {
    map.values.toList.sortBy(_.getId)
  }
}
  • ファイルの書き込みライブラリは scala.io に用意されていないのでjava.io.PrintWriter を利用します。(それにしても try~catchがゆるく書けて良いですね)
private def writeTodoFile(list: List[Todo]) = {
    val pw = new PrintWriter(filePath, encoding)
    try list.foreach(todo => pw.write(todo.toTsvString + "\n")) finally pw.close()
  }
  • workspace の中のファイルを読み込むために、Environment.getFile()を利用しています。 Environment.simple()でルートからの相対パスを指定してファイルを選択できます。
val s = Source.fromFile(Environment.simple().getFile("data/TODO.txt"), encoding)

Enum の書きっぷりを変更

Enumeration クラスを使うよりは sealed traitを使う方が柔軟に行けそうだったものの、JavaでいうvalueOfが無いのは辛かったので object の中に case objectを列挙し、withNameを実装しました。

package services.domain

sealed trait TodoStatus

/**
  * todo status
  */
object TodoStatus {

  case object UNDONE extends TodoStatus

  case object DOING extends TodoStatus

  case object DONE extends TodoStatus

  def withName(s: String) = s.toLowerCase match {
    case "undone" => UNDONE
    case "doing" => DOING
    case "done" => DONE
  }
}

動作確認

“UNDONE"な2つを"DOING"に。 f:id:msawady:20170820221421p:plain f:id:msawady:20170820221525p:plain

感想と、次にやること

  • パラメータの型にはハマったものの、サクッとブラウザからサーバーへのメソッドが通ったのは嬉しかったです。
    • パラメータの型については、もう少しきれいなやり方がないか頑張りたい。
    • いっそ、ReqMessageクラスを作って、Jsonで展開するほうが楽かもしれない。
  • Enumだったり、ファイルの書き込みだったり、サーバー側のScalaの知識もついたなぁと。
  • updateが出来たので、add/delete も実装したい
  • データの管理をDBにしたい、Scala書いてる方が楽しいけど、ちゃんと勉強しなければ。

以上です。今週は色々と触れて勉強になりました。