Besides the well-known SOLID principles, there are actually other useful and widely recognized design principles. This article introduces these design principles, mainly including the following three:
- KISS Principle;
- DRY Principle;
- LOD Principle.
KISS Principle
The KISS principle (Keep It Simple, Stupid) is an important principle in software development. It emphasizes keeping things simple and intuitive in the design and implementation of software systems, avoiding excessive complexity and unnecessary design.
There are several versions of the description of the KISS principle, such as the following:
- Keep It Simple and Stupid;
- Keep It Short and Simple;
- Keep It Simple and Straightforward.
However, upon closer inspection, you’ll find that what they mean is essentially the same, which can be translated into Chinese as: “keep it as simple as possible.”
The KISS principle is an important means to ensure code readability and maintainability. The “simplicity” in KISS is not measured by the number of lines of code. Fewer lines of code do not necessarily mean simpler code. We also need to consider logical complexity, implementation difficulty, code readability, etc. Moreover, if a problem itself is complex, solving it with complex methods does not violate the KISS principle. In addition, the same code may satisfy the KISS principle in one business scenario but fail to do so in another.
Guidelines for writing code that satisfies the KISS principle:
- Do not use technologies your colleagues may not understand when implementing code.
- Do not reinvent the wheel; make good use of existing libraries.
- Do not over-optimize.
Here is an example of a simple calculator program designed with the KISS principle:
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)
}
In the above example, we defined a simple calculator structure Calculator
, containing the Add
and Subtract
methods to perform addition and subtraction. With simple design and implementation, this calculator program is clear, easy to understand, and meets the requirements of the KISS principle.
DRY Principle
The DRY principle, short for Don’t Repeat Yourself, is one of the important principles in software development. It emphasizes avoiding duplicate code and functionality, and minimizing redundancy in the system. The core idea of the DRY principle is that any piece of information in the system should have one, and only one, unambiguous representation. It avoids defining the same information or logic repeatedly in multiple places.
You might think the DRY principle is very simple and easy to apply: as long as two pieces of code look the same, then it violates DRY. But is that really the case? The answer is no. This is a common misunderstanding of the principle. In reality, duplicated code does not necessarily violate DRY, and some code that looks non-repetitive may, in fact, violate DRY.
Typically, there are three types of code repetition: implementation logic repetition, functional semantics repetition, and execution repetition. Some of these may appear to violate DRY but don’t, while others may look fine but actually violate it.
Implementation Logic Repetition
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 {}
Suppose the isValidUserName()
and isValidPassword()
functions contain duplicated code. At first glance, this seems like a clear violation of DRY. To remove the duplication, we could refactor the code and merge them into a more generic function, isValidUserNameOrPassword()
.
After refactoring, the number of lines decreases, and there is no repeated code. Is this better? The answer is no. Even from the function name, we can see that the merged isValidUserNameOrPassword()
function handles two tasks: validating usernames and validating passwords. This violates the Single Responsibility Principle and the Interface Segregation Principle.
In fact, even if we merge the two functions, problems remain. Although isValidUserName()
and isValidPassword()
appear to be logically repetitive, semantically they are not. Semantic non-repetition means that, functionally, these two methods do completely different things: one validates usernames, and the other validates passwords. While in the current design the validation logic is identical, if we merge them, we introduce potential issues. For example, one day we may change the password validation logic, at which point the two functions would differ in implementation again. We would then have to split them back into the original two functions.
For cases of duplicated code, we can often solve the problem by abstracting them into smaller, more fine-grained functions.
Functional Semantics Repetition
In the same project, consider these two functions: isValidIp()
and checkIfIpValid()
. Although their names differ and they use different implementations, their functionality is identical—they both check whether an IP address is valid.
func isValidIp(ipAddress string) bool {
// ... validation using regex
}
func checkIfIpValid(ipAddress string) bool {
// ... validation using string operations
}
In this example, although the implementations are different, the functionality is repeated, i.e., semantic repetition. This violates the DRY principle. In such cases, we should unify the implementation into a single approach, and wherever we need to check if an IP is valid, we should call the same function consistently.
Execution Repetition
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) {
// ...
}
}
In the code above, there is no logical duplication and no semantic duplication, but it still violates DRY. This is because the code contains execution repetition.
The fix is relatively simple: we just need to remove the validation logic from UserRepo
and centralize it in UserService
.
How to Improve Code Reusability?
- Reduce code coupling.
- Follow the Single Responsibility Principle.
- Separate business logic from non-business logic.
- Push common code down to shared modules.
- Apply inheritance, polymorphism, abstraction, and encapsulation.
- Use design patterns such as templates.
Here is a simple personnel management system example that applies the DRY principle to ensure clarity and reusability of code:
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)
}
In the above example, we defined a Person
struct to represent personal information, along with a PrintPersonInfo
function to print it. By encapsulating the printing logic in PrintPersonInfo
, we adhere to the DRY principle, avoiding repeated printing logic and improving code reusability and maintainability.
LOD Principle
The LOD principle (Law of Demeter), also known as the Principle of Least Knowledge, aims to reduce coupling between objects and minimize dependencies between different parts of the system. The LOD principle emphasizes that an object should know as little as possible about other objects, and should not communicate directly with strangers, but instead operate through its own members.
The Law of Demeter stresses that classes without a direct dependency should not have one, and that classes with dependencies should rely only on the necessary interfaces. The idea is to reduce coupling between classes and make them as independent as possible. Each class should know as little as possible about the rest of the system. When changes occur, fewer classes need to be aware of and adapt to those changes.
Here is an example of a simple user management system designed using the LOD principle:
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)
}
In the above example, we designed a simple user management system consisting of two parts: UserService
(user service) and UserRepository
(user repository). UserService
queries user information by calling UserRepository
. This adheres to the LOD principle by ensuring communication only happens with “direct friends.”
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