Как избавиться от дублирования кода в handlers Golang
Есть такие интерфейсы:
type Handler interface {
Register(router *mux.Router)
Get(w http.ResponseWriter, r *http.Request)
GetAll(w http.ResponseWriter, r *http.Request)
Create(w http.ResponseWriter, r *http.Request)
Delete(w http.ResponseWriter, r *http.Request)
Validate(w http.ResponseWriter, m interface{}) bool
}
type Note interface {
Handler
}
type Person interface {
Handler
}
Структура хендлеров
type handler struct {
logger logger.Logger
}
type note struct {
handler
}
type person struct {
handler
}
И метод Create у Person и у Note почти одинаковые. Различаются лишь переменные, как обойти дублирование, если router.HandleFunc("/persons/", p.Create).Methods("POST") принимает определенную функцию
func (p *person) Create(w http.ResponseWriter, r *http.Request) {
p.logger.Infoln("Handler CreatePerson")
w.Header().Set("Content-Type", "application/json")
var person models.Person
_ = json.NewDecoder(r.Body).Decode(&person)
// Если валидация структуры прошла успешно, создаём заметку в БД.
if p.Validate(w, person) {
return
}
id, err := database.CreatePerson(httpContext, &person)
if err != nil {
p.logger.Errorf("handler cannot save: %w\ndata: %w", err, person)
response(w, "handler cannot save...", http.StatusBadRequest, err.Error(), nil)
return
}
person.Id = uint(id)
// Заметка создана в бд без ошибок, создаём json ответ
response(w, "save ok", http.StatusCreated, nil, person)
}
-------------------------------
func (n *note) Create(w http.ResponseWriter, r *http.Request) {
n.logger.Infoln("Handler CreateNote")
w.Header().Set("Content-Type", "application/json")
var note models.Note
_ = json.NewDecoder(r.Body).Decode(¬e)
// Если валидация структуры прошла успешно, создаём заметку в БД.
if n.Validate(w, note) {
return
}
id, err := database.CreateNote(httpContext, ¬e)
if err != nil {
n.logger.Errorf("handler cannot save: %w\ndata: %w", err, note)
response(w, "handler cannot save...", http.StatusBadRequest, err.Error(), nil)
return
}
note.Id = uint(id)
// Заметка создана в бд без ошибок, создаём json ответ
response(w, "save ok", http.StatusCreated, nil, note)
}
Ответы (1 шт):
Я бы использовал дженерики, так как у вас зависимость от типа - в одном случае это models.Person, в другом models.Note. Все обращения к полям объектов заменил бы на вызовы функции, так как их можно убрать в интерфейсы, чтобы развязать зависимость от конкретных типов:
в типах из пакета
modelsнужно добавить методSetId(uint)с получателем-указателем, напримерpackage models type Person struct { Id uint } func (p *Person) SetId(id uint) { p.Id = id }в интерфейс
Handlerнужно добавить функциюLogger() *logger.Logger, которая возвращает логгер обработчика, чтобы избавиться от прямых вызовов видаp.logger.Errorf
После таких замен можно написать общую функцию Create
type Handler interface {
// Register(router *mux.Router)
// Get(w http.ResponseWriter, r *http.Request)
// GetAll(w http.ResponseWriter, r *http.Request)
Create(w http.ResponseWriter, r *http.Request)
// Delete(w http.ResponseWriter, r *http.Request)
Validate(w http.ResponseWriter, m interface{}) bool
Logger() *logger.Logger
}
func Create[T any, PT interface {
SetId(uint)
*T
}](w http.ResponseWriter, r *http.Request, handler Handler) {
handler.Logger().Infoln("Handler CreatePerson")
w.Header().Set("Content-Type", "application/json")
var obj T
var ptrToObj PT = &obj
_ = json.NewDecoder(r.Body).Decode(ptrToObj)
// Если валидация структуры прошла успешно, создаём заметку в БД.
if handler.Validate(w, ptrToObj) {
return
}
id, err := database.CreatePerson(httpContext, ptrToObj)
if err != nil {
handler.Logger().Errorf("handler cannot save: %w\ndata: %v", err, ptrToObj)
response(w, "handler cannot save...", http.StatusBadRequest, err.Error(), nil)
return
}
ptrToObj.SetId(uint(id))
// Заметка создана в бд без ошибок, создаём json ответ
response(w, "save ok", http.StatusCreated, nil, ptrToObj)
}
В объявлении этой функции главный трюк в том, как объявить, что некоторый тип является указателем и одновременно содержит метод SetId(uint). Это делается через объявление интерфейса в таком виде
PT interface {
SetId(uint)
*T
}
Почему нельзя просто объявить func Create[T interface {SetId(uint)}](...)
В этом случае у вас в качестве типового параметра будет не models.Person, а *models.Person, ведь метод SetId определён для указателя. А для указателя T переменная var obj T будет инициализирована значением nil и json.Decode запаникует. Нужен конкретный тип для аллокации памяти под переменную, и одновременно указатель для вызова SetId
С такой функцией Create можно переписать метод Person.Create так:
type handler struct {
logger *logger.Logger
}
func (h handler) Logger() *logger.Logger {
return h.logger
}
type person struct {
handler
}
// Validate implements Handler
func (*person) Validate(w http.ResponseWriter, m interface{}) bool {
panic("unimplemented")
}
func (p *person) Create(w http.ResponseWriter, r *http.Request) {
p.Logger().Infoln("Handler CreatePerson")
Create[models.Person](w, r, p)
}