js-gof について
js-gofは、JavaScript開発者向けにGoFパターンの活用を案内するスキルです。コンポジション、SOLID、低結合などの原則を、生成系・構造系パターンの説明とコード例を通じて扱います。
主な機能
- 01生成系パターンのガイダンス
- 02構造系パターンのガイダンス
- 03JavaScriptのコード例
- 04コンポジションと低結合の重視
活用シーン
- 01オブジェクト生成パターンの選択
- 02互換性のないインターフェースの適合
- 03疎結合なJavaScriptシステムの設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ metarhia/ metaskills --skill js-gofインストール後、Claude Code で/js-gofを入力して読み込まれたか確認します。
詳細
GoF パターン
- 継承よりも構造的なコンポジションを使用します
- マルチパラダイムなコードと契約プログラミングを使用します
- ドメイン固有言語(DSL)を使用し、宣言的な方法を優先します
- システムコードとドメインコードを分離し、混在させないようにします
- GRASP と SOLID の原則を適用します。特に、結合度を下げます
- 参照透過性を忘れないようにします
- プラットフォームに依存しないコードを優先します
- IoC と DI を使って、分離とレイヤー間の境界を実装します
- Strategy とポリモーフィック/動的ディスパッチには、オブジェクト/Map による検索を使用します
- パターンはシンプルに保ちます
- パターン名にこだわって過剰に設計しないようにします
生成パターン
Abstract factory
具体的なクラスを指定せずに、同じファミリーに属する関連オブジェクトを作成します。たとえば、異なるプラットフォーム向けの UI コンポーネントです。
参照先:https://github.com/HowProgrammingWorks/AbstractFactory
const dataAccess = {
fs: {
createDatabase: (...args) => new FileStorage(...args),
createCursor: (...args) => new FileLineCursor(...args),
},
minio: {
// factories collection for minio
},
// other implementations
};
// Usage
const accessLayer = dataAccess.fs;
const storage = accessLayer.createDatabase('./storage.dat');
const cursor = accessLayer.createCursor({ city: 'Roma' }, storage);
for await (const record of cursor) {
console.dir(record);
}
Builder
複雑で設定可能なオブジェクトを段階的に組み立てます。多くの場合、チェーンを使用します。たとえば、Query Builder や Form Generator です。
参照先:https://github.com/HowProgrammingWorks/Builder
class QueryBuilder {
#options;
constructor(table) {
this.#options = { table, where: {}, limit: null, order: null };
}
where(cond) {
Object.assign(this.#options.where, cond);
return this;
}
order(field) {
this.#options.order = field;
return this;
}
limit(n) {
this.#options.limit = n;
return this;
}
build() {
return { ...this.#options };
}
}
const query = new QueryBuilder('User')
.where({ active: true })
.order('name')
.limit(10)
.build();
別の方法として、await new QueryBuilder で呼び出す非同期コンストラクターを作成するか、すべての手順を次のような宣言的な構造にまとめることができます。
const query = await new QueryBuilder({
entity: 'User',
where: { active: true },
order: 'name',
limit: 10,
});
Factory
リテラルやメソッド、ミックスイン、setPrototypeOf など、さまざまな手法でオブジェクトを作成する関数またはメソッドです。
参照先:https://github.com/HowProgrammingWorks/Factory
const createUser = (name, role) => ({ name, role, createdAt: Date.now() });
const createAdmin = (name) => createUser(name, 'admin');
または、次のようにします。
class Connection {
constructor(url) {
// implementation
}
// implementation
}
const factory = (() => {
let index = 0;
return () => new Connection(`http://10.0.0.1/${index++}`);
})();
Factory Method
インスタンスを作成するために適切な抽象化を選択します。JavaScript では、if、switch、またはコレクション(辞書)からの選択を使って実装できます。
参照先:https://github.com/HowProgrammingWorks/FactoryMethod
class Person {
constructor(name) {
this.name = name;
}
static factory(name) {
return new Person(name);
}
}
class Product {
constructor(value) {
this.field = value;
}
}
class Creator {
factoryMethod(...args) {
return new Product(...args);
}
}
Prototype
リソースを節約するため、準備済みのインスタンスを複製してオブジェクトを作成します。Prototype-programming(Flyweight により近いもの)とは混同しないでください。
参照先:https://github.com/HowProgrammingWorks/PrototypePattern
const proto = { type: 'widget', color: 'blue' };
const clone = () => ({ ...proto });
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
move(x, y) {
this.#x += x;
this.#y += y;
}
clone() {
return new Point(this.#x, this.#y);
}
}
class Line {
#start;
#end;
constructor(start, end) {
this.#start = start;
this.#end = end;
}
move(x, y) {
this.#start.move(x, y);
this.#end.move(x, y);
}
clone() {
const start = this.#start.clone();
const end = this.#end.clone();
return new Line(start, end);
}
}
// Usage
const p1 = new Point(0, 0);
const p2 = new Point(10, 20);
const line = new Line(p1, p2);
const cloned = line.clone();
cloned.move(2, 3);
const point = (x, y) => {
const move = (dx, dy) => {
x += dx;
y += dy;
};
const clone = () => point(x, y);
return { move, clone };
};
// Usage
const { move, clone } = point(10, 20);
const c1 = clone();
move(-5, 10);
Flyweight
複数のインスタンス間で共通の状態を共有し、メモリの割り当てを節約します。
参照先:https://github.com/HowProgrammingWorks/Flyweight
const flyweightPool = new Map();
const getFlyweight = (shared) => {
const { char, font, size } = shared;
const key = `${char}:${font}:${size}`;
let flyweight = flyweightPool.get(key);
if (!flyweight) {
flyweight = Object.freeze({ ...shared });
flyweightPool.set(key, flyweight);
}
return flyweight;
};
const createChar = (char, font, size, row, col) => {
const intrinsic = getFlyweight({ char, font, size });
return { intrinsic, row, col };
};
const a1 = createChar('A', 'Arial', 12, 0, 0);
const a2 = createChar('A', 'Arial', 12, 0, 5);
console.log(a1.intrinsic === a2.intrinsic); // true
Singleton
単一のインスタンスへのグローバルなアクセスを提供します。アンチパターンと見なされることも多く、エクスポートされた参照を ESM/CJS のモジュールキャッシュ経由で利用するのが最も簡単な実装方法です。
参照先:https://github.com/HowProgrammingWorks/Singleton
この状態をインポートする場所がどこであっても、同じコレクションになります
const connections = new Map(); // shared state
const nop = () => {}; // pure function with no state
module.exports = { connections, nop };
class Singleton {
static #instance;
constructor() {
const instance = Singleton.#instance;
if (instance) return instance;
Singleton.#instance = this;
}
}
function Singleton() {
const { instance } = Singleton;
if (instance) return instance;
Singleton.instance = this;
}
const Singleton = new (function () {
const single = this;
return function () {
return single;
};
})();
const Singleton = (() => {
let instance;
class Singleton {
constructor() {
if (instance) return instance;
instance = this;
}
}
return Singleton;
})();
const singleton = (() => {
const instance = {};
return () => instance;
})();
const singleton = (
(instance) => () =>
instance
)({});
Object Pool
頻繁な作成と破棄の際にリソースを節約するため、あらかじめ作成したオブジェクトを再利用します。
参照先:https://github.com/HowProgrammingWorks/Pool
class Pool {
#available = [];
#factory = null;
constructor(factory, size) {
this.#factory = factory;
for (let i = 0; i < size; i++) this.#available.push(factory());
}
acquire() {
return this.#available.pop() || this.#factory();
}
release(obj) {
this.#available.push(obj);
}
}
構造パターン
Adapter
互換性のないインターフェースを互換性のあるものに変換します。これにより、コードを変更せずにサードパーティーのコンポーネントを使用できます。関数の契約をオブジェクトに、またはその逆に変換することもできます。
参照先:https://github.com/HowProgrammingWorks/Adapter
const promisify =
(fn) =>
(...args) =>
new Promise((resolve, reject) => {
fn(...args, (err, data) => (err ? reject(err) : resolve(data)));
});
const timer = new Timer(1000); // wraps setInterval
for await (const step of timer) {
// gives [Symbol.asyncIterator] contract
console.log({ step });
}
class ArrayToQueueAdapter {
#array = null;
constructor(array) {
if (!Array.isArray(array)) {
throw new Error('Array instance expected');
}
this.#array = array;
}
enqueue(data) {
this.#array.push(data);
}
dequeue() {
return this.#array.pop();
}
get count() {
return this.#array.length;
}
}
Wrapper
呼び出しを委譲し、動作を追加する関数ラッパーです。Adapter の特殊なケースです。
参照先:https://github.com/HowProgrammingWorks/Wrapper
const withLogging =
(fn, label) =>
(...args) => {
console.log(`${label} called`, args);
const result = fn(...args);
console.log(`${label} returned`, result);
return result;
};
const wrapInterface = (anInterface) => {
const wrapped = {};
for (const key in anInterface) {
const fn = anInterface[key];
wrapped[key] = wrapFunction(fn);
}
return wrapped;
};
class Timeout {
constructor(fn, msec) {
this.function = fn;
this.timer = setTimeout(() => {
this.timer = null;
}, msec);
}
execute(...args) {
let result = undefined;
if (!this.timer) return result;
clearTimeout(this.timer);
this.timer = null;
result = this.function(...args);
return result;
}
}
Boxing
プリミティブ値をオブジェクト型でラップし、メソッドを追加したりインターフェースを統一したりします。たとえば、String を AddressString に絞り込む場合です。
参照先:https://github.com/HowProgrammingWorks/ADT
Box、Container、または Value-Object の例:
class AddressString {
#value;
constructor(value) {
if (typeof value !== 'string') {
throw new TypeError('Address must be a string');
}
const str = value.trim();
if (str === '') throw new TypeError('Address must be a non-empty');
this.#value = str;
}
get city() {
const index = this.#value.indexOf(',');
return this.#value.slice(0, index).trim();
}
toString() {
return this.#value;
}
valueOf() {
return this.#value;
}
}
// Usage
const address = new AddressString('London, 221B Baker Street');
console.log(address.city); // 'London'
console.log(`Delivery to: ${address}`);
Value-Object の例:
const KMH_TO_MPH = 0.621371;
class SpeedValue {
#kmh;
constructor(value, unit = 'kmh') {
if (typeof value !== 'number') {
throw new TypeError('Speed must be a number');
}
if (value < 0) throw new TypeError('Speed must be non-negative');
if (unit !== 'kmh' && unit !== 'mph') {
throw new TypeError('Unit must be kmh or mph');
}
this.#kmh = unit === 'mph' ? value / KMH_TO_MPH : value;
}
get kmh() {
return this.#kmh;
}
get mph() {
return this.#kmh * KMH_TO_MPH;
}
toString() {
return `${this.#kmh} km/h`;
}
valueOf() {
return this.#kmh;
}
}
Decorator
継承を使わず、通常はコンポジションと宣言的な構文によって動作を動的に拡張します。実質的にメタデータを追加します。
参照先:https://github.com/HowProgrammingWorks/Decorator
const add = (a, b) => a + b;
const loggedAdd = withLogging(add, 'add');
JavaScript/TypeScript で従来型のデコレーターを使うのは、あまりよい方法ではありません
const validator = new MinLengthValidator(
new EmailValidator(new RequiredValidator(new BaseValidator())),
20,
);
const input = '[email protected]';
const result = validator.validate(input);
メタデータは、JS らしい方法でいくつかの方法によりインスタンスに追加できます。
インスタンスの Symbol プロパティ:
const VALIDATOR_META = Symbol('validatorMeta');
class EmailValidator {
constructor(next) {
this.next = next;
this[VALIDATOR_META] = { type: 'email' };
}
}
独自プロパティとしてメタデータを付与するラッパー。
JSDoc アノテーション(実行時コストはゼロ):
/**
* @typedef {{ type: string, label: string }} ValidatorMeta
* @typedef {{ validate(v: string): boolean, meta: ValidatorMeta }} Validator
*/
/** @type {Validator} */
const validator = Object.assign(
new EmailValidator(new RequiredValidator(new BaseValidator())),
{ meta: { type: 'email', label: 'Email field' } },
);
モジュール内の WeakMap(完全にプライベートで、オブジェクトの形状を汚しません):
const metadata = new WeakMap();
const createValidator = ({ args, meta }) => {
const instance = new Validator(...args);
metadata.set(instance, meta);
return instance;
};
module.exports = { createValidator };
プロキシ
呼び出し、読み取り、書き込みを横取りしてオブジェクトへのアクセスを制御します。遅延初期化、キャッシュ、セキュリティに有用です。GoF パターンまたは JavaScript のネイティブな Proxy で実装できます。
参考:https://github.com/HowProgrammingWorks/Proxy
開発者から直接求められない限り、組み込みの JS Proxy クラスは使用しないでください。最適化が解除されるためです。代わりに、追加の振る舞いを備えたコンテナとして GoF の Proxy を使用してください。
const fs = require('node:fs');
const statistics = { bytes: 0, chunks: 0, events: {} };
class StatReadStream extends fs.ReadStream {
emit(name, data) {
if (name === 'data') {
statistics.bytes += data.length;
statistics.chunks++;
}
const counter = statistics.events[name] || 0;
statistics.events[name] = counter + 1;
super.emit(name, data);
}
}
const getStatistics = () => structuredClone(statistics);
const createReadStream = (path, options) => new StatReadStream(path, options);
module.exports = { ...fs, createReadStream, getStatistics };
ブリッジ
コンポジションまたは集約によって、2 つ以上の抽象化階層を分離し、それぞれを独立して進化できるようにします。
参考:https://github.com/HowProgrammingWorks/Bridge
class CommunicationProtocol {
sendCommand(device, command) {
console.log({ device, command });
throw new Error('sendCommand() must be implemented');
}
}
class MQTTProtocol extends CommunicationProtocol {
sendCommand(device, command) {
console.log(`[MQTT] Sending '${command}' to ${device}`);
}
}
class HTTPProtocol extends CommunicationProtocol {
sendCommand(device, command) {
console.log(`[HTTP] Sending '${command}' to ${device}`);
}
}
class IoTDevice {
constructor(name, protocol) {
this.name = name;
this.protocol = protocol;
}
operate(command) {
this.protocol.sendCommand(this.name, command);
}
}
class SmartLight extends IoTDevice {
turnOn() {
this.operate('Turn On Light');
}
turnOff() {
this.operate('Turn Off Light');
}
}
class SmartThermostat extends IoTDevice {
setTemperature(temp) {
this.operate(`Set Temperature to ${temp}°C`);
}
}
コンポジット
共通インターフェースを実装し、個々のオブジェクトとそのツリー構造を一様に扱います。例:DOM やファイルシステム。
参考:https://github.com/HowProgrammingWorks/Composite
reduce を使った再帰的なコンポジット:
const calculateTotal = (order) => {
const items = Array.isArray(order) ? order : Object.values(order);
return items.reduce((sum, item) => {
if (typeof item.price === 'number') return sum + item.price;
else return sum + calculateTotal(item);
}, 0);
};
ネストされたコンポジット構造としての JSON:
const json = `{
"electronics": {
"laptop": { "price": 1200 },
"accessories": {
"mouse": { "price": 25 },
"keyboard": { "price": 75 }
}
},
"books": {
"fiction": { "price": 15 },
"technical": { "price": 60 }
}
}`;
const order = JSON.parse(json);
console.log(calculateTotal(order));
ファサード
複雑なシステムへのアクセスを簡素化し、統一されたわかりやすいインターフェースを提供することで、内部の複雑さを隠し、保護します。
参考:https://github.com/HowProgrammingWorks/Facade
const createApi = (db, cache, logger) => ({
async getUser(id) {
const cached = cache.get(id);
if (cached) return cached;
const user = await db.row('User', ['*'], { id });
cache.set(id, user);
logger.info(`User ${id} loaded`);
return user;
},
});
Timer、Logger、Task などのクラスを隠します:
scheduler.task('name2', '2019-03-12T14:37Z', (done) => {
setTimeout(() => {
done(new Error('task failed'));
}, 1100);
});
コンテキスト
共通の環境を共有しない異なるコンポーネント(抽象化、モジュール、レイヤー)間で、密結合を避けながら状態と依存関係を受け渡します。
参考:https://github.com/HowProgrammingWorks/Context
class AccountService {
constructor(context) {
this.context = context;
}
getBalance(accountId) {
const { console, accessPolicy, user } = this.context;
console.log(`User ${user.name} requesting balance for ${accountId}`);
if (!accessPolicy.check(user.role, 'read:balance')) {
console.error('Access denied: insufficient permissions');
return null;
}
const balance = 15420.5;
console.log('Access granted');
return balance;
}
}
class AccessPolicy {
constructor() {
this.permissions = {
admin: ['read:balance', 'read:transactions', 'write:transactions'],
user: ['read:balance'],
guest: [],
};
}
check(role, permission) {
return this.permissions[role]?.includes(permission);
}
}
class User {
constructor(name, role) {
this.name = name;
this.role = role;
}
}
const accessPolicy = new AccessPolicy();
const user = new User('Marcus', 'admin');
const context = { console, accessPolicy, user };
const accountService = new AccountService(context);
const balance = accountService.getBalance('Account-123');
console.log(`Balance = $${balance}`);
クロージャーを使った別の方法:
const createAccountService = (context) => {
const { console, accessPolicy, user } = context;
const getBalance = (accountId) => {
console.log(`User ${user.name} requesting balance for ${accountId}`);
if (!accessPolicy.check(user.role, 'read:balance')) {
console.error('Access denied: insufficient permissions');
return null;
}
return 15420.5;
};
const getTransactions = (accountId) => {
if (!accessPolicy.check(user.role, 'read:transactions')) {
console.error('Access denied: insufficient permissions');
return null;
}
console.log(`User ${user.name} reading transactions for ${accountId}`);
return ['TR-123', 'TR-456', 'TR-789'];
};
return { getBalance, getTransactions };
};
const accessPolicy = {
permissions: {
admin: ['read:balance', 'read:transactions', 'write:transactions'],
user: ['read:balance'],
guest: [],
},
check: (role, permission) =>
accessPolicy.permissions[role]?.includes(permission),
};
const context = {
console,
accessPolicy,
user: { name: 'Marcus', role: 'admin' },
};
const accountService = createAccountService(context);
console.log('Balance:', accountService.getBalance('ACC-001'));
console.log('Transactions:', accountService.getTransactions('ACC-001'));