水無瀬のプログラミング日記

プログラムの勉強した時のメモ

Yahoo天気をスクレイピングして予報をSlackへ通知する

はじめに

ちょっと前にYahoo天気の指数情報に洗濯や熱中症などに混じって、
ビールが追加されて話題になってました。
数ある天気予報の中でビールの指数を出しているところは見たことがなかったので、
Yahoo天気の予報をSlackに通知して毎朝確認しよう!と思ったのが今回の話。
APIを提供してくれていれば楽だったんだけど、
無さそうだったのでスクレイピングして無理やり取ってくることにする。

今回はtypescriptでサクッと作ったものをAWSに乗せて使うことにする。

TL;DR.

コード

事前準備

コード側

今回使うライブラリなどをinstallしておく。

# 今回使うlibrart(httpリクエスト用、HTMLパーサ)
$ npm install --save request jsdom
# 型定義(好みで)
$ npm install --save-dev @types/request @types/jsdom
# その他
# TSとかeslintとかwebpackとかetc...

Slack側

ここからwebhook許可しとく

追加手順

プルダウンから通知を送りたいチャンネルを指定してAdd Incoming WebHooks integrationをクリック。
(もしかしたら日本語表記だと違うかも)

f:id:minase_mira:20190628215613p:plain

Setup InstructionsにあるWebhook URLをコピーしておく。 これでSlack側の設定は完了

f:id:minase_mira:20190628215746p:plain

実装

実践ドメイン駆動設計をレイヤードアーキテクチャの辺りまで読んだので、
レイヤードアーキテクチャ的に実装していくことにする。
+ typescriptでDIをやるためにinversifyを入れる。
必須ではないので必要であれば追加でインストール(npm install --save inversify)。

Interface

とりあえず叩かれる用。
受け取るパラメータも特にないので、ただapplication層を呼び出すだけ。

コード中の@injectable()@inject()inversify用の設定。
@injectable()はDIされる側の設定。interfaceの実装食らうに付けておく。
@inject()はDIする側の設定。これで、interfaceを呼び出せば対応する実装クラスを引っ張ってこれる。

Application

Domain使ってるところ。
Domデータ取ってくる→欲しいデータに整形する→Slackへ通知の流れ。

Domain

ScrapingService

サイトにGet飛ばしてhtml取ってくるところ。
取ってきたデータをjsdomを使ってパースして返す。

ConverterService

パースしたデータを使いやすいように整形するところ。
整形したり、必要なデータを取り出したりしている。

ゴリ押しに次ぐゴリ押しで実装してしまったので、 もうちょい綺麗にしたい。。。

InformSlackService

Slcakへ通知するところ。
送るデータを受け取って、Slackのwebhookで送れる形にしてあげる。

Infrastructure

HttpRequest

requestをラップしているだけ。
ここももう少し綺麗にしたいところ。
他のライブラリに変えても良い気がするし、promiseかまさなくても良いようにしたい。

inversify用設定

inversifyのGithubを参考にしながら設定を進める。

inversify.config.ts

interfaceと実装を紐づける設定。
DIコンテナ的な設定の認識。

const container = new Container();
container
  .bind<WeatherNewsService>(TYPES.WeatherNewsService)
  .to(WeatherNewsServiceImpl);
container
  .bind<ConverterService>(TYPES.ConverterService)
  .to(ConverterServiceImpl);
container
  .bind<InformSlackService>(TYPES.InformSlackService)
  .to(InformSlackServiceImpl);
container.bind<ScrapingService>(TYPES.ScrapingService).to(ScrapingServiceImpl);
container.bind<HttpRequest>(TYPES.HttpRequest).to(HttpRequestImpl);
container.bind<WeatherNews>(TYPES.WeatherNews).to(WeatherNews);

export { container };

inversify.type.ts

実行時に識別子が必要なので、宣言しておく。
公式にならってSymbolを使っているが、クラスでも文字列でもOKだそう。

const TYPES = {
  WeatherNewsService: Symbol.for('WeatherNewsService'),
  ConverterService: Symbol.for('ConverterService'),
  InformSlackService: Symbol.for('InformSlackService'),
  ScrapingService: Symbol.for('ScrapingService'),
  HttpRequest: Symbol.for('HttpRequest'),
  WeatherNews: Symbol.for('WeatherNews')
};

export { TYPES };

実行してみる

ここまでのコードを実行してみる。
実際に作ったものはAWSに乗せるように設定が変わっているけど、
概ね下記のようなwebpack.configを用意すればビルドできるはず。

// output.pathに絶対パスを指定する必要があるため、pathモジュールを読み込んでおく
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const slsw = require('serverless-webpack');

module.exports = {
  mode: slsw.lib.webpack.isLocal ? 'development' : 'production',
  entry: slsw.lib.entries,
  devtool: 'source-map',
  target: 'node',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          {
            loader: 'ts-loader'
          },
          {
            loader: 'eslint-loader'
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.json', '.ts'],
    modules: ['node_modules']
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  externals: [nodeExternals()]
};

下記のようなメッセージがSlackに通知されればOK。

f:id:minase_mira:20190628215832p:plain

まとめ

実装して動かしてみるところまでやった。
思ったより長くなりそうなので、今回はここまでにする。
次回serverless frameworkを使ってAWSに上げるところまでやる。
それでは今回はこのへんで。

追記

次回やった

参考

yaml書き方メモ

はじめに

最近設定ファイルをyamlで書くことがある今日このごろ。
毎回書き方忘れて調べてるのでまとめることにする。 JSONならわかるので比較しながら書いていく。

基本構文

JSONと同じ(はず)。
ただ、文字列を"で囲って上げる必要はない。
囲んでも問題ないし、\nとかの特殊文字や空白を含めるたい時は囲って上げる必要がある。

ちなみにyamlにはコメントがある。
先頭に#を付けた行はコメント行になる。

配列

yaml

# ブロック形式
- aaa
- bbb
- ccc    
#  インライン形式
[aaa, bbb, ccc]

json

["aaa", "bbb", "ccc"]

連想配列

yaml

key1: value1
key2: 100

json

{
  "key1": "value1",
  "key2": 100
}

組み合わせ

yamlの方は自信ないかも

yaml

key1:
  - value1
  - value2
key2:
  - value3:
  - hoge
  - huga
  - value4:
  - piyo
  - poyo
key3:
  - value5: foo
    value6: bar
  - value7: 100
    value8: 200

json

{
  "key1": [
    "value1",
    "value2"
  ],
  "key2": [
        {
            "value3": [
                "hoge",
                "huga"
            ]           
        },
        {
            "value4": [
                "piyo",
                "poyo"
            ]
        }
  ],
  "key3": [
    {
    "value5": "foo",
    "value6": "bar"
    },
    {
    "value7": 100,
    "value8": 200
    }
  ]
}

まとめ

ざっくり書き方をまとめてみた。
公式サイトを見る限り他にも色々あるっぽいけど、
とりあえずこれくらい押さえておけば書けそうなので今回はここまでにしておく。
また何かあったら調べようと思う。
それでは、今回はこのへんで。

参考

NestJS事始め

はじめに

expressの書き方に疲れてspringを使おうか迷っていた今日このごろ。

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

と謳っているNestJSなるフレームワークがあると聞いたので触ってみる。

TL;DR.

今回作ったサンプル

Install

公式のcliがあるのでサクッとインストール。

$ npm install -g @nestjs/cli

Projectの作成

$ nest new project-name
> npm か yarnどっち使うか聞かれるので好みの方を選択

これでアプリの雛形が作成される。 生成されるファイルは下記の通り。

.
├── README.md
├──node_modules
│   └── 省略
├── nest-cli.json
├── nodemon-debug.json
├── nodemon.json
├── package-lock.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
└── tslint.json

とりあえず起動してみる

@nestjs/cliは後述するが色々ファイルを生成してくれる。
してくれるが、起動してくれる機能は無いので生成されたpackage.jsonに書いてあるnpm scriptを使う。
port3000で起動するので、とりあえずcurlで叩いてみる。

$ npm start
[Nest] 45934   - 06/05/2019, 10:08 PM   [NestFactory] Starting Nest application...
[Nest] 45934   - 06/05/2019, 10:08 PM   [InstanceLoader] AppModule dependencies initialized +19ms
[Nest] 45934   - 06/05/2019, 10:08 PM   [RoutesResolver] AppController {/}: +7ms
[Nest] 45934   - 06/05/2019, 10:08 PM   [RouterExplorer] Mapped {/, GET} route +6ms
[Nest] 45934   - 06/05/2019, 10:08 PM   [NestApplication] Nest application successfully started +3ms
    
$ curl localhost:3000/
> Hello World!

無事動いているようなので手を加えていってみる。

Controller,Serviceを追加してみる

公式サイトのDocumentを参考にしながら、新しいController,Serviceを作ってみる。

# ファイルの生成とmoduleへの追加を自動でしてくれる
$ nest generate controller sample
> CREATE /src/sample/sample.controller.spec.ts (493 bytes)
> CREATE /src/sample/sample.controller.ts (101 bytes)
> UPDATE /src/app.module.ts (397 bytes)

# `g`はgenerateのエイリアス。 sは`service`のエイリアス。(controllerのエイリアスは`co`)
$ nest g s sample 
> CREATE /src/sample/sample.service.spec.ts (460 bytes)
> CREATE /src/sample/sample.service.ts (90 bytes)
> UPDATE /src/app.module.ts (597 bytes)

生成されたファイル

controller

import { Controller } from '@nestjs/common';

@Controller('sample')
export class SampleController {}

service

import { Injectable } from '@nestjs/common';

@Injectable()
export class SampleService {}

Controller,Serviceをちょっと修正

このままでは増えただけなので、ちゃんと中身を書いていく。 とりあえずGET,POST,PUT,DELETE辺りを実装してみる。

Controller

公式サイトのControllerを参考にしながら、書いていく。
色々アノテーションを付けた。今回使って無いのもあるので、詳しくはこちらから。

import {Body, Controller, Delete, Get, HttpCode, Param, Post, Put, Req} from '@nestjs/common';
import {SampleService} from './sample.service';

@Controller('sample')
export class SampleController {
    constructor(private readonly sampleService: SampleService) {}

    @Get()
    public getSample(): string {
        return this.sampleService.getSample();
    }

    /**
     * パスの値を可変にしたい場合、express同様`:変数名`でOK
     * @param params Expressのreq.paramsと同義
     */
    @Get(':id')
    public getSamplePath(@Param() params): string {
        return this.sampleService.getSampleById(params.id);
    }

    /**
     * requestの全量は`@Req()`をつけることで取得できる
     * @param req Expressのreqと同義
     */
    @Post()
    @HttpCode(201)
    public postSample(@Req() req): any {
        return this.sampleService.registerSample(req.body);
    }

    /**
     * Putサンプル
     * @param params パスから受け取る
     * @param body Expressのreq.bodyと同義。bodyだけ取得できる
     */
    @Put(':id')
    public putSample(@Param() params, @Body() body): any {
        return this.sampleService.updateSample(params.id, body);
    }

    /**
     * HttpCodeでレスポンスのステータスコードを指定できる。
     * @param id 直接取得したい場合は`@Param()`の引数に変数名を指定してあげればOK
     */
    @Delete(':id')
    @HttpCode(204)
    public deleteSample(@Param('id') id: string): void {
        this.sampleService.deleteSample(id);
    }
}

Service

こちらはデフォルトとにならってメソッドを増やしただけ。

import { Injectable } from '@nestjs/common';

@Injectable()
export class SampleService {

    public getSample(): string {
        return 'sample service!';
    }

    public getSampleById(id: string): string {
        return `id is ${id}`;
    }

    public registerSample(param: any): any {
        return {message: 'OK', result: param};
    }

    public updateSample(id: string, param: any): any {
        return {message: id, result: param};
    }

    public deleteSample(id: string): void {
        console.log(`${id} is deleted`);
    }
}

叩いてみる

$ curl localhost:3000/sample
> sample service!

$ curl localhost:3000/sample/sampleId
> id is sampleId

$ curl -XPOST -H "Content-Type:application/json" localhost:3000/sample -d '{"id": "sampleId", "title": "sample", "message": "sample message"}' | jq .
>{
>  "message": "OK",
>  "result": {
>  "id": "sampleId",
>  "title": "sample",
>  "message": "sample message"
> }
>}

$ curl -XPUT -H "Content-Type:application/json" localhost:3000/sample/sampleId -d '{"title": "sample", "message": "sample message"}' -v | jq .
>{
>  "message": "sampleId",
>  "result": {
>    "title": "sample",
>    "message": "sample message"
>  }
>}

$ curl -XDELETE localhost:3000/sample/sampleId -v
> ...
>< HTTP/1.1 204 No Content
>< X-Powered-By: Express
>< Date: Wed, 05 Jun 2019 14:49:40 GMT
>< Connection: keep-alive

HTMLを返せるようにする

HTML

まず返すHTMLファイルを作る。
ファイルは下記のような箇所に配置しておく。

.
├── README.md
├── nest-cli.json
├── nodemon-debug.json
├── nodemon.json
├── package-lock.json
├── package.json
├── src
├── test
├── tsconfig.build.json
├── tsconfig.json
├── tslint.json
└── views # ここを追加
    └── index.html # 追加したHTML

main.ts修正

自動生成されたmain.tsに追記する。
これも同じく公式サイトを参考に書いていく。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {NestExpressApplication} from '@nestjs/platform-express';
import {join} from 'path';

async function bootstrap() {
    // createの後ろに型を追加
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
    // ファイルが入っているフォルダを指定
  app.useStaticAssets(join(__dirname, '..', 'views'));
  await app.listen(3000);
}
bootstrap();

動かしてみる

npm startで起動した後、localhost:3000にアクセス。 下記の画像の用にHTMLが返ってくればOK。

f:id:minase_mira:20190607224607p:plain

Helmetを導入する

expressでとりあえず入れとけって風潮があるhelmetを導入してみる。
これも公式サイトにあるので参考にしてやる。

helmetインストール

$ npm install --save helmet

main.ts改修

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {NestExpressApplication} from '@nestjs/platform-express';
import {join} from 'path';
// import追加
import * as helmet from 'helmet';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.useStaticAssets(join(__dirname, '..', 'views'));
    // ここでhelmetを使うよう修正
  app.use(helmet());
  await app.listen(3000);
}
bootstrap();

まとめ

今回はnest.jsを使ってみた。
謳っているだけあって、全般的にAngularに似てる感じ。
Controllerの書き方とかはSpringに似てるのかなと思った。
expressよりも書きやすいしデフォルトでtsだし、個人的にはとても良さげ。
多分SSRとかもできるはずなのでいずれ調べたいところ。
これからガンガン使ってみようと思う。

参考資料

Angular8アップデート内容ざっくりまとめ

はじめに

今日、Angular8がリリースされたらしい。 Mediumも更新されていたので、さっくりまとめる。

原文

https://blog.angular.io/version-8-of-angular-smaller-bundles-cli-apis-and-alignment-with-the-ecosystem-af0261112a27

アップデート方法

直近のアップデートを入れている人は、ng update @angular/cli @angular/coreでOK。
(多分Angular6くらい?)
それ以外の人はupdate.angular.ioでアップデート方法を確認できる。

Differential Loading By Default

Differential Loadingというブラウザが自身の性能に合わせて、モダンかレガシーなJavaScriptを選ぶプロセスがある。
Angularのビルドはデフォルトでモダン向けのビルド(es2015)とレガシー向けのビルド(es5)を行ってくれるようになった。
ユーザがAngular8のアプリケーションを読み込んだ時、自動で必要なファイルを取得してくれる。

ng updateでAngular8にバージョンアップするとtsconfig.jsonをDifferential Loadingに対応するよう勝手にアップデートしてくれる。
CLItsconfig.jsontargetを見てDifferential Loadingが必要か判断してくれる。
targetes2015を設定するとモダンとレガシー、2つのバンドルを生成してくれる。
実行時にScriptタグを見て、正しい方を読み込んでくれる。

<script type="module" src="…"> // Modern JS
    
<script nomodule src="…"> // Legacy JS

angular.ioではこれでモダンブラザ向けに40KBほど初期バンドルサイズを削減できたらしい。7〜20%くらいの割合に当たるとのこと。
Differential Loadingについて詳しくしりたい場合はこちらから。

Route Configurations use Dynamic Imports

モジュールの遅延ロードをするとき、今まではAngular独自の書き方であったが、一般的なDynamic Importの形に変わった。 ちなみにこれでVSCodeとかのサポートを受けられるらしい。
これもng updateすると勝手にアップデートしてくれる。

// 今まで
{path: '/admin', loadChildren: './admin/admin.module#AdminModule'}

// これから
{path: `/admin`, loadChildren: () => import(`./admin/admin.module`).then(m => m.AdminModule)}

Builder APIs in the CLI

ng new, ng generate, ng add, ng updateのようなBuilderAPIが増えた。
こことかここを確認すると良さそう。
最新版のAngular Fireを使えばngコマンドでFirebaseにデプロイできる。

$ ng add @angular/fire
$ ng run my-app:deploy

Workspace APIs in the CLI

今まではworkspace configurationを変更するために、直接angular.jsonを変更する必要があった。
8からはAPIが用意されたらしい。詳しくはこちらから。
リンクも読んだんだけど、そもそもWorkspaceが何のことかわからなかったので、なんの恩恵があるかわからなかった。
悲しいから今度調べてみる。

Web Worker Support

WebWorkerをCLIから作れるようになった。
プロジェクトに追加するには、ng generate webWorker my-workerでOK。
一度追加してしまえば、CLIがよしなにバンドルしてくれるし分割してくれる。
詳しくからこちらから。

const worker = new Worker(`./my-worker.worker`, { type: `module` });

AngularJS Migration Improvements

AngularJSの$location serviceを使う場合、LocationUpgradeModuleを提供するようになったらしい。
これにより、AngularのLocationにシフトしてくれるらしい。
AngularJSを知らないのでなんとも言えないけど、AngularからAngularJSのアプリケーションをロードできるっぽい。
詳しくはこことかここを参照すると良い。

New Deprecation Guide

廃止予定の機能も廃止予定バージョンから2バージョン後までサポートしてくれるらしい。 Angular8でplatform-webworkerが非推奨になり8.1で廃止されるが、Angular9,10でも機能してくれるとのこと。 非推奨機能の一覧と削除予定こちらから。

Ivy & Bazel

今回入るって言われてた気がするけど、入らなかったらしい。
続報についてはブログ(原文のMedium)で出されるとのこと。
入らなかったのはとても残念。

勘違いで一応入っているっぽい?
オプトインとのこと。
続報がブログであることは変わらなそう。

まとめ

ざっくりアップデート内容を和訳してまとめてみた。
5,6,7では正直クリティカルなアップデートはなかった気もしてる。
ただ、今回はDifferential LoadingとかWebWokerとかBuilder APIとか入れといた方が良さそうなものが多い印象。
今後もアップデートは続くだろうし積極的に追ってきたいところ。
とりあえず手元のアプリをアップデートするところから始めます。
それでは今回はこの辺で。

@ngrx/effectsを使ってみる

はじめに

@ngrx/store についてまとめたから、今回は@ngrx/effectsについてまとめる。
インストールしてサクッと動かしてみるとこまで。

TL;DR.

ソースコード

@ngrx/effectsとは

APIアクセスなどの副作用のある処理をラップしてくれるもの。

前提条件

(なくてもいいけど、)前回の状態

インストール

$ npm install --save @ngrx/effects

Service作成

ここは普通のAngularと同じく、API叩くServiceを作成する。
ただ、今回は 作るのがめんどくさかったから サクッと作るためObservable生成して流すことにする。

    @Injectable({
        providedIn: 'root'
    })
    export class TodoService {
    
        constructor(private http: HttpClient) {
        }
    
        public fetchAll(): Observable<Array<string>> {
            return Observable.create(observable => {
                observable.next(['task1', 'task2', 'task3']);
                observable.complete();
                return observable;
            });
        }
    }

Action作成

サービスを取得するためのActionを作成する。
前回の状態があるなら、todo.action.tsに追記でOK。

    import { Action } from '@ngrx/store';
    
    export enum ActionTypes {
        Fetch = 'TODO_FETCH',
            Load = 'TODO_LOAD'
    }
    
    export class Fetch implements Action {
        readonly type = ActionTypes.Fetch;
    } 

Reducer作成

取得結果を処理する。
結果を受け取ってリストをまるっと入れ替えてあげる。
Actionと同じく todo.reducer.tsに記載。

    import { Action } from '@ngrx/store';
    import { ActionTypes } from '../action/todo.action';
    
    export const initialList: Array<string> = [];
    
    export function todoReducer(state = initialList, action: Action): Array<string> {
        switch (action.type) {
            case ActionTypes.Fetch:
                return [...action['payload']];
            default:
                return state;
        }
    }

Effects作成

serviceをラップして作ってあげる様に作ってあげる。
名前はtodo.effects.ts とする。

    import {Injectable} from '@angular/core';
    import {Actions, Effect, ofType} from '@ngrx/effects';
    import {EMPTY} from 'rxjs';
    import {catchError, map, mergeMap} from 'rxjs/operators';
    import {TodoService} from '../service/todo.service';
    import {ActionTypes, Fetch} from '../action/todo.action';
    
    @Injectable()
    export class TodoEffects {
    
        @Effect()
        loadTodo$ = this.actions$
            .pipe(
                // このイベントが発火するためのAction
                ofType(ActionTypes.Load),
                // サービス呼び出す
                mergeMap(() => this.todoService.fetchAll()
                    .pipe(
                        map(serverTodo => {
                   // serviceの結果を元にAction発火
                            return new Fetch({todos: serverTodo});
                        }),
                        catchError(() => EMPTY)
                    ))
            );
    
        constructor(
            private actions$: Actions,
            private todoService: TodoService
        ) {
        }
    }

moduleに追記

effectsを使うためにmoduleに追記する。

    @NgModule({
        declarations: [
            AppComponent
        ],
        imports: [
            // 配列形式でEffects定義。
            EffectsModule.forRoot([TodoEffects])
        ],
        providers: [TodoService],
        bootstrap: [AppComponent]
    })
    export class AppModule {
    }

Componentに定義

componentから呼び出すための設定。
今回は初期表示時に設定した内容がほしいので、OnInit → effectsのイベント発火 → 結果をsubscribe。
という流れで設定。

    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit {
      public todo$: Observable<Array<string>>;
      public todolist: Array<string> = [];
    
      constructor(
          private store: Store<{ todo: Array<string>}>,
          private service: TodoService
      ) {
        this.todo$ = store.pipe(select('todo'));
        this.todo$.subscribe(res => this.todolist = res);
      }
    
      ngOnInit(): void {
        // 初期表示用にイベント発火
        this.store.dispatch({ type: ActionTypes.Load });
      }
    }

動かしてみる

ng serve で起動。
初期表示がされてればOK!

まとめ

今回はサクッとeffectsを使ってみた。
ドキュメントのコードを丸コピしてたんだけど、
effectsからReducerに通るところでハマってた。
ちゃんと文章読むのは大事だねというところで、今回はこのへんで。

参考資料

JJUG CCC 2019 Springに参加してきました

はじめに

5月18日(土)に開催されたJJUG CCC 2019 Springに参加してきた。
今回は聞いてきたセッションについて簡単に内容と所感についてまとめてく。

参加してきたセッション

はじめてのgRPC

概要

JJUG概要
資料

感想

gRPCの概要、どの様に使うかという話。
gRPC自体聞いたことがなかったので、どんなものか知りたく参加してきた。
元々Googleの社内で使われていたものだとのこと。
セッション内でも言われてたけど、クライアント側で使われるのはまだ先そう(通信経路全部Http/2が必要らしい。)
便利そうだから今度触ってみたい。

パッケージ管理してなかった既存システムに後付けでGradleを導入した

概要

JJUG概要
資料

感想

闇に手を加えられないというのはよく聞く話だと思うけど、今回はその闇に手を入れた話。
スコープを決めてやることやらないこと決めようというとのこと。
良く良く考えたら当たり前のことだと思うけど、なかなか実践できないんだろうなと思った。
スコープとかやるやらとかはこういう話に限らないと思うので、意識していきたいところ。

先行開発!Javaでクリーンアーキテクチャ -- ゼロから始める新規開発

概要

JJUG概要
資料

感想

クリーンアーキテクチャの概要解説。
タイトルはJavaでとあるけど、内容はJavaに限った話じゃなかった。
クリーンアーキテクチャって名前は聞くけど、
いまいちわかってなかったから説明を聞けてよかった。

テストエンジニアが教えるJunitを書き始める前に考えるべきテスト

概要

JJUG概要
資料

感想

テストの考え方についての話。
テストコード書く前に考えることあるよね、曖昧な表現してると認識に差が出るよねって感じ。
テストの内容を議論するのはテスト前にできるし、やっとくと後続での工数が削減できるだろうというのは納得できた。
今後意識していけたらと思うところ。

1400万ユーザーのWebサービスを15年運用して考える、Javaである理由

概要

JJUG概要
資料

感想

自社サービスの開発にあたりJavaを使っている理由の説明。
昔は選択肢が少なかったからJava、その後も社内ノウハウがあったりユーザが多いからJavaとのこと。
個人的にはもっとテクニカルな理由でJavaを選んでいる理由を知りたかった。
最近だとKotlinの方が人気っぽいし、
あえてJavaを使う理由があるのかと思ってただけにちょっと残念。

ストラングラーパターンによるマイクロサービスマイグレーションの勘所

概要

JJUG概要
資料

感想

レガシーな環境を新しい環境にリプレイスした話。
技術的な話が多く、似たようなことをするのであればとても参考になると思った。
ただKubernetesを触ったこと無いし、よくある話なのかもしれないけどちょっと追いつけなかった。

マイクロサービス:4つの分割アプローチの比較

概要

JJUG概要
資料

感想

マイクロサービスを作るに辺り、システムをどの様に分割していくかという考え方の話。
ただし、考え方は実践ベースの話だけじゃなくこうした方が良いんじゃないかっていう考察込。 タイトルはマイクロサービスってなってるけど、分割基準はモジュール分けるときとかにも使えると思った。
やっぱりEricEvansの考え方は難しいというオチ。

まとめ

はじめてこういうイベントに参加してきた。
色々な話を聞けたのは楽しかったしタメになったけど、
45分っていうのは話してる人からすると全体的に短かった印象。
ただ、気楽に聞けるのはこのくらいの時間かなって思った。
今後も積極的にイベントに参加していきたい所存。

P.S.

同日に開催されていたInside Frontendの方がちょっと興味あった。
(イベント自体は知ってたけど、開催日時をよく見てなかった。。。)

Kotlin + Spring bootでRSSリーダーを作ってみる

はじめに

諸事情によりRSSリーダーを作ろうと思った今日このごろ。
具体的に言うとSlackのRSSリーダーを使って色々なものを読んでいたんだけど、
通知が即時でうるさい + ミュートにしたらしたでスマホからだと未読がどこからかわからない。
という問題というか気になることがあったので自作してみる。
作成方針としてはAPIとして立てつつ、どっかのFaaSで定期実行でも良いようにする。

技術選定理由

技術の選定理由としては、最近Java触ってオワコン言われている(と個人的に感じてる)割に書きやすいやん!と思ったのでJavaでやる。
予定だったけど、良く良く考えたらイケてるのJavaじゃなくてlombokだったのでおとなしくKotlinを使う。
Javaも8以降StreamAPIがあったり、10からは型推論もできて HogeClass hoge = new HogeClass() みたいなの書かなくて良いし( var hoge = new HogeClass()って書ける)だいぶ書きやすいと思う。
でも便利系はやっぱりlombokが必要でアノテーション祭りになるのがなと思ってパス。
KotlinにしたのはJava使いたい + 記法がtsに似てて多分そこまで苦労しないだろう読み。

TL;DR

ソースコード

前提条件

KotlinとSpringbootが動くこと。
IntelliJ入れておけば特に困ること無いと思う。(たしか。)

RSSリーダーを作っていく

今回レイヤーアーキテクチャ風に作っていくことにする。
理由としては最近DDDの本を読んだので、試したかったから。
さっそくプロジェクトを作るところから始める。

プロジェクトセッティング

今回はSpringを使うので安定のSpring Initializrスタート。
Projectを Gradle 、Languageを Kotlin に設定。
Dependenciesに Web を追加でダウンロード。

便利そうなライブラリのインポート

大抵のやろうと思っていることは既に先人がやっているはずなのでまずはググってみる。
どうやらJavaRssリーダーを作るには Romeというやつが良いらしい。
build.gradle に下記を追記する。

dependences {
    implementation 'com.rometools:rome:1.12.0'
}

Interface層

APIとして立てるためにサクッとコントローラーを作る。
特に変なことはしてない想定。Service呼び出して取得結果を返す。

@RestController("/v1/rss")
class RssReaderController(private val rssService: RssService) {

    /**
     * Rssフィード取得
     */
    @GetMapping
    fun readRss(): List<RssModel> {
        return this.rssService.fetchRss()
    }
}

Application層

ドメイン層のClassを使うようにしている。
ここらへんアーキテクチャの理解が甘くて間違ってるかもしれないけど、
方針としてはビジネスロジックは含まない + 各ビジネスロジックを使うだけ。
一応取得するURLは apprication.yaml から読むようにした。

@Component
class RssServiceImpl(private val rssConverter: RssConverter, private val rssUseCase: RssUseCase) : RssService {
    // 自分のはてなブログURL
    @Value("\${properties.url.hatena}")
    lateinit var url: String

    override fun fetchRss(): List<RssModel> {
        val syndFeed = rssUseCase.fetchRss(url)
        return this.rssConverter.toRssModel(syndFeed)
    }
}

Domain層

UseCase

ネーミングセンスがないので、 名前が思いつかなかったので、UseCaseにした。
(幅広い名前にしちゃうと後々便利屋クラスになりそうだけど、今回は自分しか触らないのでOKとする。) やることはInfrastructure層を呼び出して使うだけ。Rssフィードを取得するメソッドを作成。

@Service
class RssUseCaseImpl(private var rssReader: RssReader): RssUseCase {

    override fun fetchRss(url: String): SyndFeed {
        return rssReader.fetchRss(url)
    }
} 

Converter

取得したRssフィードから必要なデータを取り出してデータクラスに格納する。
最大取得件数を最初はかいてたんだけどマジックナンバーイケてないし、
ここにハードコードもどうなん?ということで apprication.yaml に切り出し。
取得するデータは、記事タイトル/記事内容(一部)/記事URLの3つ。

@Service
class RssConverterImpl() : RssConverter {
    @Value("\${properties.count}")
    val count: Int = 0

    override fun toRssModel(syndFeed: SyndFeed): List<RssModel> {
        val rssList: MutableList<RssModel> = mutableListOf()
        val getMax = count - 1
        // 最新5件のみ取得
        for (i in 0..getMax) {
            val entry = syndFeed.entries[i]
            rssList.add(RssModel(entry.title, entry.description.value, entry.link))
        }

        return rssList
    }
}

Infrastructure層

実際にRSSフィードを取得するクラス。
Romeの使い方はGithubのExample丸コピなので省略

@Service
class RssReaderImpl: RssReader {

    override fun fetchRss(url: String): SyndFeed {
        return SyndFeedInput().build(XmlReader(URL(url)))
    }
}

使ってみる

ここまでで動くようになったはずなので実際に使ってみる。
試しに自分のはてなブログのデータを取ってみる。

# Springの起動
$ gradlew bootRun
# url叩くだけ(jqはお好みで)
$ curl localhost:8080/v1/rss | jq 
> [
>  {
>    "title": "Docker上でSpring bootのビルド&実行するまで",
>    "description": "はじめに JavaというかSpringを触っている今日このごろ。 ついでにDockerで動かそうと思ったのでその設定をしていく。 前提 Dockerが使えること。 設定方法については割愛。 環境 OS: macOS Mojave Docker: Docker for Mac v2.0たのは下記の通り。書いてないとこは何でも良いと思う。 項目 内容 Project Gradle project Language Java Spring boot 2.2.0(S…",
>    "url": "https://minase-program.hatenablog.com/entry/2019/03/06/233949"
>  },
>  {
>    "title": "React+Redux+TypeScript事始め",
>    "description": "はじめに 普段はAngularを使っているので、他のフレームワークを触ってみようと思った今日このごろ。 Vueは超絶軽く触ったことがあるので今回はReact。 JSXがパッと見とっつきにくかったので後回しにしてたけど、ちゃんと向き合pm install -g create-react-app # TSを使いたいからオプション指定 $ create-react-app react-sample --types…",
>    "url": "https://minase-program.hatenablog.com/entry/2019/02/13/225531"
>  },
>  ...以降続き省略

まとめ

RSSリーダーって思ったより簡単に作れる + Kotlinが結構快適。
とりあえず取得するとこまでは作ったので、次はどこかに上げることを検討することにする。
それでは今回はこの辺で。