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させて使用する