Theme
SD MILIEU

2022-6-21

耳にはするが意味がわかってなかった用語をまとめる

耳にはするが意味をふんわりとは知っているがちゃんと理解していない用語がよくある。随時その用語と詳細を追加していく。

null安全

Null Pointer Exception を起こさないようにする仕組みのこと。

TypeScriptで言うところの以下のようなコード

const hoge: string[] | undefined = undefined

if (hoge.length === 0) someFunc() // <= コンパイルエラー

上記のコードはTypeScriptだと hoge.length をしている時点で string[] であることが確定していないのでコンパイルエラーとなる。これを回避するには以下のようにする必要がある。

const hoge: string[] | undefined = undefined

if (hoge !== undefined) {
  if (hoge.length === 0) someFunc() // <= コンパイルOK
}

TypeScriptを使っていると割と当然のように思える仕組みだが、このような仕組みがない言語だと List 型の変数に当然のように null が入っているケースがあり、nullチェックがないせいでランタイムエラーが発生するというのがよくあったみたい。

null安全でない言語は、もはやレガシー言語だ

参照透過性

関数の性質。引数で結果が一意に定まること。

以下の関数は、引数がそもそも無いのに呼び出したタイミングによって結果が変わってしまうので参照透過性が無い。

const getCurrentUnixTime = () => {
  return Date.now()
}

参照透過性がないと、関数のテストが非常にやりにくくなる。今回の場合だと Date のモックをする必要がある。

以下のようにコードを修正すると、改善する。

const getUnixTime = (date: Date) => {
  return date.getTime()
}

const currentDate = new Date()
const currentUnixTime = getUnixTime(currentDate)
  • 現在のUnixTimeを取得する

という処理を

  • 現在時刻を取得する
  • 時刻をUnixTimeに変換する

という参照透過に出来る処理と出来ない処理に分割することで、テストしやすい部分を作り出せた。

現在時刻を取得する箇所に関してはテストしにくいのはどうしようもない。

関数型プログラムの紹介かつ自分用備忘録的なもの

純粋関数

参照透過かつ副作用を持たない関数のこと。

参照透過はあくまで返り値が一意に定まるだけで、副作用の有無は問わない。なので「参照透過だが副作用がある」はありうる。

例えばJavaScriptの sort 関数は元となる配列に破壊的変更を加えるが、ソート後の配列を返り値として返しており、そしてそれは引数により一意に定まるので参照透過である。

なので「参照透過な関数」って言葉が出てくる時、正しくは「純粋関数」を指していることが結構ありそう。

ちなみに純粋関数には、結果をキャッシュすることも出来るというメリットもあったりする。

純粋関数 - Qiita

冪等性

現在の状態に関わらず、何回実行しても実行後の状態が同じになること。

例えば、サーバー監視の有効/無効を制御する toggleServerMonitoring という処理があるとする。これは現在の状態に応じて実行後の状態は異なる。これは冪等性が「無い」。

冪等性が無いと困ることがある。一例として何らかの理由で同じ処理が複数回実行された際に予期せぬ結果になることがある。例えばHttpリクエストに失敗してリトライしたら、2回処理が実行された等。

今回のケースだと、 toggleServerMonitoring ではなく enableServerMonitoringdisableServerMonitoring の処理を用意しそれぞれ必要な方を叩けばいい。サーバーの監視を有効化したい際は enableServerMonitoring を実行し、そうすればリトライにより複数回実行されても「サーバーの監視が有効化されている」という状態になることは変わらない。

冪等性とは「同じ操作を何度繰り返しても、同じ結果が得られる性質」のこと

第一級関数・高階関数・コールバック関数

第一級関数

関数を第一級オブジェクトとして扱うことのできるプログラミング言語の性質、またはそのような関数のこと
第一級関数

たとえば生成、代入、演算、(引数・戻り値としての)受け渡しといったその言語における基本的な操作を制限なしに使用できる対象のことである。
第一級オブジェクト

まとめると、「生成・代入・演算・引数戻り値としての受け渡しといった基本的な操作を制限なしに使用できる関数のこと」。関数が数値とかと同様に操作出来るのであれば第一級関数なので、JavaScriptの関数は第一級関数。C言語は関数ポインタにすれば基本的な操作が可能だが、関数ポインタという物を経由する必要があるからか第一級関数とみなされてはいない模様。

高階関数・コールバック関数

高階関数は「引数または戻り値に関数がある関数」のこと。コールバック関数は「関数の引数として指定された関数」のこと。

const highOrderFunction = (callbackFunction: () => void) => {
  // なんらかの処理

  callbackFunction()
}

TypeScriptのコードで言うと、上の highOrderFunction が高階関数で callbackFunction がコールバック関数。

メタプログラミング

なんかプログラムでプログラムを弄ること。

言葉では説明しづらいので具体例を挙げる。

const repository = {
  async fetchHtml() {
    const response = await fetch('https://example.com')
    const text = response.text()

    return text
  }
}

const main = async () => {
  const result = await repository.fetchHtml()
  console.log(result)
}

main()

上のような通信が伴う処理がある時、main関数をテストしたいなってなったら困る。テストのための通信を受けるサーバーを用意したりなんやかんやで。

なので、テストの時には実際に通信を走らせたくない。素直にやるのであればDIしてテスト時は実際に通信が走らないMockRepository的なのを呼び出せばいいけど、以下のような書き方もできる。

const repository = {
  async fetchHtml() {
    const response = await fetch('https://example.com')
    const text = response.text()

    return text
  }
}

Object.defineProperty(repository, 'fetchHtml', {
  value: async () => {
    return 'mocked html'
  }
})

const main = async () => {
  const result = await repository.fetchHtml()
  console.log(result)
}

main()

上は Object.defineProperty で関数の動作を上書きしている。この方法であればinterfaceを大量に作ったりとかしなくてもテストに関して解決する。Jestの jest.mock はここらへんを使ってうまいことなんやかんやしてるのだと思う。

こんな風にランタイムに記述されたソースの動作を無理やり上書きしたりする感じのやつをメタプログラミングって言うんだと思う(ふんわりとした理解)。

メタプログラミング - JavaScript | MDN

腐敗防止層

理想の構造にしたいが現実的に無理な時に、腐敗防止層という層を作って理想的じゃない部分はそこに押し込める。インタフェースだけ理想の形にして、腐敗防止層内で理想の構造と現実の構造を変換する、的なやつ、多分。

個人的によくやるのが、フロントエンド的にはこういうAPIが欲しいが、諸々の事情でそのAPIが用意出来ない場合に、腐敗防止層を作って理想的なAPIが存在しない悪影響を最小限に留めるようにしたりする。(サードパーティのAPIを利用して何か作るときによくやる)

具体例

例えば、あるAPI [GET] /user/:id では性別を、数値で返していて、

1: 男性
2: 女性
3: その他

みたいなマッピングになっているとする。

また、別のAPI [GET] /company/:id/employee では性別を文字列で返していて、

male: 男性
female: 女性
other: その他

みたいなマッピングになっている。

本来ならバックエンド側を改修すべきだが、バックエンドの人手が足りないとかそこらへんの事情で対応無理でーす、な状況。レガシーサービスのフロントエンドのリプレースであり得る。

まぁ、割とこのレベルの状況な時点で相当やばいんだけど、それはさておき。

ここは本来API側で解決すべき内容なので、UI側には悪影響を与えたくない。

なので、Repository層を腐敗防止層としてレスポンスを変換するみたいな事をする。

例えば、UI側での性別の型が、

type Gender = 'male' | 'female' | 'other'

だとすると、UI側の型と異なっており変換が必要な UserRepository では

const convertGender = (gender: number) => {
  switch (gender) {
    case 0:
      return 'male'
    case 1:
      return 'female'
    case 2:
      return 'other'
  }

  throw new Error(`想定外のgender: ${gender}`)
}

const fetchUser = (id: string): Promise<User> => {
  const response = axios.get('some_api_url')

  return {
    ...response.data,
    gender: convertGender(response.data.gender)
  }
}

みたいな感じで変換して返却する。

こうしておけばUI側の実装はAPIが理想的じゃない事を気にせず実装出来てシンプルになる。

DTO/DAO

  • DTO
    • データの受け渡しのための入れ物となるオブジェクト
  • DAO
    • データストアへのアクセス手段を提供するオブジェクト
      • Repositoryとほぼ同じだが、Repositoryより抽象度が低い

コード例(TypeScript)

type UserDTO = {
  id: string
  name: string
  age: number
}

interface UserDAO {
  list(): Promise<UserDTO[]>

  find(id: string): Promise<UserDTO[]>

  ...
}

こんな感じのやつだと思う、意識してないだけでめっちゃ使う(特にDTO)。

DTO見てると「なんでこんなものに大仰なパターン名がつけられてるんだ?」って思ったりするけど、JavaScriptと比べてJavaはオブジェクトの作成が面倒(クラス定義してgetter/setter定義して〜ってやらないといけない)なので、手間がある分特別感があって名付けされたのかも(適当)。

ちなみに、DTOのJavaでの実装例見てると「なんでわざわざprivateにしてgetter/setter定義してるんだ?この利用法ならpublicでいいのでは?」と思ってたら同様のことを思っている人がいた。さらに言うとデータ受け渡しのオブジェクトなんだからgetterだけ用意するreadonlyな形がより良い気もする。

DAOは上でRepositoryより抽象度が低いと書いたが、DAOの解説記事によっては「データベースの変更に対応できるようインターフェースを定義して〜」とかもあったりするので解釈がマチマチなのかもしれない。たまに抽象度が低いRepositoryを見たりする(自分もやってしまったりする)のでまぁそういうもんか。

参考

やはりお前たちのRepositoryは間違っている - Qiita

構造的部分型

派生型の方式。派生型の方式としては構造的部分型(TypeScript,Goとか)と公称型(Java,C++)があるみたい。

違い

派生型は原則としてリスコフの置換原則っていうのを満たす必要がある。

リスコフの置換原則はざっくり「型Aの派生型Bが存在する場合、AはBで置換できる」っていうやつ。

リスコフの置換原則(LSP)をしっかり理解する - Qiita

公称型と構造的部分型の違いは、公称型では派生型同士は置換できないという点。

具体的に言うと、

// 公称型(Java)

class A {
  void someFunc() {
    //~
  }
}

class B extends A {
  void someFunc() {
    //~
  }
}

class C extends A {
  void someFunc() {
    //~
  }
}

// 問題なし
A var1 = new B()
A var2 = new C()

// コンパイルエラー
B var3 = new C()
C var4 = new B()
// 構造的部分型(TypeScript)

class A {
  someFunc() {
    //~
  }
}

class B extends A {
  someFunc() {
    //~
  }
}

class C extends A {
  someFunc() {
    //~
  }
}

// 問題なし
const var1: A = new B()
const var2: A = new C()

// 問題なし
const var3: B = new C()
const var4: C = new B()

こんな感じ。

構造的部分型は「シグネチャだけ揃ってれば置換可能でいいよね!」という割とゆるふわな感じ。

参考

構造的部分型 (structural subtyping) | TypeScript入門『サバイバルTypeScript』