0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goで学ぶKISS、DRY、LODの設計原則

0
Posted at

表紙

SOLID 原則はよく知られていますが、それ以外にも有用で広く認められている設計原則があります。本記事では、それらの設計原則を紹介します。主に以下の 3 つの設計原則です:

  • KISS 原則
  • DRY 原則
  • LOD 原則

KISS 原則

KISS 原則(Keep It Simple, Stupid)はソフトウェア開発における重要な原則であり、ソフトウェアシステムを設計・実装する際にはシンプルで直感的に保ち、過度な複雑さや不要な設計を避けるべきであると強調しています。

KISS 原則にはいくつかのバージョンの表現があります。例えば次のようなものです:

  • Keep It Simple and Stupid
  • Keep It Short and Simple
  • Keep It Simple and Straightforward

しかし、よく見れば分かるように、これらが表そうとしている意味はほぼ同じで、日本語に翻訳すると「できる限りシンプルに保つ」となります。

KISS 原則はコードの可読性と保守性を保証するための重要な手段です。ここでいう「シンプル」とはコードの行数の少なさを意味するのではありません。行数が少ないからといってシンプルとは限らず、論理的な複雑さ、実装の難易度、コードの可読性なども考慮する必要があります。また、もともと複雑な問題を複雑な方法で解決することは KISS 原則に反していません。さらに、同じコードであっても、あるビジネスシーンでは KISS 原則に適合していても、別のシーンでは適合しない可能性もあります。

KISS 原則を満たすコードを書くためのガイドラインは以下の通りです:

  • 同僚が理解できない技術を使ってコードを実装しないこと
  • 車輪の再発明をせず、既存のユーティリティライブラリを活用すること
  • 過度な最適化を行わないこと

以下は KISS 原則を用いて設計したシンプルな計算機プログラムの例です:

package main

import "fmt"

// Calculator 簡単な計算機構造を定義
type Calculator struct{}

// Add メソッドは2つの数を加算する
func (c Calculator) Add(a, b int) int {
    return a + b
}

// Subtract メソッドは2つの数を減算する
func (c Calculator) Subtract(a, b int) int {
    return a - b
}

func main() {
    calculator := Calculator{}

    // 5 + 3 を計算
    result1 := calculator.Add(5, 3)
    fmt.Println("5 + 3 =", result1)

    // 8 - 2 を計算
    result2 := calculator.Subtract(8, 2)
    fmt.Println("8 - 2 =", result2)
}

上記の例では、シンプルな計算機構造 Calculator を定義し、加算と減算を実現する AddSubtract メソッドを含めました。シンプルな設計と実装により、この計算機プログラムは明確で理解しやすく、KISS 原則の要件を満たしています。

DRY 原則

DRY 原則(Don’t Repeat Yourself)はソフトウェア開発における重要な原則のひとつであり、コードや機能の重複を避け、システム内の冗長性をできる限り減らすことを強調しています。DRY 原則の核心的な考え方は、システム内のいかなる情報も唯一明確な表現形式を持つべきであり、同じ情報やロジックを複数箇所で繰り返し定義してはならないということです。

DRY 原則はとても簡単で適用も容易だと感じるかもしれません。「二つのコードが同じなら DRY 原則違反だ」と思う人もいるでしょう。本当にそうでしょうか?答えは「いいえ」です。これは多くの人が抱いている誤解です。実際には、コードが重複しているからといって必ずしも DRY 原則違反とは限らず、逆に一見すると重複していないコードでも DRY 原則に違反している場合があります。

典型的なコードの重複には三つの種類があります。それは「実装ロジックの重複」「機能の意味的重複」「コード実行の重複」です。これらの重複は、一見 DRY 原則違反に見えて実際には違反していなかったり、その逆もあり得ます。

実装ロジックの重複

type UserAuthenticator struct{}

func (ua *UserAuthenticator) authenticate(username, password string) {
    if !ua.isValidUsername(username) {
        // ... code block 1
    }

    if !ua.isValidPassword(username) {
        // ... code block 1
    }
    // ...省略...
}

func (ua *UserAuthenticator) isValidUsername(username string) bool {}

func (ua *UserAuthenticator) isValidPassword(password string) bool {}

仮に isValidUserName()isValidPassword() のコードが重複しているとします。一見すると明らかに DRY 原則に違反しているように見えます。そこで重複を取り除くため、これらを統合し、より汎用的な関数 isValidUserNameOrPassword() にリファクタリングしたとしましょう。

リファクタリング後は行数も減り、重複もなくなりました。本当に良くなったのでしょうか?答えは「いいえ」です。関数名からも分かるように、統合された isValidUserNameOrPassword() は「ユーザ名の検証」と「パスワードの検証」という二つの異なる責務を持ってしまい、「単一責任原則」や「インターフェース分離の原則」に違反しています。

さらに言えば、isValidUserName()isValidPassword() はコード上の実装ロジックが同じであっても、意味的には重複していません。すなわち、機能的には全く別の処理をしているのです。一方は「ユーザ名の検証」であり、もう一方は「パスワードの検証」です。現状では検証ロジックが同一だったとしても、将来的にパスワードの検証ロジックが変更された場合、二つの関数の実装は異なるものになります。その場合、統合した関数を再び分割せざるを得ません。

このような重複の問題に対しては、より細かい粒度の関数に抽象化する方法で解決できます。

機能の意味的重複

同じプロジェクト内に isValidIp()checkIfIpValid() という二つの関数があるとします。名前は違い、実装ロジックも異なるものの、どちらも「IP アドレスが有効かどうか」を判定する機能を持っています。

func isValidIp(ipAddress string) bool {
    // ... 正規表現による判定
}

func checkIfIpValid(ipAddress string) bool {
    // ... 文字列処理による判定
}

この例ではコードのロジックは重複していませんが、機能的には重複しています。すなわち意味的に同じことをしているため、DRY 原則違反とみなされます。プロジェクト内では実装方法を統一し、IP アドレスの判定が必要な箇所は必ず同じ関数を呼び出すようにすべきです。

コード実行の重複

type UserService struct {
    userRepo UserRepo
}

func (us *UserService) login(email, password string) {
    existed := us.userRepo.checkIfUserExisted(email, password)
    if !existed {
        // ...
    }
    user := us.userRepo.getUserByEmail(email)
}

type UserRepo struct{}

func (ur *UserRepo) checkIfUserExisted(email, password string) bool {
    if !ur.isValidEmail(email) {
        // ...
    }
}

func (ur *UserRepo) getUserByEmail(email string) User {
    if !ur.isValidEmail(email) {
        // ...
    }
}

このコードには実装の重複も意味的な重複もありませんが、それでも DRY 原則に違反しています。その理由は「実行の重複」が存在しているからです。isValidEmail() が複数箇所で呼ばれており、同じ処理が繰り返されているのです。

この問題の解決は比較的簡単で、検証ロジックを UserRepo から取り除き、UserService 側で一元的に処理するようにすれば良いのです。

どうすればコードの再利用性を高められるか?

  • コードの結合度を減らすこと
  • 単一責任原則を満たすこと
  • 業務ロジックと非業務ロジックをモジュール化して分離すること
  • 共通コードを下位レイヤーにまとめること
  • 継承・多態・抽象化・カプセル化を活用すること
  • テンプレートなどのデザインパターンを応用すること

以下は簡単な人員管理システムの例で、DRY 原則を用いてコードの明確さと再利用性を確保しています:

package main

import "fmt"

// Person 構造体は人員情報を表す
type Person struct {
    Name string
    Age  int
}

// PrintPersonInfo 人員情報を出力する
func PrintPersonInfo(p Person) {
    fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
}

func main() {
    // 二人の人員情報を作成
    person1 := Person{Name: "Alice", Age: 30}
    person2 := Person{Name: "Bob", Age: 25}

    // 人員情報を出力
    PrintPersonInfo(person1)
    PrintPersonInfo(person2)
}

上記の例では、Person 構造体を定義して人員情報を表し、さらに PrintPersonInfo 関数を定義して人員情報を出力しています。出力処理を PrintPersonInfo にカプセル化することで、DRY 原則に従い、重複した出力ロジックを避け、コードの再利用性と保守性を高めています。

LOD 原則

LOD 原則(Law of Demeter、ディミテルの法則)は「最少知識の原則」とも呼ばれ、オブジェクト間の結合度を下げ、システム各部分の依存関係を減らすことを目的としています。LOD 原則は「あるオブジェクトは他のオブジェクトについて知ることをできる限り少なくすべきであり、直接知らないオブジェクトと通信すべきではない。自分のメンバーを通じて操作すべきだ」と強調しています。

ディミテルの法則は「本来直接依存関係を持つべきでないクラス同士は依存してはならず、依存関係を持つクラス間でも必要最小限のインターフェースだけに依存すべき」と説きます。この法則は、クラス間の結合を減らし、クラスをできるだけ独立させることを目指しています。各クラスはシステムの他の部分についてできるだけ少なく知るべきであり、変化が起きた場合に影響を受けるクラスの数を減らすことができます。

以下は LOD 原則を用いて設計したシンプルなユーザ管理システムの例です:

package main

import "fmt"

// UserService ユーザサービス、ユーザ管理を担当
type UserService struct{}

// GetUserByID ユーザIDに基づきユーザ情報を取得
func (us UserService) GetUserByID(id int) User {
    userRepo := UserRepository{}
    return userRepo.FindByID(id)
}

// UserRepository ユーザリポジトリ、ユーザデータを管理
type UserRepository struct{}

// FindByID ユーザIDに基づきユーザ情報を検索
func (ur UserRepository) FindByID(id int) User {
    // データベースからユーザ情報を検索するシミュレーション
    return User{id, "Alice"}
}

// User ユーザ構造体
type User struct {
    ID   int
    Name string
}

func main() {
    userService := UserService{}

    user := userService.GetUserByID(1)
    fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
}

上記の例では、ユーザ管理システムを UserService(ユーザサービス)と UserRepository(ユーザリポジトリ)の二つの部分で構成しています。UserServiceUserRepository を呼び出してユーザ情報を取得しており、直接の友達(自分が保持するメンバー)とのみ通信するという LOD 原則の要件に従っています。


私たちはLeapcell、Goプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?