皆さんはオンボーディングされたプロジェクトやアサインされたタスクのソースコードを読む際にソースコードの説明があると嬉しいですよね?しかし,コメントを書く側では具体的にどのようにコメントを書くといいのだろう...と思いませんか?
どうせ開発するならソースコードで会話できると楽しいですよ!
ソフトウェア開発において,チームでのコミュニケーションとクリーンなコード作成は非常に重要である.本記事では、Goを使用したAPI実装例を通じて、効果的なコミュニケーションの実践方法を紹介する.
ちなみにプルリクエスト(PR)のメッセージも非常に大事である.ぜひこちらもコミュニケーションの例にしてほしい.
コメントがないと...
可読性の低下
- 関数や構造体の目的が不明確になる
- パラメータや返り値の意味が分かりにくくなる
保守性の悪化
- コードの意図や背景が理解しづらくなり、修正や拡張が困難になる
- エッジケースや特殊な処理の理由が不明確になる
チーム開発の障害
- 他の開発者がコードを理解するのに時間がかかる
- 知識の共有が難しくなり、属人化のリスクが高まる
エラーハンドリングの不透明化
- 特定のエラー処理の理由や意図が分かりにくくなる
API使用の困難
- 公開APIの場合、使用方法や注意点が不明確になる
デバッグの困難
- コードの意図が不明確なため、問題の特定に時間がかかる
設計意図の喪失
- アーキテクチャや設計パターンの採用理由が不明確になる
パフォーマンスへの考慮の不透明化
- 特定の実装選択の理由(パフォーマンス最適化など)が分からなくなる
セキュリティ上の考慮事項の不明確化:
- セキュリティ関連の処理の重要性や理由が伝わりにくくなる
ビジネスロジックの不透明化:
- コード内のビジネスルールや重要な決定事項が分かりにくくなる
チームコミュニケーションの重要性
ただただコーディングができればいいのではない.コミュニケーション(コメントに限らず)は開発において非常に重要である.
共通理解の促進:
コメントを通じて、各関数の目的、パラメータ、戻り値を明確に説明することで、チームメンバー全員が同じ理解を持つことができる.
一貫性の維持:
エラーハンドリング,URLパラメータの処理,レスポンス形式などについて,チーム内で統一した方針を持つことの重要性を強調していく.
知識の共有:
依存性注入やコンストラクタの使用方法など,設計パターンや原則についての知識をチーム内で共有すると良い.
コードレビューの効率化:
詳細なコメントは、コードレビューのプロセスを円滑にし、建設的なフィードバックを促進します。
長期的なメンテナンス性:
明確なドキュメンテーションは、将来のメンテナンスや新しいチームメンバーのオンボーディングを容易にします。
実際私の実務経験ではコメントがあるとオンボーディングの際非常に楽になることがわかった.
GoでのAPI実装例
以下は、ユーザー情報を取得するAPIサーバーの実装例である.コメントを通じることでチームのコミュニケーションを促進できる.
具体的に
- 関数名
- 関数の概要
- パラメータ(引数)
- 返り値
- 注意点(あれば)
- 例外処理,エラーハンドリング
(各関数内に動作の詳細を記載してもいい.)
を書くと良い.
main関数は1つの動きを実装しているわけではないので動作別に関数内にコメントを残すとなお良い
package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"strconv"
"strings"
_ "github.com/lib/pq"
)
// User ユーザー情報を表す構造体
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// UserRepository ユーザーデータの永続化を担当するインターフェース
type UserRepository interface {
GetUserByID(ctx context.Context, id int) (*User, error)
CreateUser(ctx context.Context, user *User) error
}
// PostgresUserRepository PostgreSQLを使用したUserRepositoryの実装
type PostgresUserRepository struct {
db *sql.DB
}
// NewPostgresUserRepository PostgresUserRepositoryのコンストラクタ
//
// 概要:
// データベース接続を受け取り、新しいPostgresUserRepositoryインスタンスを生成する
//
// パラメータ:
// - db *sql.DB: 初期化済みのデータベース接続
//
// 返り値:
// - *PostgresUserRepository: 新しく生成されたリポジトリインスタンス
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
// GetUserByID 指定されたIDのユーザーを取得する
//
// 概要:
// データベースから指定されたIDのユーザー情報を取得する
//
// パラメータ:
// - ctx context.Context: コンテキスト
// - id int: 取得するユーザーのID
//
// 返り値:
// - *User: 取得されたユーザー情報
// - error: エラー情報。ユーザーが見つからない場合やデータベースエラーの場合にnilではない
//
// 例外処理・エラーハンドリング:
// - ユーザーが見つからない場合はsql.ErrNoRowsを返す
// - その他のデータベースエラーの場合は対応するエラーを返す
func (r *PostgresUserRepository) GetUserByID(ctx context.Context, id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = $1"
row := r.db.QueryRowContext(ctx, query, id)
var user User
err := row.Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
if err == sql.ErrNoRows {
return nil, err
}
return nil, err
}
return &user, nil
}
// CreateUser 新しいユーザーを作成する
//
// 概要:
// 提供されたユーザー情報を元に、データベースに新しいユーザーを作成する
//
// パラメータ:
// - ctx context.Context: コンテキスト
// - user *User: 作成するユーザーの情報
//
// 返り値:
// - error: エラー情報。作成に成功した場合はnil
//
// 例外処理・エラーハンドリング:
// - データベースエラーが発生した場合は対応するエラーを返す
func (r *PostgresUserRepository) CreateUser(ctx context.Context, user *User) error {
query := "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id"
err := r.db.QueryRowContext(ctx, query, user.Name, user.Email).Scan(&user.ID)
if err != nil {
return err
}
return nil
}
// UserHandler ユーザー関連のHTTPリクエストを処理するハンドラー
type UserHandler struct {
repo UserRepository
}
// NewUserHandler UserHandlerのコンストラクタ
//
// 概要:
// UserRepositoryを受け取り、新しいUserHandlerインスタンスを生成する
//
// パラメータ:
// - repo UserRepository: ユーザーデータの操作を担当するリポジトリ
//
// 返り値:
// - *UserHandler: 新しく生成されたハンドラーインスタンス
func NewUserHandler(repo UserRepository) *UserHandler {
return &UserHandler{repo: repo}
}
// GetUser ユーザー情報を取得するハンドラー
//
// 概要:
// 指定されたユーザーIDに基づいてユーザー情報を取得し、JSONレスポンスとして返す
//
// パラメータ:
// - w http.ResponseWriter: HTTP応答を書き込むためのインターフェース
// - r *http.Request: HTTPリクエスト情報を含む構造体
//
// 返り値:
// なし(HTTPレスポンスとして結果を送信)
//
// 例外処理・エラーハンドリング:
// - 無効なユーザーID: 400 Bad Request
// - ユーザーが見つからない: 404 Not Found
// - サーバー内部エラー: 500 Internal Server Error
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := extractUserID(r)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
user, err := h.repo.GetUserByID(r.Context(), id)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "User not found", http.StatusNotFound)
} else {
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
writeJSONResponse(w, user)
}
// CreateUser 新しいユーザーを作成するハンドラー
//
// 概要:
// HTTPリクエストのボディから新しいユーザー情報を読み取り、データベースに保存する
//
// パラメータ:
// - w http.ResponseWriter: HTTP応答を書き込むためのインターフェース
// - r *http.Request: HTTPリクエスト情報を含む構造体
//
// 返り値:
// なし(HTTPレスポンスとして結果を送信)
//
// 例外処理・エラーハンドリング:
// - 無効なリクエストボディ: 400 Bad Request
// - サーバー内部エラー: 500 Internal Server Error
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := h.repo.CreateUser(r.Context(), &user); err != nil {
http.Error(w, "Failed to create user", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
writeJSONResponse(w, user)
}
// extractUserID URLパスからユーザーIDを抽出する関数
//
// 概要:
// HTTP requestのURLパスからユーザーIDを抽出し、整数値として返す
//
// パラメータ:
// - r *http.Request: HTTPリクエスト情報を含む構造体
//
// 返り値:
// - int: 抽出されたユーザーID
// - error: エラー情報。抽出に失敗した場合はnilではない
//
// 例外処理・エラーハンドリング:
// - URLパスが不正な形式の場合はエラーを返す
// - ユーザーIDが整数に変換できない場合はエラーを返す
func extractUserID(r *http.Request) (int, error) {
parts := strings.Split(r.URL.Path, "/")
if len(parts) != 3 || parts[1] != "users" {
return 0, errors.New("invalid URL path")
}
id, err := strconv.Atoi(parts[2])
if err != nil {
return 0, errors.New("invalid user ID")
}
return id, nil
}
// writeJSONResponse JSONレスポンスを書き込む関数
//
// 概要:
// 与えられたデータをJSON形式でHTTPレスポンスとして書き込む
//
// パラメータ:
// - w http.ResponseWriter: HTTP応答を書き込むためのインターフェース
// - data interface{}: JSON形式に変換して送信するデータ
//
// 返り値:
// なし
//
// 例外処理・エラーハンドリング:
// - JSONエンコードに失敗した場合は500 Internal Server Errorを返す
func writeJSONResponse(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
}
}
func main() {
// データベース接続の初期化
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer db.Close()
// リポジトリとハンドラーの初期化
repo := NewPostgresUserRepository(db)
handler := NewUserHandler(repo)
// ルーティングの設定
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
handler.GetUser(w, r)
case http.MethodPost:
handler.CreateUser(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
})
// サーバーの起動
log.Println("Server starting on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
実践的なコミュニケーション改善策
定期的な技術ミーティング:
週1回の技術ミーティングを開催し,コーディング規約や設計パターンについて議論する.
ペアプログラミング:
定期的にペアプログラミングセッションを実施し,知識共有と相互学習を促進する.
コードレビューの文化を創る:
すべての変更に対してコードレビューを必須とする.
ドキュメンテーションの重視:
コメントだけでなく,プロジェクトのREADMEや社内Wiki(ConfluenceやNotion)など、包括的なドキュメンテーションを作成する.
効果的なチームコミュニケーションは、高品質なソフトウェア開発の基盤である.コード内のコメントから始まり,日々のやり取りに至るまで,オープンで建設的なコミュニケーションを心がけることで、チームの生産性と製品の品質を大きく向上させることができるだろう.
なんなら交換日記みたいにソースコードのコメントで会話する人もいるそうだ
以上の実践を通じて,チームメンバー全員が同じ目標に向かって協力し,より強固で効率的な開発プロセスを構築することができると思う.