エンジニアとしての強み
各カテゴリの詳細を確認できます
疎結合
コンポーネント間の依存関係を最小限に抑え、変更の影響範囲を限定するための設計手法について紹介します。
インフラの仕様への依存を吸収する
インフラストラクチャの違いによる影響を最小化する
例:ID、日付、文字コード、数値の範囲等において、バックエンド、データベース言語によって仕様が異なることは多いです。インフラ層に変換を集約することで各々の間の多くの依存を吸収することができます。
実装例:日付形式の変換
// インフラ層での日付変換
// infrastructure/date-converter.ts
export function toISOString(date: Date): string {
return date.toISOString();
}
export function fromDatabaseDate(dbDate: string): Date {
// データベース固有の日付形式を JavaScript の Date に変換
return new Date(dbDate);
}
export function toDatabaseDate(date: Date): string {
// JavaScript の Date をデータベース固有の形式に変換
return date.toISOString().slice(0, 19).replace('T', ' ');
}
// 使用例
// repository/user-repository.ts
import { fromDatabaseDate, toDatabaseDate } from '../infrastructure/date-converter';
export async function getUserLastLogin(userId: string) {
const result = await db.query('SELECT last_login FROM users WHERE id = ?', [userId]);
// データベースから取得した日付を変換
return result.length > 0 ? fromDatabaseDate(result[0].last_login) : null;
}
export async function updateUserLastLogin(userId: string, lastLogin: Date) {
// JavaScript の Date をデータベース形式に変換
await db.query('UPDATE users SET last_login = ? WHERE id = ?', [toDatabaseDate(lastLogin), userId]);
}
インターフェースの挿入
抽象化によるコンポーネント間の結合度低減
インターフェースにはオブジェクトの振る舞いに制約を与える効果があります。具体的には以下のような利点があります:
- 必要なメソッドを定めておくことで、依存されるコンポーネントの変更頻度を低下させる
- 参照元において抽象的にメソッドを扱うことができ(ポリモーフィズム)、インターフェース上同一な変更であれば参照元のソース変更の必要なく、コードを変更できる
これらによって変更の波及を吸収することができます。
注意点
インターフェースの過度の設定は必要な変更が行えず、インターフェースに合わせるために、モデリングの観点で人工的な実装を増加させる可能性があります。
実装例:支払い処理のインターフェース
// インターフェース定義
// interfaces/payment-processor.ts
export interface PaymentProcessor {
processPayment(amount: number, currency: string, paymentDetails: any): Promise<PaymentResult>;
refundPayment(transactionId: string, amount: number): Promise<RefundResult>;
}
export interface PaymentResult {
success: boolean;
transactionId?: string;
errorMessage?: string;
}
export interface RefundResult {
success: boolean;
refundId?: string;
errorMessage?: string;
}
// 具体的な実装(Stripe)
// services/stripe-payment-processor.ts
import { PaymentProcessor, PaymentResult, RefundResult } from '../interfaces/payment-processor';
import Stripe from 'stripe';
export class StripePaymentProcessor implements PaymentProcessor {
private stripe: Stripe;
constructor(apiKey: string) {
this.stripe = new Stripe(apiKey, { apiVersion: '2023-10-16' });
}
async processPayment(amount: number, currency: string, paymentDetails: any): Promise<PaymentResult> {
try {
const paymentIntent = await this.stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Stripe uses cents
currency,
payment_method: paymentDetails.paymentMethodId,
confirm: true,
});
return {
success: paymentIntent.status === 'succeeded',
transactionId: paymentIntent.id,
};
} catch (error) {
return {
success: false,
errorMessage: error.message,
};
}
}
async refundPayment(transactionId: string, amount: number): Promise<RefundResult> {
try {
const refund = await this.stripe.refunds.create({
payment_intent: transactionId,
amount: Math.round(amount * 100), // Stripe uses cents
});
return {
success: refund.status === 'succeeded',
refundId: refund.id,
};
} catch (error) {
return {
success: false,
errorMessage: error.message,
};
}
}
}
// 具体的な実装(PayPal)
// services/paypal-payment-processor.ts
import { PaymentProcessor, PaymentResult, RefundResult } from '../interfaces/payment-processor';
import { PayPalClient } from '@paypal/checkout-server-sdk';
export class PayPalPaymentProcessor implements PaymentProcessor {
private client: PayPalClient;
constructor(clientId: string, clientSecret: string) {
// PayPal クライアント初期化コード
this.client = new PayPalClient(clientId, clientSecret);
}
async processPayment(amount: number, currency: string, paymentDetails: any): Promise<PaymentResult> {
// PayPal 固有の実装
// ...
}
async refundPayment(transactionId: string, amount: number): Promise<RefundResult> {
// PayPal 固有の実装
// ...
}
}
// 使用例
// controllers/checkout-controller.ts
import { PaymentProcessor } from '../interfaces/payment-processor';
export class CheckoutController {
constructor(private paymentProcessor: PaymentProcessor) {}
async checkout(req, res) {
const { amount, currency, paymentDetails } = req.body;
const result = await this.paymentProcessor.processPayment(amount, currency, paymentDetails);
if (result.success) {
// 支払い成功の処理
return res.status(200).json({ success: true, transactionId: result.transactionId });
} else {
// 支払い失敗の処理
return res.status(400).json({ success: false, error: result.errorMessage });
}
}
}
不要なメソッドを加えない
最小限のインターフェースで結合度を下げる
使用しないgetterやsetterを加えることでオブジェクトの「表現」に依存する設計となります。
例:
物理的な物体の位置は座標で表せますが、その表現が直交座標もしくは極座標であるかは対象のオブジェクトによるものではなく、実装上の都合です。アプリケーション上関心があるのが物体間の距離のみであるとすれば、dist(Object)メソッドのみ公開すればよいでしょう。しかし例えばgetX,getYを公開していた場合、まずdistに相当する処理がいたるところで実装されるリスクが高まり、その場合、極座標系への変更時にはその参照元のソースも影響範囲となってしまいます。
実装例:最小限のインターフェース
// 悪い例:実装の詳細を露出させる
class Position {
private x: number;
private y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
// 実装の詳細を露出させるゲッター
getX(): number {
return this.x;
}
getY(): number {
return this.y;
}
}
// 使用例(悪い例)
function calculateDistance(pos1: Position, pos2: Position): number {
// 実装の詳細(直交座標系)に依存
const dx = pos2.getX() - pos1.getX();
const dy = pos2.getY() - pos1.getY();
return Math.sqrt(dx * dx + dy * dy);
}
// 良い例:必要な操作のみを公開する
class Position {
private x: number;
private y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
// 必要な操作のみを公開
distanceTo(other: Position): number {
const dx = other.x - this.x;
const dy = other.y - this.y;
return Math.sqrt(dx * dx + dy * dy);
}
// 内部表現を変更しても、公開インターフェースは変わらない
// 例えば極座標系に変更しても、distanceTo メソッドの動作は同じ
}
// 使用例(良い例)
function calculateDistance(pos1: Position, pos2: Position): number {
// 実装の詳細に依存せず、必要な操作のみを使用
return pos1.distanceTo(pos2);
}
Partialオブジェクト、オプショナル属性を濫用しない
型安全性を維持するための原則
特にTypescriptでは、nullである可能性やundefinedである可能性をエディタが警告してくるので、全てのフィールドにおいてそれらを含むことを許すPartial型を使いたくなる欲求が生じうります。
例外:
- デフォルト値がドメインで定められたFactory関数の引数
- nullable
実装例:Partial型の適切な使用
// ユーザーモデル
interface User {
id: string;
name: string;
email: string;
age: number;
createdAt: Date;
updatedAt: Date;
}
// 悪い例:Partial型の濫用
function updateUser(userId: string, userData: Partial<User>): Promise<User> {
// userData のどのフィールドも undefined かもしれないため、
// 型安全性が損なわれる
return db.users.update({ where: { id: userId }, data: userData });
}
// 良い例:更新に必要なフィールドのみを定義した型
interface UserUpdateData {
name?: string;
email?: string;
age?: number;
}
function updateUser(userId: string, userData: UserUpdateData): Promise<User> {
// 更新可能なフィールドのみを明示的に定義
return db.users.update({ where: { id: userId }, data: userData });
}
// Factory関数での適切な使用例
interface UserCreateOptions {
name: string;
email: string;
age?: number; // オプショナルだが、明示的に定義
}
function createUser(options: UserCreateOptions): User {
const now = new Date();
return {
id: generateId(),
name: options.name,
email: options.email,
age: options.age ?? 0, // デフォルト値を設定
createdAt: now,
updatedAt: now,
};
}
技術的命名のトップレベル(他ファイルから参照可能)での使用を避ける
ドメイン言語を優先した命名規則
アプリケーションの実現にどのような処理方法を取るのかはあくまで技術的な関心にすぎません。特にそのコンポーネントで責務を集約しているものであれば、外部にその影響が及ぶことは不必要な結合を生み出すこととなります。
実装例:技術的命名と意味的命名
// 悪い例:技術的な実装の詳細が名前に現れている
export class UserRedisRepository {
async getHashMapUserById(id: string): Promise<User> {
// Redis のハッシュマップから取得する実装
}
async setUserInHashMap(user: User): Promise<void> {
// Redis のハッシュマップに保存する実装
}
}
// 使用例(悪い例)
const userRepo = new UserRedisRepository();
const user = await userRepo.getHashMapUserById('123');
await userRepo.setUserInHashMap(user);
// 良い例:技術的な詳細を隠蔽し、ドメイン言語を使用
export interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
}
// 実装クラスは内部で技術的な詳細を扱う
export class RedisUserRepository implements UserRepository {
async findById(id: string): Promise<User> {
// Redis のハッシュマップから取得する実装
}
async save(user: User): Promise<void> {
// Redis のハッシュマップに保存する実装
}
}
// 使用例(良い例)
const userRepo: UserRepository = new RedisUserRepository();
const user = await userRepo.findById('123');
await userRepo.save(user);
設計思想カテゴリ
他のカテゴリを見る