エンジニアとしての強み
各カテゴリの詳細を確認できます
ソフトウェアアーキテクチャ
私が設計・実装するソフトウェアアーキテクチャの概要と構成要素について紹介します。
責務の明確な分離
各コンポーネントは明確な責務を持ち、単一責任の原則に従って設計されています。これにより、コードの可読性、保守性、テスト容易性が向上します。
変更頻度に基づく分類
構成要素は変更頻度(温度)によって分類されており、変更の影響範囲を最小限に抑える設計となっています。低温の要素はドメインルールやインフラストラクチャなど、変更頻度が低く安定した部分です。
フロントエンドとバックエンドの明確な境界
フロントエンド、バックエンド、およびその境界となる要素が明確に定義されており、各層の責務が明確です。これにより、フロントエンドとバックエンドの開発を並行して進めることができます。
スキーマ駆動開発
Resource、DomainSchema、Client-Resourceなどのスキーマ定義を中心に開発を進めることで、型安全性を確保し、バグの早期発見と開発効率の向上を実現しています。
アーキテクチャ構成要素
アーキテクチャを構成する各要素の詳細情報と実装例です。各要素は明確な責務を持ち、単一責任の原則に従って設計されています。
Page
フォルダ
app
責務
エンドポイントに配置する画面コンポーネントと、SSR(サーバーサイドレンダリング)時の認証/認可、初期データ取得制御を行う
オブジェクトの送信イベント
- 初期データの転送タイミング: 初期表示送信先: Featureオブジェクト: Resourceのレスポンス
実装例
// app/games/memory/page.tsx
import { getServerSession } from "next-auth"
import { redirect } from "next/navigation"
import { authOptions } from "@/infrastructure/auth"
import { MemoryGameFeature } from "@/features/memory-game/memory-game-feature"
import { trpc } from "@/infrastructure/trpc/server"
export default async function MemoryGamePage() {
// 認証チェック
const session = await getServerSession(authOptions)
if (!session) {
redirect("/auth/signin")
}
// 初期データ取得
const initialGameState = await trpc.memoryGame.getInitialState.query({
userId: session.user.id,
})
// Featureコンポーネントに初期データを渡す
return <MemoryGameFeature initialState={initialGameState} />
}
Batch
フォルダ
batch
責務
バッチ処理メソッドの定義
オブジェクトの送信イベント
- バッチの実行タイミング: バッチの実行送信先: Infrastructureオブジェクト: -
実装例
// batch/leaderboard-update.ts
import { db } from "@/infrastructure/db"
import { games, userStats, leaderboard } from "@/infrastructure/db/schema"
import { eq, desc, and, lt, gt } from "drizzle-orm"
import { sql } from "drizzle-orm/sql"
export async function updateDailyLeaderboard() {
// 昨日の日付範囲を計算
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
yesterday.setHours(0, 0, 0, 0)
const today = new Date()
today.setHours(0, 0, 0, 0)
// 昨日完了したゲームの集計
const dailyResults = await db.execute(
sql`
SELECT
"userId",
MAX("score") as "highScore",
AVG("score") as "avgScore",
COUNT(*) as "gamesPlayed"
FROM ${games}
WHERE
"gameType" = 'memory' AND
"status" = 'completed' AND
"completedAt" >= ${yesterday} AND
"completedAt" < ${today}
GROUP BY "userId"
ORDER BY "highScore" DESC
LIMIT 100
`
)
// 日次ランキングテーブルの更新
for (const result of dailyResults) {
await db.insert(leaderboard)
.values({
userId: result.userId,
score: result.highScore,
gameType: "memory",
period: "daily",
metadata: {
avgScore: result.avgScore,
gamesPlayed: result.gamesPlayed,
date: yesterday.toISOString().split('T')[0]
}
})
}
return {
success: true,
processedCount: dailyResults.length,
date: yesterday.toISOString().split('T')[0]
}
}
Component
フォルダ
components
責務
特定の画面に限らない画面コンポーネントの定義
実装例
// components/game-card.tsx
"use client"
import { useState, useEffect } from "react"
import { Card } from "@/components/ui/card"
import { cn } from "@/lib/utils"
interface GameCardProps {
id: string
value: string
isFlipped: boolean
isMatched: boolean
onClick: () => void
disabled?: boolean
}
export function GameCard({
id,
value,
isFlipped,
isMatched,
onClick,
disabled = false,
}: GameCardProps) {
const [isFlippedState, setIsFlippedState] = useState(isFlipped)
// 外部のisFlippedプロパティが変更されたら内部状態も更新
useEffect(() => {
setIsFlippedState(isFlipped)
}, [isFlipped])
// マッチした場合のスタイル
const matchedStyle = isMatched
? "bg-green-100 dark:bg-green-900/20 border-green-300 dark:border-green-800"
: ""
// カードクリック時の処理
const handleClick = () => {
if (disabled || isFlippedState || isMatched) return
onClick()
}
return (
<div
className={cn(
"relative w-full h-full perspective-1000 cursor-pointer",
disabled ? "opacity-70 cursor-not-allowed" : "hover:scale-105",
)}
onClick={handleClick}
>
{/* カード内容 */}
</div>
)
}
Data
フォルダ
data
責務
ビルド/デプロイよりも変更頻度が低く、インメモリで管理するデータの定義
実装例
// data/game-achievements.ts
import { z } from "zod"
import { achievementSchema } from "@/domain/schema/achievement"
// 実績データの型
const achievementDataSchema = z.array(
achievementSchema.extend({
id: z.string(),
name: z.string(),
description: z.string(),
icon: z.string(),
requirement: z.record(z.any()),
points: z.number(),
rarity: z.enum(["common", "uncommon", "rare", "epic", "legendary"]),
})
)
type AchievementData = z.infer<typeof achievementDataSchema>
// メモリーゲームの実績データ
export const memoryGameAchievements: AchievementData = [
{
id: "memory_first_win",
name: "初めての勝利",
description: "メモリーゲームを初めてクリアする",
icon: "🏆",
gameType: "memory",
requirement: {
gamesCompleted: 1
},
points: 5,
rarity: "common"
},
{
id: "memory_perfect_game",
name: "パーフェクトマッチ",
description: "ミスなしでゲームをクリアする",
icon: "✨",
gameType: "memory",
requirement: {
mistakeCount: 0
},
points: 20,
rarity: "rare"
}
]
DomainRule
フォルダ
domain/rules
責務
ドメインルールの定義
実装例
// domain/rules/memory-game-rules.ts
import { nanoid } from "nanoid"
import type { Card } from "@/resource/memory-game-resource"
// カードの値のセット
const cardValues = ["🍎", "🍌", "🍇", "🍊", "🍓", "🍉", "🍍", "🥝", "🥥", "🍒", "🥭", "🍑"]
// カードをシャッフルする関数
function shuffleArray<T>(array: T[]): T[] {
const newArray = [...array]
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[newArray[i], newArray[j]] = [newArray[j], newArray[i]]
}
return newArray
}
// カードを生成する関数
export function generateCards(pairCount = 8): Card[] {
// 使用する値を選択
const selectedValues = cardValues.slice(0, pairCount)
// 各値のペアを作成
const cards: Card[] = []
for (const value of selectedValues) {
cards.push(
{ id: nanoid(), value, isFlipped: false, isMatched: false },
{ id: nanoid(), value, isFlipped: false, isMatched: false }
)
}
// カードをシャッフル
return shuffleArray(cards)
}
Feature
フォルダ
features
責務
エンドポイントに対応した画面コンポーネントの定義
オブジェクトの送信イベント
- 初期データの転送タイミング: 初期表示送信先: Hookオブジェクト: Client-Resource
- 状態とハンドラの転送タイミング: -送信先: Viewオブジェクト: Client-Resource、関数
実装例
// features/memory-game/memory-game-feature.tsx
"use client"
import { useMemoryGame } from "@/hooks/use-memory-game"
import { MemoryGameView } from "@/views/memory-game-view"
import type { MemoryGameInitialState } from "@/resource/memory-game-resource"
interface MemoryGameFeatureProps {
initialState: MemoryGameInitialState
}
export function MemoryGameFeature({ initialState }: MemoryGameFeatureProps) {
// Hookを使用してロジックと状態を管理
const {
gameState,
cards,
selectedCards,
matchedPairs,
isGameOver,
score,
handleCardClick,
handleRestart,
} = useMemoryGame(initialState)
// Viewコンポーネントに状態とハンドラを渡す
return (
<MemoryGameView
cards={cards}
selectedCards={selectedCards}
matchedPairs={matchedPairs}
isGameOver={isGameOver}
score={score}
onCardClick={handleCardClick}
onRestart={handleRestart}
/>
)
}
Hook
フォルダ
hooks
責務
フロントエンドロジックの定義
実装例
// hooks/use-memory-game.ts
"use client"
import { useState, useEffect, useCallback } from "react"
import { trpc } from "@/infrastructure/trpc/client"
import { pusherClient } from "@/infrastructure/pusher/client"
import type { MemoryGameInitialState, Card } from "@/resource/memory-game-resource"
import type { MemoryGameState } from "@/client-resource/memory-game-state"
export function useMemoryGame(initialState: MemoryGameInitialState) {
// クライアント状態の初期化
const [gameState, setGameState] = useState<MemoryGameState>({
cards: initialState.cards,
selectedCards: [],
matchedPairs: [],
isGameOver: false,
score: initialState.score,
})
// tRPCクライアントの設定
const saveScore = trpc.memoryGame.saveScore.useMutation()
// カード選択ハンドラ
const handleCardClick = useCallback((card: Card) => {
if (
gameState.selectedCards.length === 2 ||
gameState.selectedCards.some(c => c.id === card.id) ||
gameState.matchedPairs.includes(card.value)
) {
return
}
setGameState(prev => ({
...prev,
selectedCards: [...prev.selectedCards, card],
}))
}, [gameState.selectedCards, gameState.matchedPairs])
// 他のロジックとハンドラ...
return {
...gameState,
handleCardClick,
handleRestart,
}
}
Infrastructure
フォルダ
infrastructure
責務
ライブラリ、フレームワークのWrapperの定義
実装例
// infrastructure/db/index.ts
import { drizzle } from "drizzle-orm/postgres-js"
import postgres from "postgres"
import * as schema from "./schema"
import { env } from "@/infrastructure/env"
// PostgreSQLクライアントの初期化
const connectionString = env.DATABASE_URL
const client = postgres(connectionString)
export const db = drizzle(client, { schema })
// infrastructure/trpc/router.ts
import { initTRPC, TRPCError } from "@trpc/server"
import { getServerSession } from "next-auth"
import { authOptions } from "@/infrastructure/auth"
import superjson from "superjson"
// tRPCの初期化
const t = initTRPC.create({
transformer: superjson,
})
// ミドルウェア: 認証チェック
const isAuthenticated = t.middleware(async ({ next, ctx }) => {
const session = await getServerSession(authOptions)
if (!session || !session.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "認証が必要です",
})
}
return next({
ctx: {
...ctx,
session,
},
})
})
Logic
フォルダ
logics
責務
バックエンドロジックの定義
オブジェクトの送信イベント
- 外部データへのアクセスタイミング: 外部データへのアクセス送信先: Repositoryオブジェクト: -
- フロントエンドへの転送タイミング: フロントエンドへの転送送信先: Hookオブジェクト: Resourceのメッセージ
実装例
// logics/memory-game-logic.ts
import { memoryGameRepository } from "@/repository/memory-game-repository"
import { memoryGameTrigger } from "@/trigger/memory-game-trigger"
import { generateCards } from "@/domain/rules/memory-game-rules"
import type { Card } from "@/resource/memory-game-resource"
export const memoryGameLogic = {
// 初期状態取得
async getInitialState(userId: string) {
// 既存のゲーム状態を取得
const existingGame = await memoryGameRepository.getGameByUserId(userId)
if (existingGame && !existingGame.isCompleted) {
return {
userId,
cards: existingGame.cards,
score: existingGame.score,
}
}
// 新しいゲームを作成
const cards = generateCards()
await memoryGameRepository.createGame(userId, cards)
return {
userId,
cards,
score: 0,
}
},
// スコア保存
async saveScore(userId: string, score: number) {
// ゲームを完了状態に更新
await memoryGameRepository.updateGame(userId, {
score,
isCompleted: true,
completedAt: new Date(),
})
// 他のロジック...
return { success: true }
}
}
Repository
フォルダ
repository
責務
外部データへのアクセスメソッドの定義
オブジェクトの送信イベント
- インフラへのアクセスタイミング: インフラへのアクセス送信先: Infrustructureオブジェクト: -
実装例
// repository/memory-game-repository.ts
import { db } from "@/infrastructure/db"
import { games, userStats, leaderboard } from "@/infrastructure/db/schema"
import { eq, desc, and, gt } from "drizzle-orm"
import type { Card } from "@/resource/memory-game-resource"
export const memoryGameRepository = {
// ゲーム取得
async getGameByUserId(userId: string) {
const result = await db.query.games.findFirst({
where: and(
eq(games.userId, userId),
eq(games.gameType, "memory")
),
orderBy: [desc(games.createdAt)]
})
return result
},
// ゲーム作成
async createGame(userId: string, cards: Card[]) {
await db.insert(games).values({
userId,
gameType: "memory",
status: "in_progress",
startedAt: new Date(),
metadata: { cards },
score: 0,
})
},
// 他のメソッド...
}
Service
フォルダ
services
責務
画面ごとのtRPCルーター定義
オブジェクトの送信イベント
- バックエンドロジック実行タイミング: バックエンドロジック実行送信先: Logicオブジェクト: リクエストスキーマ
実装例
// services/memory-game-service.ts
import { z } from "zod"
import { router, publicProcedure, protectedProcedure } from "@/infrastructure/trpc/router"
import { memoryGameLogic } from "@/logics/memory-game-logic"
import { memoryGameSchema } from "@/resource/memory-game-resource"
export const memoryGameRouter = router({
// 初期状態取得
getInitialState: protectedProcedure
.input(z.object({
userId: z.string(),
}))
.query(async ({ input, ctx }) => {
return memoryGameLogic.getInitialState(input.userId)
}),
// スコア保存
saveScore: protectedProcedure
.input(z.object({
score: z.number(),
}))
.mutation(async ({ input, ctx }) => {
const userId = ctx.session.user.id
return memoryGameLogic.saveScore(userId, input.score)
}),
// ゲームリスタート
restartGame: protectedProcedure
.mutation(async ({ ctx }) => {
const userId = ctx.session.user.id
return memoryGameLogic.restartGame(userId)
}),
// ランキング取得
getLeaderboard: publicProcedure
.query(async () => {
return memoryGameLogic.getLeaderboard()
}),
})
View
フォルダ
views
責務
画面UIの構成定義
実装例
// views/memory-game-view.tsx
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { type Card as CardType } from "@/resource/memory-game-resource"
import { Trophy } from 'lucide-react'
interface MemoryGameViewProps {
cards: CardType[]
selectedCards: CardType[]
matchedPairs: string[]
isGameOver: boolean
score: number
onCardClick: (card: CardType) => void
onRestart: () => void
}
export function MemoryGameView({
cards,
selectedCards,
matchedPairs,
isGameOver,
score,
onCardClick,
onRestart,
}: MemoryGameViewProps) {
return (
<div className="container py-8 max-w-4xl">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">神経衰弱ゲーム</h1>
<div className="flex items-center gap-4">
<div className="text-xl font-semibold">スコア: {score}</div>
<Button onClick={onRestart}>リスタート</Button>
</div>
</div>
{isGameOver ? (
<div className="flex flex-col items-center justify-center p-8 bg-primary/10 rounded-lg">
<Trophy className="h-16 w-16 text-primary mb-4" />
<h2 className="text-2xl font-bold mb-2">ゲームクリア!</h2>
<p className="text-xl mb-4">最終スコア: {score}</p>
<Button onClick={onRestart} size="lg">
もう一度プレイ
</Button>
</div>
) : (
<div className="grid grid-cols-4 md:grid-cols-6 gap-4">
{/* カードのレンダリング */}
</div>
)}
</div>
)
}
Trigger
フォルダ
trigger
責務
画面ごとのサーバープッシュの定義
オブジェクトの送信イベント
- フロントエンドへの転送タイミング: フロントエンドへの転送送信先: Hookオブジェクト: Resourceのメッセージ
実装例
// trigger/memory-game-trigger.ts
import { pusher } from "@/infrastructure/pusher/server"
import type { Card } from "@/resource/memory-game-resource"
export const memoryGameTrigger = {
// 特定ユーザーのゲーム状態更新
async updateGameState(userId: string, data: {
cards?: Card[],
score?: number,
isGameOver?: boolean,
matchedPairs?: string[]
}) {
await pusher.trigger(
`memory-game-${userId}`,
"game-update",
{
type: "game-update",
...data
}
)
},
// 全ユーザーへのリーダーボード更新通知
async broadcastLeaderboardUpdate(leaderboard: any[]) {
await pusher.trigger(
"memory-game-leaderboard",
"leaderboard-update",
{
type: "leaderboard-update",
leaderboard
}
)
}
}
Client-Resource
フォルダ
client-resource
責務
クライアント状態のスキーマの定義
実装例
// client-resource/memory-game-state.ts
import { z } from "zod"
// カードの型定義
export const cardSchema = z.object({
id: z.string(),
value: z.string(),
isFlipped: z.boolean().default(false),
isMatched: z.boolean().default(false),
})
export type Card = z.infer<typeof cardSchema>
// メモリーゲームのクライアント状態スキーマ
export const memoryGameStateSchema = z.object({
cards: z.array(cardSchema),
selectedCards: z.array(cardSchema).default([]),
matchedPairs: z.array(z.string()).default([]),
isGameOver: z.boolean().default(false),
score: z.number().default(0),
})
export type MemoryGameState = z.infer<typeof memoryGameStateSchema>
DomainSchema
フォルダ
domain/schema
責務
ドメインオブジェクトのスキーマ定義
実装例
// domain/schema/game.ts
import { z } from "zod"
// ゲームの状態を表すスキーマ
export const gameStatusSchema = z.enum([
"waiting",
"in_progress",
"completed",
"cancelled"
])
export type GameStatus = z.infer<typeof gameStatusSchema>
// ゲームのドメインスキーマ
export const gameSchema = z.object({
id: z.string().uuid(),
userId: z.string(),
gameType: z.string(),
status: gameStatusSchema,
startedAt: z.date().nullable(),
completedAt: z.date().nullable(),
score: z.number().default(0),
metadata: z.record(z.any()).optional(),
})
export type Game = z.infer<typeof gameSchema>
// ユーザーの統計情報スキーマ
export const userStatsSchema = z.object({
userId: z.string(),
gamesPlayed: z.number().default(0),
gamesWon: z.number().default(0),
totalScore: z.number().default(0),
highScore: z.number().default(0),
averageScore: z.number().default(0),
lastPlayedAt: z.date().nullable(),
})
export type UserStats = z.infer<typeof userStatsSchema>
Resource
フォルダ
resource
責務
フロントエンド/バックエンド間でやり取りされるオブジェクトのスキーマ定義
実装例
// resource/memory-game-resource.ts
import { z } from "zod"
// カードのスキーマ
export const cardSchema = z.object({
id: z.string(),
value: z.string(),
isFlipped: z.boolean().optional(),
isMatched: z.boolean().optional(),
})
export type Card = z.infer<typeof cardSchema>
// リクエストスキーマ
export const getInitialStateRequestSchema = z.object({
userId: z.string(),
})
export type GetInitialStateRequest = z.infer<typeof getInitialStateRequestSchema>
export const saveScoreRequestSchema = z.object({
score: z.number(),
})
export type SaveScoreRequest = z.infer<typeof saveScoreRequestSchema>
// レスポンススキーマ
export const memoryGameInitialStateSchema = z.object({
userId: z.string(),
cards: z.array(cardSchema),
score: z.number().default(0),
})
export type MemoryGameInitialState = z.infer<typeof memoryGameInitialStateSchema>
export const gameResultSchema = z.object({
success: z.boolean(),
message: z.string().optional(),
})
export type GameResult = z.infer<typeof gameResultSchema>