西東京市の16〜64歳のワクチン接種予約は7/15の西東京市新聞にて周知予定
表記の通り。西東京市新聞の最新号に記載されている。面倒だったのでキャプチャは置かないが各自確認されたし。
接種券は到着しているが予約できなくて混乱した人に向けて・・・。
SIerエンジニアが初めてのWebサイト作成〜自治会サイトのマイグレーション〜
記事の内容
SIerエンジニア(34)が初めてのWebサイト作成を行った体験録。
読んで得することはない。
経緯
社長から社長が個人で保守している自治会サイト(≠自治体サイト)のマイグレーションをしたいと飲み会で発言。
サイト自体のマイグレーションは多いに賛成するところであり、私のスキルの棚卸しとしてもチャレンジしやすい案件と思い受けた。
私のスキルセット
私の環境
- 土日に軽くコーディングできるくらいの時間は取れる
- 平日夜はコーディングほぼ不可能
- 2020/4はニート。
システム概念図
実際の構成
フロント構成
フロントは、自治会員用の公開サイトと、運営者用の管理者サイトを作成した。
言語
TypeScript
パッケージマネージャ
webpack
フレームワーク
React および React Hooks
ライブラリ
ライブラリ | 目的 |
---|---|
dotenv | 環境変数制御用 |
react-router | ルーティング用 |
react-table | テーブル表示用 |
material-ui | cssフレームワーク |
material-io | cssフレームワーク |
date-picker | 日付選択UIライブラリ |
marked | マークダウン to HTML |
外部サービス
firebase authentication
フロントのログイン時に利用。
エディタ
外部サービス
firebase authentication
ログイン後、フロントからサーバサイド処理を呼び出された際の認証トークン検証。
DB
情報共有
Stock
メモ類の管理に使った。
正直、機能面が弱いのとエディタの書き心地が悪かった。
マークダウン対応していないのも微妙。
後発にしては強みがどこなのかわからなかった。
Slack
チャット。個人開発ゆえ、あまり使わなかった。
使ったことがなかったので使ってみた感。
GitHub
コード共有。
開発の流れ
超ざっくり要件定義
社長と会話して要件定義を図る。
当初スコープは、現行踏襲+便利機能もろもろ追加(pwaとか備品管理したいとか夢がいっぱい語られた)。
正直、細かい点をどうしたいという希望が出てこなかったので若干の不安を抱いた。
この時点では、自分自身、開発を甘く見ていたのでいろいろ夢を語った。
プロジェクト数
- フロント(外部公開サイト)
- 管理サイト
- サーバサイド
- 移行用
現行サイトの記事の移行プロジェクト。
記事の取り込み、マークダウンへの変換を実施。
難所
ミドルウェア全般
メールサーバ構築 困った度:★★★★★
安請け合いしたメールサーバが地獄だった。
どこかで躓くと死ねる。
反省点は、/var/log/maillogや/var/log/messagesを見てから対応するのが基本である。
コストを考えると、AWSで調達したほうがよかった気がする。
構成は
Webクライアント:Rainloop
メールソフト:postfix, dovecot
hugoインストール 困った度:★★★★★
go製のブログフレームワークであるhugoをインストール。
makeするなどの手順があり、あまり経験がなく難航した。
実装面
raect-table 困った度:★★★★★
Typescriptでコーディングしていたが、react-tableをTypescriptで使うには難易度が高かった。
useSortby, usePaginationなどのpluginを使う場合、型定義ファイルを差し替える必要がある。
この情報を理解するのに手間取った。
正直型定義ファイルを再作成するのは苦痛だった。
CORS 困った度:★★☆☆☆
クライアントからSpring Bootへのリクエストが延々と拒否される。
根本原因と表出したエラーがうまく噛み合っていなくて混乱した。
Webでよく見る事例を行っていれば普通は出来る。
環境別ビルド 困った度:★☆☆☆☆
webpackやSpringBootにおける環境別ビルドの制御。
レスポンシブ 困った度:★★★☆☆
cssって一つの言語レベル。。。
Spring Data JPA 困った度:★★★★★
@OneToOneの際に親側に外部キーを持たせないといけない点を知らず詰まった。
全体的に
いろいろな問題が頻発したが、複合的な問題となっているケースもあり、
過去の障害切り分けの経験が生きた。
楽しかったところ
全体像の設計
Webサイト全体のアーキテクチャから考える経験は無かったので、かなり楽しかった。
どうやって見せるか、どうやって記事を編集するか、など。
react hooks
useContextによるエラー制御やログインユーザの保持。
useStateによる状態管理。
hooksという仕組みの制限が見通しの良さをある程度要求してくれた。
簡易ブログ記述画面
プレビュー付きマークダウンエディタを用意できた。
気持ちいい。
テキストエリアにファイルをドラッグアンドドロップすると、ファイルのアップロードが行われ、
マークダウン用の参照パスがエディタ上に挿入される。
反省点
ポエム
フロントエンド実装を伴う開発を初めて一気通貫でやってみたが、
使えるライブラリがとても多くあったし、現在進行形で日々よく出来たライブラリに出会っている。
技術のコモディティ化を身を持って体感した。
より、何を作るのか?が問われる時代が来ている。
まとめ
システム全体のライフサイクル含めて完成したときの気持ちよさはすごい。
Javaの反変・共変
反変・共変
共変とは
型の順序関係を維持する (≤ で順序づけたとき、特殊から一般の順になる) とき、共変である (covariant) という。 wiki
反変とは
型の順序関係を反転させる (≤ で順序づけたとき、一般から特殊の順になる) とき、反変である (contravariant) という。 wiki
不変とは
上記いずれにも該当しないとき、不変である (invariant) という。 wiki
Javaによる例示
の前に、Javaはジェネリクス型以外は共変である。
が、ジェネリクスにおいては不変である。
共変(特殊→一般という変換例)
通常の型
// 通常の型での共変 Object object = new Object(); String mojiretsu = "あああ"; object = mojiretsu; // 共変により代入可能
配列
// 配列 String[] 特殊 = new String[]{""}; Object[] 一般 = new Object[]{1}; 一般 = 特殊; // 共変により代入可能
ジェネリクス
List<Object> 一般 = new ArrayList<>(); List<String> 特殊 = new ArrayList<>(); 一般 = 特殊; // コンパイルエラー(ジェネリクスは不変のため)
反変(一般→特殊)
通常の型
Object object = new Object(); String mojiretsu = "あああ"; mojiretsu = object; // コンパイルエラー(反変のため)
配列
String[] 特殊 = new String[]{""}; Object[] 一般 = new Object[]{1}; 特殊 = 一般;// コンパイルエラー(反変のため)
ジェネリクス
List<Object> 一般 = new ArrayList<>(); List<String> 特殊 = new ArrayList<>(); 特殊 = 一般; // コンパイルエラー(ジェネリクスは不変のため)
境界型パラメータ
前提として、
Animalクラス
これを継承したDogクラスやCatクラスがあるものとする。
上限境界ワイルドカード
上限境界は、ジェネリクスに共変をもたらすために用いる。
共変つまり、ジェネリクスのリスト(List<? extends Animal>
に共変性(特殊→一般)をもたらす。
List<? extends Animal> animalList = new ArrayList<>(); List<Dog> dogList = new ArrayList<>(); animalList = dogList;
animalListに共変性がもたらされているため、変数としてdogListの代入(特殊→一般)が可能になっている。
下限境界ワイルドカード
下限境界は、ジェネリクスに反変をもたらすために用いる。
List<? super Dog> dogList = new ArrayList<>(); List<Animal> animalList = new ArrayList<>(); dogList = animalList;
dogListに反変性がもたらされているため、変数としてdogListにanimalListの代入(一般→特殊)が可能になっている。
上限境界ワイルドカードの使い所
メソッド引数型
特化した要素を持つListや汎化した要素を持つList受け取る。 そして、汎化したクラスで展開できる。
private void kyouhenAgrs(List<? extends Animal> animalList){ animalList.forEach(e->{ // Animal型として展開。 }); }
これはこういった形でAnimalやその特化した要素を持つListで呼び出せる。
kyouhenAgrs(new ArrayList<Dog>()); kyouhenAgrs(new ArrayList<Cat>()); kyouhenAgrs(new ArrayList<Animal>());
メソッド返却型
特化したListを同じ表現で返却できる。
private List<? extends Animal> kyohenReturnDog(){ return new ArrayList<Dog>(); } private List<? extends Animal> kyohenReturnCat(){ return new ArrayList<Cat>(); }
List<Animal> animalList = new ArrayList<>(); List<? extends Animal> dogList = kyohenReturnDog(); List<? extends Animal> catList = kyohenReturnCat(); // 統一的に扱えるため、集約ができる。 animalList.addAll(dogList); animalList.addAll(catList);
できないこと
パラメータ化された型を引数に取るメソッド呼び出しは不可能。
? extends Animal
に相当する型を引数に取るものは呼び出せない。
List<? extends Animal> animalList = new ArrayList<>(); // すべてパラメータ化された型を引数に取る animalList.add(new Animal()); // コンパイルエラー animalList.add(new Dog()); // コンパイルエラー animalList.add(new Cat()); // コンパイルエラー animalList.add(new Object()); // Objectですらコンパイルエラー
List#addは
boolean add(E e);
であり、
Listは
public interface List<E> extends Collection<E> { }
であるため、addで受け取るEとは、パラメータ化された型である。
下限境界ワイルドカードの使い所
Animal ->Dogの継承関係では、
List<? super Dog>
は、Dogを下限とするListである。
継承で見るとこんな感じでDogより上の関係をすべて内包可能とする。
つまり、Listには、Dog, Animalそして、すべての祖であるObjectが入りうるが、確定できるのはObjectであることだけである。
ということで、取り出し時は、Object型でしか取り出せないということである。
メソッド引数型
private void callHanpenArgsDog(){ // List<Dog>, List<Animal>, List<Object>というDogの親以上を // 型パラメータに持つListを引数にできる。 hanpenArgsDog(new ArrayList<Dog>()); hanpenArgsDog(new ArrayList<Animal>()); hanpenArgsDog(new ArrayList<Object>()); } private void hanpenArgsDog(List<? super Dog> dogList) { dogList.forEach(e -> { // 取りだしはObjectになる。 }); }
メソッド返却型
上で述べたように<? super T>
は、Objectであることしか確定できないため、
取り出し時はすべてObjectになる。
返却型として指定するメリットは無いように思われる。
※後述する「上限付きクラスと下限付き変数型の組み合わせ」の項ではObject以外に確定することができるので、そちらを活用すれば返却型に定義する意味合いが出てくる。
JDK内の利用事例
java.util.Collections#addAll
public static <T> boolean addAll(Collection<? super T> c, T... elements) { boolean result = false; for (T element : elements) result |= c.add(element); return result; }
可変長の実装Tを、T
を汎化して保持するコレクションへ追加する。
非常にわかりやすい。T
は、T
およびTの親
であればコレクションに格納できるだろう。
Animal, Dogの例でいうと、こんな感じで呼び出せる
Collections.addAll(new ArrayList<Animal>(),new Dog()); Collections.addAll(new ArrayList<Animal>(),new Cat()); Collections.addAll(new ArrayList<Animal>(),new Animal());
java.util.Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) { int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } } }
国内で探した限りは、非常によく見るPECSの例。
提供する主体をProducerと呼び、消費する主体をCustomerと呼ぶ。
Producerはextends、Customerはsuperとして、PECS。
srcがProducerでTを上限とするListとなり要素を提供する。
destがCustomerでTを下限とするListとなり、要素を消費する。
Collections.copy(new ArrayList<Animal>(), new ArrayList<Dog>()); Collections.copy(new ArrayList<Animal>(), new ArrayList<Animal>());
java.util.List#sort
default void sort(Comparator<? super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); } }
これもかなりわかりやすい。
List<Animal> animalList = new ArrayList<>(); Comparator<Animal> animalSorter = Comparator.comparing(Animal::no); animalList.sort(animalSorter); List<Dog> dogList = new ArrayList<>(); dogList.sort(animalSorter);
List<Animal>
でもList<Dog>
でも、Comparator<Animal>
でsort
できるようになっているべきだろう。
sort
のシグネチャがComparator<E>
だと、List<Dog>
のsort
はanimalSorter
ではできない。
上限付きクラスと下限付き変数型の組み合わせ
最後に載せているリンク集にある記事内で言及されているものを確認したい。
まず、以下2つのクラス群を定義してみる。
Water
->DrinkingWater
->Perie
は、そのまま水、飲み水、ペリエと具体化していくクラス。
WaterCup
、DrinkingWaterCup
、PerieCup
は、それぞれ水を入れられるカップ、飲み水を入れられるカップ、ペリエを入れられるカップとなる。それぞれ型パラメータで上限境界を設定している。
WaterCup
には、Water
以下が存在することができ、Water
として取得可能。
extends
パラメータを与えることで、Producerとしての立ち位置としている。
だがextends
のみだと上限境界の例のとおり、何も設定することはできないクラスとなる。
ここからさらに、下限境界指定した変数宣言を取り入れる。
WaterCup<? super Water>
には何でも注げる。
WaterCup<? super Water> waterCupLowerWater = new WaterCup<>(); // TをWaterもしくはWaterの親以上にした。Waterなので多態により、Water以下を受け入れる。 waterCupLowerWater.setWater(new Water()); waterCupLowerWater.setWater(new DrinkingWater()); waterCupLowerWater.setWater(new Perie()); // TはWaterもしくはWaterの親以上なので、Waterは確定できる。 waterCupLowerWater.getWater().waterMethod();
下限境界をDrinkingWater
に変えてみる。
WaterCup<? super DrinkingWater>
には、DrinkingWater
およびPerie
が注げる。
WaterCup<? super DrinkingWater> waterCupLowerDrinkingWater = new WaterCup<>(); // 下限をDrinkingWaterで与えられているため、値の設定が可能。 // 値自体は、T、もしくはTの子以下を設定可能。つまり、DrikingWater、その子のPerieは設定可能。 // waterCupLowerDrinkingWater.setWater(new Water()); // compile error waterCupLowerDrinkingWater.setWater(new DrinkingWater()); waterCupLowerDrinkingWater.setWater(new Perie()); // extends はWaterCupに指定されたwaterとなるため、waterであることが確定できる。 waterCupLowerDrinkingWater.getWater().waterMethod();
DrinkingWaterCup<? super Perie>
の例
DrinkingWaterCup<? super Perie> drinkingWaterCup = new DrinkingWaterCup<>(); drinkingWaterCup.setWater(new Water()); // コンパイルエラー drinkingWaterCup.setWater(new DrinkingWater()); // コンパイルエラー drinkingWaterCup.setWater(new Perie()); drinkingWaterCup.getWater().drinkingwaterMethod();
PerieCup<? super Perie>
の例
PerieCup<? super Perie> perieCup = new PerieCup<>(); perieCup.setWater(new Water()); // コンパイルエラー perieCup.setWater(new DrinkingWater()); // コンパイルエラー perieCup.setWater(new Perie()); perieCup.getWater().perieMethod();
こういった組み合わせをまとめるとこうなる。
Xはコンパイルエラー。
extends
の指定がProducerの定義となり、get
の返却型(上限境界)を決める。
変数に定義したsuper
の指定がConsumerの定義となり、set
の受け入れ可能な型(下限境界)を決める。
リンク集
Windows環境における高精度時刻取得について
結論
Windows8未満
GetSystemTimeAsFileTime
Windows8以降
以下を比較検討のうえ、採用。
GetSystemTimePreciseAsFileTime
GetSystemTimeAsFileTime
上記に関するMicrosoftのまとめ
docs.microsoft.com
以下は検証の記録。
事前確認
事前知識
分解能
表現粒度を指す
例:12:11:59.999→分解能1ミリ、12:11:59→分解能1秒
精度
判別可能な最小の断面
例:0.111,0.112,0.112 ... →精度1ミリ、0.110, 0.125,0.140... 精度15ミリ
検証環境
OS:Windows10 Pro
分解能の確認
分解能の確認コード
// SystimeSample.cpp : コンソール アプリケーションのエントリ ポイントを定義します。 // #include "stdafx.h" #include <Windows.h> #include <stdio.h> #include <mmsystem.h> #pragma comment(lib, "winmm.lib") int main() { DWORD dwTimeAdjustment; DWORD dwTimeIncrement; BOOL bTimeAdjustmentDisabled; GetSystemTimeAdjustment(&dwTimeAdjustment, &dwTimeIncrement, &bTimeAdjustmentDisabled); printf("分解能(クロック割り込み間隔):%f ms\n", (double)dwTimeIncrement / 10000.0); printf("調整機能:%s\n", (bTimeAdjustmentDisabled ? "True" : "False")); printf("加算値(単位:ナノ秒):%f nano \n", GetSystemTimeAdjustment); TIMECAPS ptc; timeGetDevCaps(&ptc, sizeof(ptc)); printf("マルチメディアタイマー分解能:%d ms\n", ptc.wPeriodMin); LARGE_INTEGER lpFrequency; QueryPerformanceFrequency(&lpFrequency); printf("高分解能カウンター分解能:%f ms\n", 1000.0 / lpFrequency.QuadPart); return 0; }
結果
分解能(クロック割り込み間隔):15.625000 ms
調整機能:True
加算値(単位:ナノ秒):0.000000 nano
マルチメディアタイマー分解能:1 ms
高分解能カウンター分解能:0.000428 ms
上記はあくまでRTC(リアルタイムクロック・・・ハードウェア クロック)についての情報ぽい。
検証方法
APIを10万回連続で実行し、ミリ秒部分の更新が起きた際に、前回との差を記録する。
Windows previous versions documentation | Microsoft Docsによる計測結果
差分(ms) | 出現回数 |
---|---|
1 | 99995 |
2 | 1 |
12 | 1 |
15 | 1 |
32 | 2 |
Windows previous versions documentation | Microsoft Docsによる計測結果
差分(ms) | 出現回数 |
---|---|
1 | 99993 |
2 | 2 |
18 | 1 |
22 | 1 |
29 | 1 |
30 | 1 |
31 | 1 |
Windows previous versions documentation | Microsoft Docsによる計測結果
差分(ms) | 出現回数 |
---|---|
1 | 99997 |
2 | 2 |
32 | 1 |
GetSystemTimePreciseAsFileTime関数による計測結果
さらに調べると・・・Win8以降、高精度のシステム時刻取得API「GetSystemTimePreciseAsFileTime」が追加されている。
差分(ms) | 出現回数 |
---|---|
1 | 99995 |
4 | 1 |
3 | 1 |
6 | 1 |
2 | 1 |
31 | 1 |
環境によっては、GetSystemTimePreciseAsFileTimeが遅い場合もあるらしい。
GetSystemTimePreciseAsFileTimeが1ミリ秒の精度を持つのはドキュメントを読むと納得出来るが、
他の関数も1ミリ秒の精度が出ているのはなぜだろうか。
10万回の検証コード
// SystimeSample.cpp : コンソール アプリケーションのエントリ ポイントを定義します。 // #include "stdafx.h" #include <Windows.h> #include <stdio.h> #include "stdafx.h" #include <Windows.h> #include <stdio.h> #include <unordered_map> #include <iostream> const int N = 100001; const int ignoreN = 0; int main() { int secondAndMillseconds[N][2]; std::fill(secondAndMillseconds[0], secondAndMillseconds[N], 0); SYSTEMTIME tm, oldTime; FILETIME ft; GetSystemTimePreciseAsFileTime(&ft); //GetSystemTimeAsFileTime(&ft); //GetSystemTime(&tm); //GetLocalTime(&tm); FileTimeToSystemTime(&ft, &tm); oldTime = tm; for (int i = 0; i < N;) { GetSystemTimePreciseAsFileTime(&ft); //GetSystemTimeAsFileTime(&ft); FileTimeToSystemTime(&ft, &tm); //GetSystemTime(&tm); //GetLocalTime(&tm); if (tm.wMilliseconds != oldTime.wMilliseconds) { // printf("%d:%d:%d.%d\n", tm.wHour, tm.wMinute, tm.wSecond, tm.wMilliseconds); secondAndMillseconds[i][0] = tm.wSecond; secondAndMillseconds[i][1] = tm.wMilliseconds; oldTime = tm; i++; } } int oldSecond = 0; int oldMillSecond = 0; int second = 0; int millSecond = 0; int diffMillSecond = 0; // 差分別の出現回数map std::unordered_map<int, int> map; int startIndex = ignoreN; oldSecond = secondAndMillseconds[startIndex][0]; oldMillSecond = secondAndMillseconds[startIndex][1]; for (int i = startIndex + 1; i < N; i++) { // 0秒と59秒を比べる状況の考慮 if (secondAndMillseconds[i][0] == 0 && oldSecond == 59) { second = 60; } else { second = secondAndMillseconds[i][0]; } millSecond = secondAndMillseconds[i][1]; diffMillSecond = (second * 1000 + millSecond) - (oldSecond * 1000 + oldMillSecond); if (map[diffMillSecond] == 0) { map[diffMillSecond] = 1; } else { map[diffMillSecond] = map[diffMillSecond] + 1; } oldSecond = secondAndMillseconds[i][0]; oldMillSecond = secondAndMillseconds[i][1]; } for (auto itr = map.begin(); itr != map.end(); ++itr) { std::cout << itr->first // キーを表示 << "," << itr->second << "\n"; // 値を表示 } return 0; }