課題9: 構造体を使ったユーザー管理システム
課題概要
構造体を使って、ユーザー管理システムを実装します。構造体の定義、埋め込み、JSONシリアライゼーション、バリデーション、CRUDオペレーションを学びます。
マンダトリー要件
要件1: ユーザーモデル
ユーザーとアドレスの構造体を定義してください。
ファイル: models/user.go
package models
import (
"encoding/json"
"time"
)
type Address struct {
Street string `json:"street"`
City string `json:"city"`
State string `json:"state"`
ZipCode string `json:"zip_code"`
Country string `json:"country"`
}
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"` // JSONに含めない
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age,omitempty"`
Address *Address `json:"address,omitempty"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// NewUser はユーザーを作成
func NewUser(username, email, password string) (*User, error) {
// TODO: 実装してください
// - バリデーション(username、email、password)
// - CreatedAt と UpdatedAt を現在時刻に設定
// - IsActive を true に設定
}
// FullName は氏名を返す
func (u *User) FullName() string {
// TODO: 実装してください
}
// Validate はユーザー情報をバリデーション
func (u *User) Validate() error {
// TODO: 実装してください
// - Username: 3文字以上
// - Email: @ を含む
// - Password: 8文字以上
// - Age: 0以上150以下(設定されている場合)
}
// ToJSON はJSON文字列に変換
func (u *User) ToJSON() (string, error) {
// TODO: 実装してください
}
// FromJSON はJSON文字列から復元
func FromJSON(jsonStr string) (*User, error) {
// TODO: 実装してください
}
// Clone はユーザーのコピーを作成
func (u *User) Clone() *User {
// TODO: 実装してください
// Address もディープコピー
}
要件2: ユーザーリポジトリ
ユーザーの保存・検索機能を実装してください。
ファイル: repository/user_repository.go
package repository
import (
"errors"
"yourproject/models"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrDuplicateUser = errors.New("user already exists")
ErrInvalidID = errors.New("invalid user ID")
)
type UserRepository struct {
users map[int]*models.User
nextID int
byEmail map[string]*models.User
}
// NewUserRepository はリポジトリを作成
func NewUserRepository() *UserRepository {
// TODO: 実装してください
}
// Create はユーザーを作成
func (r *UserRepository) Create(user *models.User) error {
// TODO: 実装してください
// - IDを自動採番
// - Email重複チェック
// - byEmail にも登録
}
// GetByID はIDでユーザーを取得
func (r *UserRepository) GetByID(id int) (*models.User, error) {
// TODO: 実装してください
}
// GetByEmail はEmailでユーザーを取得
func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
// TODO: 実装してください
}
// Update はユーザー情報を更新
func (r *UserRepository) Update(user *models.User) error {
// TODO: 実装してください
// - 存在チェック
// - Email変更時は byEmail も更新
// - UpdatedAt を更新
}
// Delete はユーザーを削除
func (r *UserRepository) Delete(id int) error {
// TODO: 実装してください
// - users と byEmail の両方から削除
}
// List はすべてのユーザーを返す
func (r *UserRepository) List() []*models.User {
// TODO: 実装してください
}
// FindByCity は都市でユーザーを検索
func (r *UserRepository) FindByCity(city string) []*models.User {
// TODO: 実装してください
}
// Count はユーザー数を返す
func (r *UserRepository) Count() int {
// TODO: 実装してください
}
// Clear はすべてのユーザーを削除
func (r *UserRepository) Clear() {
// TODO: 実装してください
}
要件3: ユーザーサービス
ビジネスロジックを含むサービス層を実装してください。
ファイル: service/user_service.go
package service
import (
"errors"
"yourproject/models"
"yourproject/repository"
)
var (
ErrInvalidCredentials = errors.New("invalid credentials")
ErrUnauthorized = errors.New("unauthorized")
)
type UserService struct {
repo *repository.UserRepository
}
// NewUserService はサービスを作成
func NewUserService(repo *repository.UserRepository) *UserService {
// TODO: 実装してください
}
// Register は新規ユーザーを登録
func (s *UserService) Register(username, email, password string) (*models.User, error) {
// TODO: 実装してください
// - バリデーション
// - パスワードのハッシュ化(簡易的にsha256を使用)
// - リポジトリに保存
}
// Login はログイン認証
func (s *UserService) Login(email, password string) (*models.User, error) {
// TODO: 実装してください
// - Email でユーザーを検索
// - パスワード照合
// - IsActive チェック
}
// UpdateProfile はプロフィールを更新
func (s *UserService) UpdateProfile(id int, firstName, lastName string, age int) error {
// TODO: 実装してください
// - ユーザーを取得
// - フィールドを更新
// - リポジトリに保存
}
// UpdateAddress はアドレスを更新
func (s *UserService) UpdateAddress(id int, address *models.Address) error {
// TODO: 実装してください
}
// DeactivateUser はユーザーを無効化
func (s *UserService) DeactivateUser(id int) error {
// TODO: 実装してください
// IsActive を false に設定
}
// ActivateUser はユーザーを有効化
func (s *UserService) ActivateUser(id int) error {
// TODO: 実装してください
}
// GetUsersByCity は都市でユーザーを検索
func (s *UserService) GetUsersByCity(city string) []*models.User {
// TODO: 実装してください
}
// GetStatistics はユーザー統計を返す
func (s *UserService) GetStatistics() *UserStats {
// TODO: 実装してください
}
type UserStats struct {
TotalUsers int `json:"total_users"`
ActiveUsers int `json:"active_users"`
UsersByCity map[string]int `json:"users_by_city"`
AverageAge float64 `json:"average_age"`
}
要件4: CLIインターフェース
コマンドラインから操作できるツールを実装してください。
ファイル: main.go
package main
import (
"bufio"
"fmt"
"os"
"strings"
"yourproject/models"
"yourproject/repository"
"yourproject/service"
)
func main() {
repo := repository.NewUserRepository()
svc := service.NewUserService(repo)
// サンプルデータ
seedData(svc)
reader := bufio.NewReader(os.Stdin)
for {
fmt.Println("\nUser Management System")
fmt.Println("1. Register user")
fmt.Println("2. Login")
fmt.Println("3. List users")
fmt.Println("4. Search by city")
fmt.Println("5. Statistics")
fmt.Println("6. Exit")
fmt.Print("Select option: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
switch input {
case "1":
handleRegister(svc, reader)
case "2":
handleLogin(svc, reader)
case "3":
handleList(repo)
case "4":
handleSearch(svc, reader)
case "5":
handleStats(svc)
case "6":
fmt.Println("Goodbye!")
return
default:
fmt.Println("Invalid option")
}
}
}
func handleRegister(svc *service.UserService, reader *bufio.Reader) {
// TODO: 実装してください
}
func handleLogin(svc *service.UserService, reader *bufio.Reader) {
// TODO: 実装してください
}
func handleList(repo *repository.UserRepository) {
// TODO: 実装してください
}
func handleSearch(svc *service.UserService, reader *bufio.Reader) {
// TODO: 実装してください
}
func handleStats(svc *service.UserService) {
// TODO: 実装してください
}
func seedData(svc *service.UserService) {
// TODO: サンプルデータを作成
}
期待される出力
$ go run main.go
User Management System
1. Register user
2. Login
3. List users
4. Search by city
5. Statistics
6. Exit
Select option: 1
=== Register User ===
Username: alice
Email: alice@example.com
Password: ********
User registered successfully! ID: 1
Select option: 3
=== User List ===
ID: 1, Username: alice, Email: alice@example.com, Active: true
ID: 2, Username: bob, Email: bob@example.com, Active: true
Select option: 5
=== Statistics ===
Total Users: 2
Active Users: 2
Average Age: 27.5
Users by City:
Tokyo: 1
Osaka: 1
ボーナス課題
ボーナス1: JSON永続化
ユーザーデータをJSONファイルに保存・読み込みする機能を実装してください。
package repository
// SaveToFile はユーザーをJSONファイルに保存
func (r *UserRepository) SaveToFile(filename string) error {
// TODO: 実装してください
}
// LoadFromFile はJSONファイルからユーザーを読み込み
func (r *UserRepository) LoadFromFile(filename string) error {
// TODO: 実装してください
}
// Export はユーザーをJSONで出力
func (r *UserRepository) Export() (string, error) {
// TODO: 実装してください
}
// Import はJSONからユーザーをインポート
func (r *UserRepository) Import(jsonStr string) error {
// TODO: 実装してください
}
ボーナス2: クエリビルダー
柔軟な検索機能を実装してください。
package repository
type UserQuery struct {
repo *UserRepository
minAge *int
maxAge *int
city *string
isActive *bool
}
// Query はクエリビルダーを作成
func (r *UserRepository) Query() *UserQuery {
// TODO: 実装してください
}
// WithMinAge は最小年齢条件を追加
func (q *UserQuery) WithMinAge(age int) *UserQuery {
// TODO: 実装してください
}
// WithMaxAge は最大年齢条件を追加
func (q *UserQuery) WithMaxAge(age int) *UserQuery {
// TODO: 実装してください
}
// WithCity は都市条件を追加
func (q *UserQuery) WithCity(city string) *UserQuery {
// TODO: 実装してください
}
// WithActiveStatus は有効/無効条件を追加
func (q *UserQuery) WithActiveStatus(isActive bool) *UserQuery {
// TODO: 実装してください
}
// Execute はクエリを実行
func (q *UserQuery) Execute() []*models.User {
// TODO: 実装してください
}
// 使用例
users := repo.Query().
WithMinAge(20).
WithMaxAge(30).
WithCity("Tokyo").
WithActiveStatus(true).
Execute()
ボーナス3: イベントシステム
ユーザーの作成・更新・削除時にイベントを発火する仕組みを実装してください。
package events
type EventType string
const (
EventUserCreated EventType = "user.created"
EventUserUpdated EventType = "user.updated"
EventUserDeleted EventType = "user.deleted"
)
type Event struct {
Type EventType
UserID int
Timestamp time.Time
Data interface{}
}
type EventListener func(Event)
type EventBus struct {
listeners map[EventType][]EventListener
}
// NewEventBus はイベントバスを作成
func NewEventBus() *EventBus {
// TODO: 実装してください
}
// Subscribe はイベントを購読
func (eb *EventBus) Subscribe(eventType EventType, listener EventListener) {
// TODO: 実装してください
}
// Publish はイベントを発行
func (eb *EventBus) Publish(event Event) {
// TODO: 実装してください
}
// 使用例
bus := events.NewEventBus()
bus.Subscribe(events.EventUserCreated, func(e events.Event) {
fmt.Printf("User created: %v\n", e.Data)
})
bus.Publish(events.Event{
Type: events.EventUserCreated,
UserID: 1,
Timestamp: time.Now(),
Data: user,
})
評価基準
| 項目 | 配点 | 詳細 |
|---|---|---|
| ユーザーモデル | 25点 | 構造体定義、バリデーション、JSON |
| リポジトリ | 25点 | CRUD操作が正しく実装されている |
| サービス層 | 25点 | ビジネスロジックが適切 |
| CLIツール | 25点 | ユーザーインターフェースが動作 |
| **ボーナス1** | 10点 | JSON永続化が実装されている |
| **ボーナス2** | 10点 | クエリビルダーが動作する |
| **ボーナス3** | 5点 | イベントシステムが実装されている |
提出方法
submission/
├── go.mod
├── main.go
├── models/
│ ├── user.go
│ └── user_test.go
├── repository/
│ ├── user_repository.go
│ └── user_repository_test.go
├── service/
│ ├── user_service.go
│ └── user_service_test.go
└── bonus/
├── persistence/
├── query/
└── events/
ヒント
- バリデーション: 別関数に分離して再利用
- パスワード:
crypto/sha256でハッシュ化 - JSON:
json.Marshalとjson.Unmarshal - コピー: Address もディープコピー
- 統計: ループで集計
- イベント: Publishはすべてのリスナーを呼ぶ
テストケース
// models/user_test.go
func TestNewUser(t *testing.T) {
user, err := NewUser("alice", "alice@example.com", "password123")
if err != nil {
t.Fatalf("Failed to create user: %v", err)
}
if user.Username != "alice" {
t.Errorf("Username = %s; want alice", user.Username)
}
if !user.IsActive {
t.Error("User should be active by default")
}
}
// repository/user_repository_test.go
func TestCreateUser(t *testing.T) {
repo := NewUserRepository()
user := &models.User{
Username: "test",
Email: "test@example.com",
}
err := repo.Create(user)
if err != nil {
t.Fatalf("Failed to create user: %v", err)
}
if user.ID == 0 {
t.Error("User ID should be assigned")
}
// 重複エラー
err = repo.Create(user)
if err != ErrDuplicateUser {
t.Errorf("Expected duplicate error, got %v", err)
}
}