Make it to make it

いろいろ作ってアウトプットするブログ

TypeScriptのデザインパターン

デザインパターン1

Google Maps JavaScript APIを利用して、Google Mapsのヘルパークラスと、そのヘルパークラスに渡されるインスタンス生成用のクラスを複数用意するパターン。

.
├── Company.ts -- Creates instance to pass to CustomMap.ts
├── CustomMap.ts -- Google Maps API helper class
├── User.ts -- Creates instance to pass to CustomMap.ts
└── index.ts -- Entry point

イントロ

tsconfig.jsonはこんな感じ。

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "strict": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "module": "es6",
    "moduleResolution": "node",
    "target": "es5",
    "allowJs": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["./src/**/*"]
}

Google Mapはシンプルにindex.htmlにscriptタグでCDNで読み込む。

<script src="https://maps.googleapis.com/maps/api/js?key=XXXXXXXXXXXX"></script>

経度や緯度を含めたダミー情報を読み込む用にはfakerというパッケージを使う。 また、Google Maps JavaScript APIを使用する際の型のサポートを行うパッケージも追加する。

yarn add faker && yarn add -D @types/faker @types/googlemaps

コード

ヘルパークラスCustomMap.ts

APIを直接扱うためのクラス。

export interface IMappable {
  location: {
    lat: number
    lng: number
  }
  markerContent(): string
  color: string
}

export class CustomMap {
  private googleMap: google.maps.Map

  constructor(elId: string) {
    const $mapEl = document.getElementById(elId)

    if ($mapEl === null) {
      throw new Error('Map is null or has not been rendered!')
    }

    $mapEl.style.minHeight = '500px'

    this.googleMap = new google.maps.Map($mapEl, {
      zoom: 1,
      center: {
        lat: 0,
        lng: 0,
      },
    })
  }

  addMarker(mappable: IMappable): void {
    const marker = new google.maps.Marker({
      map: this.googleMap,
      position: {
        lat: mappable.location.lat,
        lng: mappable.location.lng,
      },
    })

    marker.addListener('click', () => {
      const infoWindow = new google.maps.InfoWindow({
        content: mappable.markerContent(),
      })

      infoWindow.open(this.googleMap, marker)
    })
  }
}

private googleMap: google.maps.Map

インスタンス変数の型をprivateで指定することで、クラスを使用する際に不用意に書き換えされないようにする。不用意にアクセスして書き換えようとすると、以下のようなエラーが表示される。

(property) CustomMap.googleMap: google.maps.Map<Element>
Property 'googleMap' is private and only accessible within class 'CustomMap'.ts(2341)

addMarker(mappable: IMappable): void

Google Map上にマーカーを表示するためのメソッド。 mappable に緯度、経度、マーカーの中身を返す関数を渡す。

インスタンス生成用クラスUser.ts

適当なユーザー名と緯度・経度を生成する。

import faker from 'faker'
import { IMappable } from './CustomMap'

export class User implements IMappable {
  name: string
  location: {
    lat: number
    lng: number
  }
  color: string = 'userDefaultColor'

  constructor() {
    this.name = faker.name.firstName()
    this.location = {
      lat: parseFloat(faker.address.latitude()),
      lng: parseFloat(faker.address.longitude()),
    }
  }

  markerContent(): string {
    return `
      <div>
        <p>User name: ${this.name}</p>
      </div>
    `
  }
}

インスタンス生成用クラスCompany.ts

適当な会社名と緯度・経度を生成する。

import faker from 'faker'
import { IMappable } from './CustomMap'

export class Company implements IMappable {
  companyName: string
  catchPhrase: string
  location: {
    lat: number
    lng: number
  }
  color: string = 'companyDefaultColor'

  constructor() {
    this.companyName = faker.company.companyName()
    this.catchPhrase = faker.company.catchPhrase()
    this.location = {
      lat: parseFloat(faker.address.latitude()),
      lng: parseFloat(faker.address.longitude()),
    }
  }

  markerContent(): string {
    return `
      <div>
        <p>Company name: ${this.companyName}</p>
        <small>Catchphrase: ${this.catchPhrase}</small>
      </div>
    `
  }
}

export class User implements IMappable

この書き方で、インスタンス生成用のクラスの補助的なインターフェースのような役割を付与することができる。

export interface IMappable {
  location: {
    lat: number
    lng: number
  }
  markerContent(): string
  color: string
}

となっているので、これらのインスタンス変数とメソッドは必須になる。 それ以外はoptionalで必要な場合に指定するかたち。

エントリーポイントindex.ts

import { User } from './User'
import { Company } from './Company'
import { CustomMap } from './CustomMap'

const user = new User()
const company = new Company()
const customMap = new CustomMap('app')

customMap.addMarker(user)
customMap.addMarker(company)

書き方メモ

当たり前といえば当たり前だが、典型的なTypeScriptファイルは下記のような構成となる。

- ファイル上部
  - クラスを扱うためのインターフェース
- ファイル下部
  - クラス定義

デザインパターン2

Bubble sortという最も単純なソートのアルゴリズムを利用して、number, string, node (独自numberのコレクション配列) のソートを行う例。

.
├── CharactersCollection.ts -- Sort array of strings
├── LinkedList.ts -- Sort array of collections of numbers
├── NumbersCollection.ts -- Sort array of numbers
├── Sorter.ts -- Common sorting algorithm
└── index.ts -- Entry point

イントロ

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true
  }
}

typescript以外に必要なパッケージをインストール。こちらの例では実行環境はNode上のみ。

yarn add -D concurrently nodemon

npm scripts

concurrentlyを使用してtscでJSにコンパイルしながらwatchを行うと同時に、nodemonでリアルタイムにterminal上にprint outする。

"start:build": "tsc -w",
"start:run": "nodemon build/index.js",
"start": "concurrently yarn:start:*",

Bubble Sort

配列の中を走査しながら前の項目と後ろの項目を比較し、指定のソートを行うというアルゴリズム。コードとしては微妙だが、forループを2重で書くことで実現できる。

for (let i = 0; i < length; i++) {
  for (let j = 0; j < length - i - 1; j++) {
    if (this.collection[j] > this.collection[j + 1]) {
      const leftHand = this.collection[j]
      this.collection[j] = this.collection[j + 1]
      this.collection[j + 1] = leftHand
    }
  }
}

コード

Abstract class Sorter.ts

Abstract classを使用することで、sortロジックのみをコンパクトに抽出して使用することができる。

export abstract class Sorter {
  abstract length: number
  abstract compare(leftIndex: number, rightIndex: number): boolean
  abstract swap(leftIndex: number, rightIndex: number): void

  sort(): void {
    const { length } = this

    for (let i = 0; i < length; i++) {
      for (let j = 0; j < length - i - 1; j++) {
        if (this.compare(j, j + 1)) {
          this.swap(j, j + 1)
        }
      }
    }
  }
}

NumbersCollection.ts

数字の配列をsortする。

import { Sorter } from './Sorter'

export class NumbersCollection extends Sorter {
  constructor(public data: number[]) {
    super()
  }

  get length(): number {
    return this.data.length
  }

  compare(leftIndex: number, rightIndex: number): boolean {
    return this.data[leftIndex] > this.data[rightIndex]
  }

  swap(leftIndex: number, rightIndex: number): void {
    const leftHand = this.data[leftIndex]
    this.data[leftIndex] = this.data[rightIndex]
    this.data[rightIndex] = leftHand
  }
}

CharactersCollection.ts

文字列の配列をsortする。

import { Sorter } from './Sorter'

export class CharactersCollection extends Sorter {
  constructor(public data: string) {
    super()
  }

  get length(): number {
    return this.data.length
  }

  compare(leftIndex: number, rightIndex: number): boolean {
    return this.data[leftIndex].toLowerCase() > this.data[rightIndex].toLowerCase()
  }

  swap(leftIndex: number, rightIndex: number): void {
    const characters = this.data.split('')

    const leftHand = characters[leftIndex]
    characters[leftIndex] = this.data[rightIndex]
    characters[rightIndex] = leftHand

    this.data = characters.join('')
  }
}

LinkedList.ts

数字のコレクションの配列をsortする。

import { Sorter } from './Sorter'

class Node {
  next: Node | null = null

  constructor(public data: number) {}
}

export class LinkedList extends Sorter {
  head: Node | null = null

  add(data: number): void {
    const node = new Node(data)

    if (!this.head) {
      this.head = node
      return
    }

    let tail = this.head
    while (tail.next) {
      tail = tail.next
    }

    tail.next = node
  }

  get length(): number {
    if (!this.head) {
      return 0
    }

    let length = 1
    let node = this.head
    while (node.next) {
      length++
      node = node.next
    }

    return length
  }

  at(index: number): Node {
    if (!this.head) {
      throw new Error('Index out of bounds')
    }

    let counter = 0
    let node: Node | null = this.head
    while (node) {
      if (counter === index) {
        return node
      }

      counter++
      node = node.next
    }

    throw new Error('Index out of bounds')
  }

  compare(leftIndex: number, rightIndex: number): boolean {
    if (!this.head) {
      throw new Error('List is empty')
    }

    return this.at(leftIndex).data > this.at(rightIndex).data
  }

  swap(leftIndex: number, rightIndex: number): void {
    const leftNode = this.at(leftIndex)
    const rightNode = this.at(rightIndex)

    const leftHand = leftNode.data
    leftNode.data = rightNode.data
    rightNode.data = leftHand
  }

  print(): void {
    if (!this.head) {
      return
    }

    let node: Node | null = this.head
    while (node) {
      console.log(node.data)
      node = node.next
    }
  }
}

エントリーポイントindex.ts

import { NumbersCollection } from './NumbersCollection'
import { CharactersCollection } from './CharactersCollection'
import { LinkedList } from './LinkedList'

const numbersCollection = new NumbersCollection([10, 3, -5, 0, 10000])
numbersCollection.sort()
console.log(numbersCollection.data)

const charactersCollection = new CharactersCollection('Xaayb')
charactersCollection.sort()
console.log(charactersCollection)

const linkedList = new LinkedList()
linkedList.add(500)
linkedList.add(-10)
linkedList.add(-3)
linkedList.add(4)

linkedList.sort()
linkedList.print()

👇ログ結果

# numbersCollection
[ -5, 0, 3, 10, 10000 ]

# charactersCollection
CharactersCollection { data: 'aabXy' }

# linkedList
-10
-3
4
500

書き方メモ

Interfaces

  • Sets up a contract between different classes
  • Use when we have very different objects that we want to work together
  • Promotes loose coupling

Inheritance / Abstract classes

  • Sets up a contract between different classes
  • Use when we are trying to build up a definition of an object
  • Strongly couples classes together

👉抽象化したビジネスロジックを含んだ抽象化クラスを、使用するクラス側ででextendsさせて使用する