
Photo by Taylor Nicole on Unsplash
The Essence of DRY Principle
Nowadays, business logic is unstable, it’s rapid changes caused by many factors such as government regulation, market needs, and so on. This phenomenon also reflects in our code. Our code can be only valid in a month or days or even in certain hours. To deal with this unstable situation, our code must be Easy To Change (ETC). One of the important aspects to make it happen is to adopt Don’t Repeat Yourself (DRY) Principle.
The duplication will cause a big trouble in our code someday, it will be difficult to maintain and understand. Any code changes will also reflect the duplication code, We change the real code together with its duplication. Therefore, We have to be very careful to changes our code that may break the running application. Finally, We decide to refactor our code to avoid duplication. Unfortunately, the cost isn’t cheap. Hence, the duplication scattered among our code base.
Duplication accident among protocol buffers implementation
Usually, to model our data structure and services through gRPC, We implement the protocol buffers. Suppose, We design a user domain in proto/user.proto file like example below:
syntax = "proto3";
package proto;
message User {
int64 id = 1;
string name = 2;
string address = 3;
int32 age = 4;
}
message UserReq {
int64 id = 1;
}
service UserService {
rpc GetUser(UserReq) returns (User);
}
user.proto
Then, We compile that file into golang code using protoc. Finally, our code structure will appear like below:
user
├── proto
│ ├── user.pb.go
│ ├── user.proto
├── user
│ ├── user.go
├── go.mod
├── go.sum
Our code usually looks like this:
package user
import (
"context"
pb "dry/proto"
)
type User struct {
Id int64
Name string
Address string
Age int32
}
type Service struct {
}
func (s *Service) GetUser(ctx context.Context, id int64) (*User, error) {
var user *User
// business logic
return user, nil
}
type Handler struct {
s Service
}
func (h *Handler) GetUser(ctx context.Context, req *pb.UserReq) (*pb.User, error) {
user, err:= h.s.GetUser(ctx, req.Id)
if err!= nil {
return nil, err
}
return userToPb(user), nil
}
func userToPb(user *User) *pb.User {
pbUser:= &pb.User{}
pbUser.Id = user.Id
pbUser.Name = user.Name
pbUser.Address = user.Address
pbUser.Age = user.Age
return pbUser
}
user-duplication.go
The code above did several duplications. Unfortunately, We usually do the same thing. We didn’t even realize the impact of such implementation. For now, let’s analyze which duplication in code above:
- We found the duplication in the User model since user.proto already define the structure We didn’t need to declare it again.
- We declared the GetUser service twice in** Handler and Service struct, How can it be? Have we done the right thing by separating representation logic and business logic? Yes, but the underlying gPRC server already handles our representation logic. So, We don’t need our Handler** struct anymore.
There is also the side effect of this kind of implementation, this one is struct transformation implemented by userToPb function. This function transform User into pb.User (it may also growth to transform back and forth in the future). Once our User struct evolves or does some changes, this** userToPb **also grows as follows. Perhaps to maintain this transformation is a trivial job, but it will be exhausting if the model is frequently changed or become a complex structure.
Avoiding Duplication with Reference and Inheritance
We didn’t want to let the duplications spread beneath our code base. Once the code grows bigger and bigger our chances to fix it will get smaller. We need to fix it as soon as possible. Let’s fix the duplication code above by change our code into the example below:
package user
import (
"context"
pb "dry/proto"
)
type User pb.User
type Service struct {
}
func (s *Service) GetUser(ctx context.Context, req *pb.UserReq) (*pb.User, error) {
var user User
// do business logic
pbUser:= pb.User(user)
return &pbUser, nil
}
user-reference.go
By referencing the User into pb.User and remove Handler struct, We wiped out all the duplication. We also remove userToPb function, only cast User to pb.User to do the transformation. Also, We can extend** User** struct functionality to compute any kind of logic such as getter, setter, and others.
It seems, our duplication problems have been resolved, Our code looks even smaller and simpler. However, it has certain limitation. We can’t add more attributes in User struct. There is also impossible to override the method. Don’t worry We have another way to break this limitation and of course, no duplication will occur.
Let’s implement inheritance into our code like the example below:
package user
import (
"context"
pb "dry/proto"
)
type User struct {
pb.User
}
type Service struct {
}
func (s *Service) GetUser(ctx context.Context, req *pb.UserReq) (*pb.User, error) {
var user User
// do business logic
return &user.User, nil
}
user-inheritance.go
Finally we break the limitation!, We even didn’t need to cast User into pb.User.
Key Take-Aways
We learned a lesson from figuring out the problems, then solved them with several approaches. The lesson can be stated in each point below:
- Duplication makes our code difficult to maintain, not easy to understand, and not Easy To Change (ETC).
- Duplication grows unnecessary complexity such as the model transformation.
- Referencing model into protocol buffers will avoid duplication with certain limitations such as adding new attributes and methods overriding.
- Implementing inheritance instead of reference will break the limitations.