Beyond SOLID: GoにおけるKISS、DRY、LOD原則
Daniel Hayes
Full-Stack Engineer · Leapcell

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原則を満たすコードを書くためのガイドライン:
- コードを実装する際に、同僚が理解できない可能性のあるテクノロジーを使用しないでください。
- 車輪の再発明をしないでください。既存のライブラリを有効に活用してください。
- 過度に最適化しないでください。
以下は、KISS原則に基づいて設計された簡単な電卓プログラムの例です。
package main import "fmt" // Calculator defines a simple calculator structure type Calculator struct{} // Add method adds two numbers func (c Calculator) Add(a, b int) int { return a + b } // Subtract method subtracts two numbers func (c Calculator) Subtract(a, b int) int { return a - b } func main() { calculator := Calculator{} // Calculate 5 + 3 result1 := calculator.Add(5, 3) fmt.Println("5 + 3 =", result1) // Calculate 8 - 2 result2 := calculator.Subtract(8, 2) fmt.Println("8 - 2 =", result2) }
上記の例では、加算と減算を実行するためのAdd
メソッドとSubtract
メソッドを含む、単純な電卓構造Calculator
を定義しました。シンプルな設計と実装により、この電卓プログラムは明確で理解しやすく、KISS原則の要件を満たしています。
DRY原則
DRY原則(_Don’t Repeat Yourself_の略)は、ソフトウェア開発における重要な原則の1つです。重複したコードや機能を避け、システム内の冗長性を最小限に抑えることを強調します。DRY原則の中核となる考え方は、システム内のあらゆる情報は、1つだけ明確な表現を持つべきであるということです。同じ情報やロジックを複数の場所で繰り返し定義することを避けます。
DRY原則は非常にシンプルで適用しやすいと思うかもしれません。2つのコードが同じように見える限り、DRYに違反します。しかし、それは本当にそうでしょうか?答えはノーです。これは、原則の一般的な誤解です。実際には、重複したコードが必ずしもDRYに違反するとは限らず、反復的でないように見えるコードが、実際にはDRYに違反する可能性があります。
通常、コードの繰り返しには、実装ロジックの繰り返し、機能的意味の繰り返し、実行の繰り返しの3種類があります。これらのうち、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 } // ... other code omitted ... } func (ua *UserAuthenticator) isValidUsername(username string) bool {} func (ua *UserAuthenticator) isValidPassword(password string) bool {}
isValidUserName()
関数とisValidPassword()
関数に重複したコードが含まれているとします。一見すると、これはDRYの明らかな違反のように思えます。重複を削除するには、コードをリファクタリングして、より一般的な関数isValidUserNameOrPassword()
にまとめることができます。
リファクタリング後、行数は減少し、重複したコードはありません。これはより良いでしょうか?答えはノーです。関数名からわかるように、マージされたisValidUserNameOrPassword()
関数は、ユーザー名の検証とパスワードの検証という2つのタスクを処理します。これは、_単一責任原則_と_インターフェース分離原則_に違反します。
実際、2つの関数をマージしても、問題は残ります。isValidUserName()
とisValidPassword()
は論理的に反復しているように見えますが、意味的にはそうではありません。意味の非反復とは、機能的に、これらの2つのメソッドが完全に異なることを意味します。1つはユーザー名を検証し、もう1つはパスワードを検証します。現在の設計では検証ロジックは同一ですが、マージすると潜在的な問題が発生します。たとえば、いつかパスワード検証ロジックを変更する可能性があり、その時点で2つの関数の実装が再び異なるようになります。その後、元の2つの関数に分割する必要があります。
コードが重複している場合は、それらをより小さく、よりきめ細かい関数に抽象化することで問題を解決できることがよくあります。
機能的意味の繰り返し
同じプロジェクトで、これら2つの関数isValidIp()
とcheckIfIpValid()
を考えてみましょう。名前が異なり、異なる実装を使用していますが、機能は同一です。どちらもIPアドレスが有効かどうかを確認します。
func isValidIp(ipAddress string) bool { // ... validation using regex } func checkIfIpValid(ipAddress string) bool { // ... validation using string operations }
この例では、実装は異なりますが、機能が繰り返されます。つまり、意味の繰り返しです。これは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に違反しています。これは、コードに実行の繰り返しが含まれているためです。
修正は比較的簡単です。UserRepo
から検証ロジックを削除し、UserService
に集中させるだけです。
コードの再利用性を向上させるには?
- コードの結合度を減らす。
- 単一責任原則に従う。
- ビジネスロジックを非ビジネスロジックから分離する。
- 共通コードを共有モジュールにプッシュする。
- 継承、ポリモーフィズム、抽象化、カプセル化を適用する。
- テンプレートなどの設計パターンを使用する。
以下は、DRY原則を適用してコードの明確さと再利用性を確保する、単純な人事管理システムの例です。
package main import "fmt" // Person struct represents personal information type Person struct { Name string Age int } // PrintPersonInfo prints personal information func PrintPersonInfo(p Person) { fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age) } func main() { // Create two people person1 := Person{Name: "Alice", Age: 30} person2 := Person{Name: "Bob", Age: 25} // Print personal information PrintPersonInfo(person1) PrintPersonInfo(person2) }
上記の例では、個人情報を表すPerson
構造体と、それを印刷するPrintPersonInfo
関数を定義しました。印刷ロジックをPrintPersonInfo
にカプセル化することで、DRY原則を遵守し、印刷ロジックの繰り返しを回避し、コードの再利用性と保守性を向上させます。
LOD原則
LOD原則(Law of Demeter)は、_最小知識の原則_とも呼ばれ、オブジェクト間の結合度を下げ、システム内の異なる部分間の依存関係を最小限に抑えることを目的としています。LOD原則は、オブジェクトは他のオブジェクトについてできるだけ知っておくべきではなく、見知らぬ人と直接通信するのではなく、独自のメンバーを介して操作することを強調します。
Law of Demeterは、直接的な依存関係がないクラスは依存関係を持つべきではなく、依存関係のあるクラスは必要なインターフェースのみに依存すべきであることを強調しています。その考え方は、クラス間の結合度を下げ、できるだけ独立させることです。各クラスは、システムの他の部分についてできるだけ知っておくべきではありません。変更が発生した場合、それらの変更を認識して適応する必要があるクラスは少なくなります。
以下は、LOD原則を使用して設計された単純なユーザー管理システムの例です。
package main import "fmt" // UserService: responsible for user management type UserService struct{} // GetUserByID retrieves user information by user ID func (us UserService) GetUserByID(id int) User { userRepo := UserRepository{} return userRepo.FindByID(id) } // UserRepository: responsible for user data maintenance type UserRepository struct{} // FindByID retrieves user information from database by ID func (ur UserRepository) FindByID(id int) User { // Simulate fetching user from database return User{id, "Alice"} } // User struct 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
(ユーザーリポジトリ)の2つの部分で構成される単純なユーザー管理システムを設計しました。UserService
は、UserRepository
を呼び出してユーザー情報を照会します。これは、「直接の友人」とのみ通信を確保することにより、LOD原則を遵守しています。
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ