git config --unset-all credential.helper git config --global --unset-all credential.helper git config --system --unset-all credential.helper
TypeScriptの型検査を無効化して型安全性を失わせる落とし穴5つとその避け方
TypeScriptにはanyを使わないよう注意しても型検査が無効化され型安全性が失われるパターンがいくつかあり、中には致命的なものも存在する。今回はこのパターンをいくつか紹介する。
コールバックのvoid戻り値型
コールバックのvoid戻り値型はすべての型を受け入れるためvoid型 === any型となる。
function f(cb: (a: number) => void): void { return cb(0); } function g(a: number): number { return a; } f(g);
このためvoid型の値はvoid演算子を使用して即座に強制的にvoid型に変換しなければならない。
void f(g);
ここでは手遅れ。
function f(cb: (a: number) => void): void { return void cb(0); }
ここで変換する。
コールバックのパラメータ数
コールバックのパラメータ数の不足は型エラーにならず検出できない。
type F = (cb: (a: number, b: string) => number) => number; type G = (cb: (a: number) => number) => number; var f: F = <G>((cb: (a: number) => number) => cb(0)); f((a, b) => a + b.length);
これを回避する方法はない。TypeScript 3.3までに修正済み。
共変・反変
interface A { x: string; } interface B { x: string; y: string; } const fb = (fa: (a: A) => void) => fa({x: 'x'}); const fa= (b: B) => alert(b.y); fb(fa);
まあ、これは仕方ないだろうか。
Generic型
TypeScriptの型検査は基本的に構造型の照合であり、型パラメータは照合の対象となる構造に含まれていないため、構造に反映されない型パラメータの型は無視される。
class C<T extends Foo | Bar> { constructor(d) { d.cmd(); } } class Foo { f; cmd() { return this; } } class Bar { b; cmd() { return this; } } var c: C<Foo> = new C<Bar>(new Bar());
このため型パラメータのみで完結する純粋な幽霊型により型検査を行わせようとしても失敗する。
class C<T> { } var c: C<number> = new C<string>();
これを回避するには型パラメータで受け取った型をプロパティやメソッドで持って構造型に反映させなければならない。
class C<T> { c: T; } var c: C<number> = new C<string>();
このほか、private/protectedアクセス修飾子を使用するとクラスが公称型となり型が一意に識別される(継承クラスの型を除く)。
Generic型定義
TypeScriptがデフォルトで定義しているいくつかの型ではこの性質のため致命的な不整合を見落とす可能性がある。
var s: PromiseLike<number | string> = <PromiseLike<string>>null; var n: PromiseLike<number> = <PromiseLike<number | string>>s;
どれだけ厳格に型を守っても、上記のように数値のみを返す型が文字列を返すようになる不正な代入が可能である。 型によっては不整合を検出するので不正に代入可能な型を即座に見分けることは難しい。
var s: ArrayLike<number | string> = <ArrayLike<string>>null; var n: ArrayLike<number> = <ArrayLike<number | string>>s;
この不整合を防ぐにはインターフェイスを拡張して先ほどと同様型パラメータを構造に落とせばよい。
interface PromiseLike<T> { _?: T; }
import/export文を含めると新規の型定義となるため含ないこと。 拡張を行っても外部からは拡張を含まない.d.tsファイルだけを参照させれば拡張を隠蔽できるので外部の型定義を汚染したり競合したりすることはない。
TypeScriptではこの種の不整合によるバグを防ぐため上記のような宣言をおまじないのように毎回追加しておく必要があるだろう。TypeScript 3.3までに修正済み。
まとめ
型があってるのに変な値が入るバグに苦しみたくなかったらここに書いたパターンと対処法を開発メンバーのTypeScript使用者全員に周知することをお勧めする。
TypeScript1.8以降で追加予定または協議中の注目の新機能12つ
TypeScript1.8以降で追加予定または協議中の注目の新機能を紹介します。
この記事はTypeScript アドベントカレンダー2015 25日目の記事です。
戻り値型の厳格な検査(--noImplicitReturns
) [Merged]
戻り値が定義した型以外になることを禁止します。 必ずオンにしましょう。しない理由がありません。 この調子でFlowtypeと型安全性をガンガン競ってほしいところです。
// The followings are all wrong in a typed context yet the compiler ignores them as a valid code: function SomeMethod(): number { return; } function SomeMethod(): number { return null; } function SomeMethod(): number { if (false) return 1; } function SomeMethod(): number { try { return 0; } catch (ex) { } }
Nullable型の禁止(--noImplicitNull
) [Suggestion]
nullやvoidがanyやvoid以外の型に混入しないよう暗黙的なキャストを禁止します。
変数ならconst
を使う方法もあるのですがオブジェクトやパラメーターはそうはいきません。
--noImplicitNull
オプションでの制御を支持しています。
バグのない世界のために今すぐ+1!
let n: number = null; // error let m: number = undefined; // error
タイプガードへのコントロールフロー解析の適用 [Discussion]
タイプガードはこれまで条件分岐先のブロックか式にしか適用されませんでしたがタイプガードの結果が以降のすべてのコードに適用されるようになるのでガード節を型解決に使えるようになります。 まだ協議中なのでぜひ+1してきてください。
function foo(x: number | string) { if (typeof x === 'string') { return; } // x should now be a number if (2 % x !== 0) { // do something } }
String Literal Types [Merged]
変数や関数に与えられる文字列の値を制限できるようになります。 ありがてぇ。
type CardinalDirection = "North" | "East" | "South" | "West"; function move(distance: number, direction: CardinalDirection) { // ... } let a: "foo" | number = "foo"; // valid let b: "bar" | "baz" = "foo"; // invalid
Readonly(final)アクセス修飾子 [Scheduled]
プロパティの再代入を型として禁止し、不変オブジェクトの定義を可能にします。
待ち望まれた機能です。
個人的にはfinal
のほうが好きです。
interface Point { x: number; y: number; } interface ImmutablePoint { readonly x: number; readonly y: number; } var pt: ImmutablePoint = { x: 4, y: 5 }; // OK, can convert mutable to non-mutable pt.x = 5; // Error, 'pt.x' is not a valid target of assignment var pt2: Point = pt; // Error, cannot convert readonly 'x' to mutable 'x' // Possibly bad behavior var pt3: Point = { x: 1, y: 1 }; var pt4: ImmutablePoint = pt3; // OK pt3.x = 5; // pt4.x is also changed? // Really bad behavior /** This function was written in TypeScript 1.0 **/ function magnitudeSquared(v: { x: number; y: number }) { return v.x * v.x + v.y * v.y; } // Now try to use it with ImmutablePoint console.log(magnitudeSquared(pt)); // Error, cannot use readonly object in non-readonly call
object
型の追加 [Accepted]
今まで地味にオブジェクト型で制約する型がありませんでしたがこれでようやく制約できるようになります。
出力したJSモジュールファイルの結合 [Merged]
出力したJSモジュールファイルを1つのファイルに結合します。
Browserifyがいらなくなくなります。 単にオプションを使って結合しただけではrequirejsに依存しますが、以下のスニペットをヘッダとして追加することでブラウザでもNodeでも動作するIsomorphicなライブラリとなります。
こちらのライブラリで実際にこの機能を使ってBrowserifyなしで同じ相互運用性を実現していますので試してみたい方は参考にするとよいでしょう。
循環参照が許されませんが、なんくるないさという気持ちになれば問題ありません。
ファイルパスのglobによるパターンマッチ [Scheduled]
ファイルパスにワイルドカードなどのglobのパターンマッチを使えるようにします。 コンパイルするファイルを何十個も列挙してメンテまでするのはつらすぎます。 v2.xで実装が予定されています。
Union/Intersection typesの型推論強化 [Merged]
Union/Intersection typeの分解時の型推論が強化され再利用性が向上します。 とてもうれしい。
type Maybe<T> = T | void; function isDefined<T>(x: Maybe<T>): x is T { return x !== undefined && x !== null; } function isUndefined<T>(x: Maybe<T>): x is void { return x === undefined || x === null; } function getOrElse<T>(x: Maybe<T>, defaultValue: T): T { return isDefined(x) ? x : defaultValue; } function test1(x: Maybe<string>) { let x1 = getOrElse(x, "Undefined"); // string let x2 = isDefined(x) ? x : "Undefined"; // string let x3 = isUndefined(x) ? "Undefined" : x; // string } function test2(x: Maybe<number>) { let x1 = getOrElse(x, -1); // number let x2 = isDefined(x) ? x : -1; // number let x3 = isUndefined(x) ? -1 : x; // number }
thisの型定義と型推論 [Discussion]
thisの型定義は避けては通れないのではないでしょうか。 クラスの自己型としてのthisはTS1.7のポリモーフィックthisで実装済みです。
let f = function(this: {data: number}) { console.log(this.data2) } let o = { data: 12 f: f g: function() { // this is inferred from the contextual type console.log(this.data); } }
function f(this: {n: number}, m: number) { return this.n + m; } class C { n: number m1(this:this, m: number) { return this.n + m } m2(this: {n: number}, m: number) { return this.n + m } }
型パラメーターによる制約 [Merged]
型パラメーターで型パラメーターの制約を作れるようになります。 今まで何度この型を書けずに涙を飲んだかわかりません。
function assign<T extends U, U>(target: T, source: U): T { for (let id in source) { target[id] = source[id]; } return target; } let x = { a: 1, b: 2, c: 3, d: 4 }; assign(x, { b: 10, d: 20 }); assign(x, { e: 0 }); // Error
?
プロパティーアクセサ [Suggestion]
CoffeeScriptの便利なやつです。
ESの仕様に入ってほしいくらいです。
ただ最初に提案されたものはx?.y?.z
の戻り値の型がx|y|z
であまりよろしくありません。
var x = { y: { z: null, q: undefined } }; console.log(x?.y?.z?.foo); // Should print 'null' console.log(x?.baz); // Still an error console.log(x.y.q?.bar); // Should print 'undefined'
ミスタイプ(ポスト)はデザイン [Design]
マイクロソフトによると指がすべって立てられたIssueはデザインによるものだそうです。
草。
まとめ
TypeScriptはバージョンアップを重ねるたびにますます洗練されていき、来年は上述の機能の追加によりさらに躍進が期待されます。
2016年もさらにヒートアップするTypeScriptをよろしく。
JavaScript(TypeScript)でEitherモナドとMaybeモナドを作る
JavaScriptでMaybeモナドのサンプルはよくありますがEitherモナドのサンプルはあまりないのでこれを作ってみます。
この記事はTypeScript アドベントカレンダー2015 24日目の記事です。
まずはMaybeモナドで肩慣らしです。
export class Maybe<T> { private MAYBE: [T, void]; constructor(private thunk_?: () => Maybe<T>) { } public bind(f: (val: T) => Maybe<T>): Maybe<T> public bind<U>(f: (val: T) => Maybe<U>): Maybe<U> public bind<U>(f: (val: T) => Maybe<U>): Maybe<U> { return new Maybe<U>(() => { const m = this.thunk_(); switch (true) { case m instanceof Just: { return f((<Just<T>>m).extract()); } case m instanceof Nothing: { return <Nothing>m; } case m instanceof Maybe: { return m.bind<U>(f); } default: { throw new TypeError(`Invalid monad value: ${m}`); } } }); } public extract(): T | void public extract<U>(defaultValue?: U): T | U public extract<U>(defaultValue?: U): T | U | void { return this.thunk_().extract(defaultValue); } } export class Just<T> extends Maybe<T> { private MAYBE_JUST: T; constructor(private val_: T) { super(); } public bind(f: (val: T) => Maybe<T>): Maybe<T> public bind<U>(f: (val: T) => Maybe<U>): Maybe<U> public bind<U>(f: (val: T) => Maybe<U>): any { return new Maybe(() => this).bind(f); } public extract(): T public extract<U>(defaultValue: U): T public extract<U>(defaultValue?: U): T { return this.val_; } } export class Nothing extends Maybe<any> { private MAYBE_NOTHING: void; public bind(f: (val: any) => Maybe<any>): Nothing { return this; } public extract(): void public extract<U>(defaultValue: U): U public extract<U>(defaultValue?: U): U { return defaultValue; } }
import {Maybe as Maybe_, Just as Just_, Nothing as Nothing_} from './maybe-impl'; export type Maybe<T> = Maybe_<T>; export namespace Maybe { export type Just<T> = Just_<T>; export function Just<T>(val: T): Just<T> { return new Just_(val); } export type Nothing = Nothing_; export const Nothing = new Nothing_(); export const Return = Just; } export type Just<T> = Maybe.Just<T>; export const Just = Maybe.Just; export type Nothing = Maybe.Nothing; export const Nothing = Maybe.Nothing; export const Return = Just;
基本的な動作を確認をしてみます。
it('Just', () => { const result = Return(0) .bind(n => Just(n + 1)) .bind(n => Just(n + 1).bind(n => Just(`Just ${n}`))) .extract('Nothing'); assert(result === 'Just 2'); }); it('Just nest', () => { const result = Return(Return(0)) .bind(m => Just(m)) .bind(m => m.bind(n => Just(n + 1)).bind(n => Just(`Just ${n}`))) .extract('Nothing'); assert(result === 'Just 1'); }); it('Nothing', () => { const result = Return(0) .bind(n => Just(n + 1)) .bind(n => Just(`Just ${n}`).bind(_ => Nothing)) .bind(throwError) .extract('Nothing'); assert(result === 'Nothing'); }); it('Nothing nest', () => { const result = Return(Return(0)) .bind(m => m.bind(n => Nothing).bind(throwError)) .bind(throwError) .extract('Nothing'); assert(result === 'Nothing'); });
モナド則を満たしているか確認します。
it('Monad law 1', () => { const f = (n: number) => Return(n + 1); const x = 0; const ma = Return(x).bind(f); const mb = f(x); assert(ma.extract() === mb.extract()); }); it('Monad law 2', () => { const f = (n: number) => Return(n + 1); const x = 0; const ma = Return(x); const mb = ma.bind(Return); assert(ma.extract() === mb.extract()); }); it('Monad law 3', () => { const f = (n: number) => Return(n + 2); const g = (n: number) => Return(n * 3); const x = 1; const ma = Return(x) .bind(f) .bind(g); const mb = Return(x) .bind(n => f(x) .bind(g)); assert(ma.extract() === mb.extract()); });
モナドであることが確認できました。
ついでに実行速度を計測しておきます。
it('Just', function (done) { benchmark('Just', () => Just(0), done); }); it('bind', function (done) { const just = Just(0); benchmark('bind', () => just.bind(n => Just(n)).extract(), done); });
Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Maybe Just x 16,005,033 ops/sec ±4.19% (74 runs sampled)' . Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Maybe Just x 10,628,015 ops/sec ±7.73% (70 runs sampled)' . Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Maybe bind x 2,352,541 ops/sec ±3.78% (74 runs sampled)' . Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Maybe bind x 2,776,510 ops/sec ±5.23% (74 runs sampled)' .
十分な速さです。
さて、本題のEitherモナドを作ってみましょう。
export class Either<L, R> { private EITHER: [L, R]; constructor(private thunk_?: () => Either<L, R>) { } public bind(f: (val: R) => Either<L, R>): Either<L, R> public bind<_R>(f: (val: R) => Either<L, _R>): Either<L, _R> public bind<_R>(f: (val: R) => Either<L, _R>): Either<L, _R> { return new Either<L, _R>(() => { const m = this.thunk_(); switch (true) { case m instanceof Left: { return <Left<L>><any>m; } case m instanceof Right: { return f((<Right<R>>m).extract()); } case m instanceof Either: { return m.bind<_R>(f); } default: { throw new TypeError(`Invalid monad value: ${m}`); } } }); } public extract(): L | R public extract<_L>(transform: (left: L) => _L): _L | R public extract<_L>(transform?: (left: L) => _L): L | _L | R { return (<(transform: (left: L) => _L) => L | _L | R>this.thunk_().extract)(transform); } } export class Left<L> extends Either<L, any> { private EITHER_LEFT: L; constructor(private val_: L) { super(); } public bind(f: (val: any) => Either<L, any>): Left<L> { return <any>this; } public extract(): L public extract<_L>(transform: (left: L) => _L): _L public extract<_L>(transform?: (left: L) => _L): L | _L { return transform ? transform(this.val_) : this.val_; } } export class Right<R> extends Either<any, R> { private EITHER_RIGHT: R; constructor(private val_: R) { super(); } public bind<L>(f: (val: R) => Either<L, R>): Either<L, R> public bind<L, _R>(f: (val: R) => Either<L, _R>): Either<L, _R> public bind<L, _R>(f: (val: R) => Either<L, _R>): any { return new Either<L, R>(() => this).bind<_R>(f); } public extract(transform?: (left: any) => any): R { return this.val_; } }
import {Either as Either_, Left as Left_, Right as Right_} from './either-impl'; export type Either<L, R> = Either_<L, R>; export namespace Either { export type Left<L> = Left_<L>; export function Left<L>(val: L): Left<L> { return new Left_<L>(val); } export type Right<R> = Right_<R>; export function Right<R>(val: R): Right<R> { return new Right_<R>(val); } export const Return = Right; } export type Left<L> = Either.Left<L>; export const Left = Either.Left; export type Right<R> = Either.Right<R>; export const Right = Either.Right; export const Return = Either.Return;
基本動作
it('Left', () => { const result = Return(0) .bind(n => Right(n + 1)) .bind(n => Right(n + 1).bind(n => Left(`Left ${n}`))) .bind(n => Right(`Right ${n}`)) .extract(l => `Left ${l}`); assert(result === 'Left Left 2'); }); it('Left nest', () => { const result = Return(Return(0)) .bind(m => m.bind(n => Left(NaN)).bind(throwError)) .bind(throwError) .extract(_ => 'Nothing'); assert(result === 'Nothing'); }); it('Right', () => { const result = Return(0) .bind(n => Right(n + 1)) .bind(n => Right(n + 1).bind(n => Right(`Right ${n}`))) .extract(l => `Left ${l}`); assert(result === 'Right 2'); }); it('Right nest', () => { const result = Return(Return(0)) .bind(m => Right(m)) .bind(m => m.bind(n => Right(n + 1)).bind(n => Right(`Right ${n}`))) .extract(_ => 'Nothing'); assert(result === 'Right 1'); });
モナド則
it('Monad law 1', () => { const f = (n: number) => Return(n + 1); const x = 0; const ma = Return(x).bind(f); const mb = f(x); assert(ma.extract() === mb.extract()); }); it('Monad law 2', () => { const f = (n: number) => Return(n + 1); const x = 0; const ma = Return(x); const mb = ma.bind(Return); assert(ma.extract() === mb.extract()); }); it('Monad law 3', () => { const f = (n: number) => Return(n + 2); const g = (n: number) => Return(n * 3); const x = 1; const ma = Return(x) .bind(f) .bind(g); const mb = Return(x) .bind(n => f(x) .bind(g)); assert(ma.extract() === mb.extract()); });
it('Right', function (done) { benchmark('Right', () => Right(0), done); }); it('bind', function (done) { const right = Right(0); benchmark('bind', () => right.bind(n => Right(n)).extract(), done); });
Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Either Right x 19,315,144 ops/sec ±4.97% (76 runs sampled)' . Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Either Right x 12,051,406 ops/sec ±5.15% (77 runs sampled)' . Chrome 47.0.2526 (Windows 7 0.0.0) LOG: 'Either bind x 2,131,294 ops/sec ±3.88% (77 runs sampled)' . Firefox 43.0.0 (Windows 7 0.0.0) LOG: 'Either bind x 2,634,425 ops/sec ±5.30% (77 runs sampled)' .
MaybeモナドもEitherモナドもJavaScriptで実用的な実装を作ることができます。
これらのモナドは以下のライブラリに含まれています。
追記
ライブラリではモナドが複数回評価されないようメモ化しました。
TypeScriptのコンパイルだけでBrowserifyを使わずにモジュールファイルを結合したIsomorphicなNPMのパッケージを作る
TypeScript1.8ではモジュールを結合して1つのファイルに出力できるようになり、これによりBrowserifyを使わずにIsomorphicなJavaScriptライブラリを作れるようになります。NPMのパッケージもNodeでもブラウザでもそのまま使える素敵なライブラリが簡単に作れます。これはもうTypeScriptを使うしかありませんね(バージョン1.8から)。
この記事はTypeScript アドベントカレンダー2015 8日目の記事です。
出力したモジュールファイルを1つのファイルに結合する機能がマージされました。
ただしこの機能はパッケージ名でネームスペースを集約できないようなのでライブラリ開発でそのまま使うことはできません。
パッケージ名でルートのファイルを作ることでネームスペースを区切れるようなのでそこだけ気をつけましょう。
// src/a.ts export default 1; // src/b.ts import a from './a'; export var b = a + 1; // mylib.ts import a from './src/a'; import {b} from './src/b'; export default a + b; console.log(a, b);
これを1つのファイルにコンパイルします。
$ tsc -t es5 -m amd --moduleResolution node --outFile bundle.js mylib.ts src/a.ts src/b.ts
次のように出力されます
// bundle.js define("src/a", ["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = 1; }); define("src/b", ["require", "exports", "src/a"], function (require, exports, a_1) { "use strict"; exports.b = a_1.default + 1; }); define("mylib", ["require", "exports", "src/a", "src/b"], function (require, exports, a_2, b_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = a_2.default + b_1.b; console.log(a_2.default, b_1.b); });
このままでは動かないので先頭に以下のコードを追加してブラウザとNodeの両方で動くようにします(Node/Chrome/Firefox/IE9で動作確認済み)。
ソースコードでrequire, exports, moduleを触っているコードには対応していないのでes6 modulesだけ使ってください。
define関数が定義されていなければブラウザでもNodeと同様に同期的に読み込まれグローバル変数に追加されます。
// node $ node > require('./bundle.js') 1 2 { default: 3 } // browser > 1 2 mylib > { default: 3 }
うまく動きました。
実際にこの方法で作ったクロスプラットフォームで動作するパッケージがこちらです。
TypeScript1.8がリリースされたらNPMのパッケージはTypeScriptでIsomorphicに作りましょう。
ヘキサゴナルアーキテクチャとレイヤードアーキテクチャの違いとマイクロサービス
ヘキサゴナルアーキテクチャとレイヤードアーキテクチャのざっくりとした違いとヘキサゴナルアーキテクチャの利点を説明する。
ヘキサゴナルアーキテクチャは主に六角形で描かれる、多角形の全辺が外部とのインターフェイスであり、内部にさらにアプリケーションレイヤーやドメインコンテキストといった1つ以上のレイヤーを持つアーキテクチャである。 層構造はレイヤードアーキテクチャと同じであり、要はレイヤードアーキテクチャを最下層を軸に一回転させて全方位に対して層構造の性格が変わらないようにしただけである。
ヘキサゴナルアーキテクチャは実装においてはレイヤードアーキテクチャと同じだが、設計を行う上でいくつかの認知的効果がある。
複数のインターフェイスを表現しやすい
レイヤードアーキテクチャではインターフェイスは直線または長方形1つのインターフェイスレイヤーで表現されるため複数のインターフェイスを定義するにはこの直線や長方形を細切れに分割していかなければならず複数のインターフェイスを表現しにくかった。
ヘキサゴナルアーキテクチャでは各面によって異なるインターフェイスを表現できるため複数のインターフェイスを設計しやすくインターフェイスを複数持つ設計に積極的になれる。
インターフェイスとビジネスロジックの分離が促進される
レイヤードアーキテクチャではインターフェイスレイヤーにビジネスロジックなど他のレイヤーの責務が混入するしやすく異なるインターフェイスを追加する障害となっていた。
ヘキサゴナルアーキテクチャは全周囲からアクセスが行われることが視覚的に明白であるためサービスがどのインターフェイスにおいても等しく機能しなければならないという設計要件を効果的に認識させることができる。開発者は常にいずれかひとつの面だけでなくすべての面からの入力に対して等しく機能する内部レイヤーを作らなければならないというイメージを持って開発することになるため抽象化が促進されインターフェイスレイヤーが適切に構築されるようになる。
なお内部レイヤーについてはインターフェイスと同じく六角形で表現する例がよくあるが円形で表現することを勧める。内部レイヤーは基本的にすべてのインターフェイスに対して同じ共通のAPIを提供すべきであり、インターフェイスごとにAPIを変える設計というのはナンセンスだからである。
複数サービス間の連携を表現しやすい
レイヤードアーキテクチャは上下の方向の概念があるため複数のアーキテクチャを連携のためにそのまま並べようとすると上下へアクセスの端点を接続できる配置を意識しなければならない煩雑さがある。これはサービス同士の関係がピラミッド型の厳格な階層構造である場合は都合がいいがネットワーク型である場合は向いていない。
ヘキサゴナルアーキテクチャは全周囲にアクセスの端点を持てるためネットワーク状のサービス連携を表現しやすい。
なお通信を行うレイヤーがインターフェイスであるかドメインまたはインフラであるかの差異を多角形の上下または左右で表現する例がよくあるが端点を外周のインターフェイスに持つか内部レイヤーに持つかで表現することを勧める。このような図は互いの位置関係を気にすることなく依存関係を表現することができるからである。たとえばサービスAの内部レイヤーとサービスBのインターフェイスが線で結ばれているならば位置関係に関係なくAはBに依存している。
マイクロサービスと相性がいい
上記のような複数サービス間の連携の表現しやすさはマイクロサービスの設計と相性がよい。マイクロサービスの連携を設計するときは見た目だけでもヘキサゴナルアーキテクチャで表したほうがいいだろう。実体は後から実装すればよい。
まとめ
要はレイヤードアーキテクチャを正しく実装するためのより正確で効果的な視覚表現がヘキサゴナルアーキテクチャである。レイヤードアーキテクチャと比べて特に変わったことをしているわけではない。
TypeScriptで形式的証明・交差型編 ~ 状態付きの型による高信頼領域の構築
TypeScriptで交差型を使って静的型に状態的な型を動的に付与していきます。 これによりサニタイズ済/未サニタイズ、エンコード済/未エンコード、不変/可変、通常文脈/エラー文脈、Truthy/Falsyといった状態を複合的に扱えるようになります。 さっそく試してみましょう。
この記事はTypeScript アドベントカレンダー2015 2日目の記事です。
サニタイズ済みである文字列のみを受け取る型を作ってみます。
declare class Sanitized { private id; } function sanitize<T>(data: T): Sanitized&T { return <Sanitized&T>data; // any processing } var plain = ''; var secure: Sanitized&string = plain; // type error var secure: Sanitized&string = sanitize(plain); // ok
たったこれだけです。たったこれだけで信頼境界の内側と外側を分離できるのです。もはやただの文字列は安全な文字列を要求する型に入れることができなくなりました。
見た目が通常の型宣言とよく似ているためアノテーションとしても利用できます。それでいて、この手法は型のみで実現され実装に影響を及ぼさないためまったく何のオーバーヘッドもありません。
信頼境界を越えるときに安全な文字列に変換し、境界内では安全な文字列だけ使うように制限すればプログラムが確実に安全なデータだけを使っていることが保証されるようになります。
苦労して経路を網羅しなくても受け取り側が型を見て不正なデータを拒否するので、境界内が要件を満たさない不正なデータで汚染されることはありません。
信頼境界内は常に安全なデータだけで満たされているクリーンな領域となります。
このような状態付きの型を定義する方法は他にもいくつかありますが、ここで紹介した交差型を使用した方法はその中でも汎用性が高く開発者の負担が小さいものです。今日からでも使い始められるでしょう。
上記のコードは最低限の機能しかないので、以下の条件を追加してもう少し実用的にしてみます。
- 二重に状態を付与しようとした場合はvoidとなる。
export declare class Sanitized { private SANITIZED; } export function type<T>(target: Sanitized&T): void export function type<T>(target: T): Sanitized&T export function type<T>(target: T): Sanitized&T { return <Sanitized&T>target; } export function extract<T>(target: Sanitized&T): T { return <T>target; }
プリミティブ型の次はデータ型にも対応できるようオブジェクトに状態を持たせてみましょう。
すいません動くコードロストして再実装できなくてロストテクノロジーになりました。以下のコード実装できた方いたら教えてください(泣)。
データ型については信頼できるデータのみを保持する信頼境界内用の公称型を用意して信頼性を確保してください。
declare class Sanitized { private id; } function sanitize<T>(data: T): Sanitized&T { return <Sanitized&T>data; // any processing } class Data<T> { constructor(public value: T) { } public sanitize(this: Data<Sanitized>): void public sanitize(): Data<Sanitized&T> public sanitize(): Data<Sanitized&T> { return new Data(sanitize(this.value)); } } var plain = new Data(''); var secure: Data<Sanitized&string> = plain; // type error var secure: Data<Sanitized&string> = plain.sanitize(); // ok var secure: Data<Sanitized&string> = plain.sanitize().sanitize(); // type error
これまでは信頼境界内の信頼性を境界通過時の検査のみで担保・依存し、ひとたび境界内に入ればあとはフリーパスになって不正なデータが混入しても検出できないという状態に甘んじていたこともありましたが、型に状態を持てるようにすることで要求される状態を持たない値が境界内で誤って使われようとしても要件を満たせず拒否される信頼性の高い領域を構築できます。
これまで境界線という線で確保していた信頼性と安全性を境界内の空間全体というより高い次元で確保できるようになったのです。より応用的には個々のデータのうち特に信頼性を求める場合は専用に公称型のデータ型を用意することでさらに確実な操作を行い、それ以外のデータは状態の要件により信頼性を確保するといったアプローチが望ましいでしょう。
今回の内容は単純な状態遷移をモデルへ部分的に適用しただけなので形式的証明と呼べるほどのものではありませんでしたが*1、実装に高水準の信頼性を与える方法として形式的証明と同様非常に効果的なものです。
外部と通信を行うアプリケーションは規模の大小にかかわらずサニタイズと正規化を厳しく要求されるため本稿で紹介したように形式的に信頼性を保証する方法が非常に重要となってきますので、これを期にぜひ自身のプロダクトの信頼性と品質の向上に取り組んでみてください。
TypeScriptアドベントカレンダー2015、需要と余裕があれば次回は状態遷移モデルの形式的証明を行う並行処理および並列処理の形式的証明(状態遷移編)です。予定は未定。
*1:形式的証明は未証明部分で破綻していては意味がないので原則として証明対象の全機能を証明します