関数設計

4 件の記事

Contents

Even if two functions perform the same process, separate them when their purposes differ. (2つの関数が同じ処理を実行する場合でも、目的が異なる場合は分離する)

解説

同じ処理内容であっても、目的やコンテキストが異なる場合は別々の関数として定義すべきです。これにより、将来的に要件が変わり処理内容が分岐した際に、影響範囲を限定できます。共通化を優先しすぎると、異なる目的の処理が密結合になり、一方の変更が他方に意図しない影響を与えるリスクが生じます。目的に応じた関数分離は、変更に強い設計の基本です。

具体例

// 悪い例(目的が異なるのに共通化)
func calculateDiscount(price float64) float64 {
    return price * 0.9  // 10%割引
}

func processNewUserOrder(price float64) float64 {
    return calculateDiscount(price)  // 新規ユーザー割引
}

func processSeasonalSale(price float64) float64 {
    return calculateDiscount(price)  // シーズンセール割引
}

// 良い例(目的ごとに分離)
func calculateNewUserDiscount(price float64) float64 {
    return price * 0.9  // 新規ユーザー割引
}

func calculateSeasonalDiscount(price float64) float64 {
    return price * 0.9  // シーズンセール割引(将来変更可能性あり)
}

func processNewUserOrder(price float64) float64 {
    return calculateNewUserDiscount(price)
}

func processSeasonalSale(price float64) float64 {
    return calculateSeasonalDiscount(price)
}

参考リンク

Contents

A function should have no more than five parameters. More than five suggests too many responsibilities or insufficient aggregation. (関数のパラメータは5つ以内にすべき。5つを超える場合は、責任が多すぎるか、集約が不足していることを示す)

解説

パラメータが多い関数は、理解と使用が困難であり、単一責任原則に違反している可能性が高いです。パラメータが5つを超える場合、関連するパラメータをオブジェクトにまとめるか、関数の責任を分割すべきです。パラメータを集約することで、関連する値が明確になり、関数シグネチャがシンプルになります。これにより、コードの可読性と保守性が向上します。

具体例

// 悪い例(パラメータが多すぎる)
func CreateUser(
    firstName string,
    lastName string,
    email string,
    phone string,
    address string,
    city string,
    zipCode string,
    country string,
) error {
    // パラメータの順序を覚えるのが困難
    // ...
}

func main() {
    CreateUser("John", "Doe", "john@example.com", "123-456",
               "123 Main St", "Tokyo", "100-0001", "Japan")
}

// 良い例(パラメータをオブジェクトに集約)
type UserProfile struct {
    FirstName string
    LastName  string
    Email     string
    Phone     string
}

type Address struct {
    Street  string
    City    string
    ZipCode string
    Country string
}

func CreateUser(profile UserProfile, address Address) error {
    // 関連するデータがグループ化され、理解しやすい
    // ...
}

func main() {
    profile := UserProfile{
        FirstName: "John",
        LastName:  "Doe",
        Email:     "john@example.com",
        Phone:     "123-456",
    }

    address := Address{
        Street:  "123 Main St",
        City:    "Tokyo",
        ZipCode: "100-0001",
        Country: "Japan",
    }

    CreateUser(profile, address)
}

// さらに良い例(責任を分割)
func CreateUser(profile UserProfile) (UserID, error) {
    // ユーザー作成のみに集中
    // ...
}

func SetUserAddress(userID UserID, address Address) error {
    // 住所設定を別関数に分離
    // ...
}

参考リンク

Contents

Keep each function to 100 lines or fewer (logical lines; wrapped lines count as one). (各関数は100行以内に収める(論理行;折り返された行は1行とカウント))

解説

長い関数は、複数の責任を持っている可能性が高く、理解とテストが困難です。100行を超える関数は、処理を小さな関数に分割することで、可読性と再利用性が向上します。短い関数は、単一の責任を持ち、名前から目的が明確になります。論理行でカウントするため、コメントや空行、折り返しは複数行とはみなされません。適切な粒度の関数は、バグの発見と修正を容易にします。

具体例

// 悪い例(長すぎる関数 - 200行以上)
func ProcessOrder(order Order) error {
    // バリデーション(30行)
    if order.ID == "" {
        return errors.New("order ID is required")
    }
    if order.CustomerID == "" {
        return errors.New("customer ID is required")
    }
    // ... さらに20項目のバリデーション

    // 在庫チェック(40行)
    for _, item := range order.Items {
        stock, err := db.GetStock(item.ProductID)
        if err != nil {
            return err
        }
        // ... 複雑な在庫ロジック
    }

    // 価格計算(40行)
    subtotal := 0.0
    for _, item := range order.Items {
        subtotal += item.Price * float64(item.Quantity)
    }
    // ... 割引、税金、送料の計算

    // 決済処理(40行)
    // ... 決済ゲートウェイとの通信

    // メール送信(30行)
    // ... メール作成と送信

    // データベース保存(20行)
    // ... 複数テーブルへの保存

    return nil
}

// 良い例(適切に分割された関数)
func ProcessOrder(order Order) error {
    if err := validateOrder(order); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }

    if err := checkInventory(order.Items); err != nil {
        return fmt.Errorf("inventory check failed: %w", err)
    }

    total, err := calculateOrderTotal(order)
    if err != nil {
        return fmt.Errorf("calculation failed: %w", err)
    }

    if err := processPayment(order.CustomerID, total); err != nil {
        return fmt.Errorf("payment failed: %w", err)
    }

    if err := saveOrder(order); err != nil {
        return fmt.Errorf("save failed: %w", err)
    }

    if err := sendConfirmationEmail(order); err != nil {
        log.Printf("email failed: %v", err)
        // メール失敗は注文処理を失敗させない
    }

    return nil
}

// 各関数は単一の責任を持ち、100行以内
func validateOrder(order Order) error {
    if order.ID == "" {
        return errors.New("order ID is required")
    }
    if order.CustomerID == "" {
        return errors.New("customer ID is required")
    }
    // ... その他のバリデーション
    return nil
}

func checkInventory(items []Item) error {
    for _, item := range items {
        if err := verifyStockAvailability(item); err != nil {
            return err
        }
    }
    return nil
}

func calculateOrderTotal(order Order) (float64, error) {
    subtotal := calculateSubtotal(order.Items)
    discount := calculateDiscount(order, subtotal)
    tax := calculateTax(subtotal - discount)
    shipping := calculateShipping(order)
    return subtotal - discount + tax + shipping, nil
}

参考リンク

Contents

Instead of flag parameters that toggle behavior inside a function, split the function. (関数内部で振る舞いを切り替えるフラグパラメータを使う代わりに、関数を分割する)

解説

フラグパラメータを持つ関数は、実質的に2つ以上の異なる処理を1つの関数に詰め込んでおり、単一責任原則に違反します。フラグによって振る舞いが変わる関数は、呼び出し側でフラグの意味を理解する必要があり、可読性が低下します。関数を分割することで、各関数の意図が明確になり、使用方法が直感的になります。これにより、コードの保守性と理解しやすさが向上します。

具体例

// 悪い例(フラグパラメータで振る舞いを変更)
func ProcessUser(user User, sendEmail bool) error {
    if err := validateUser(user); err != nil {
        return err
    }

    if err := saveUser(user); err != nil {
        return err
    }

    if sendEmail {
        // メール送信処理
        if err := sendWelcomeEmail(user.Email); err != nil {
            return err
        }
    }

    return nil
}

func main() {
    ProcessUser(newUser, true)   // trueは何を意味する?
    ProcessUser(existingUser, false)  // falseは何を意味する?
}

// 良い例(関数を分割)
func RegisterNewUser(user User) error {
    if err := validateUser(user); err != nil {
        return err
    }

    if err := saveUser(user); err != nil {
        return err
    }

    if err := sendWelcomeEmail(user.Email); err != nil {
        return err
    }

    return nil
}

func UpdateExistingUser(user User) error {
    if err := validateUser(user); err != nil {
        return err
    }

    if err := saveUser(user); err != nil {
        return err
    }

    // メール送信なし

    return nil
}

func main() {
    RegisterNewUser(newUser)       // 意図が明確
    UpdateExistingUser(existingUser)  // 意図が明確
}

参考リンク