Как избавиться от дублирования кода в 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(&note)

    // Если валидация структуры прошла успешно, создаём заметку в БД.
    if n.Validate(w, note) {
        return
    }
    id, err := database.CreateNote(httpContext, &note)
    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 шт):

Автор решения: Pak Uula

Я бы использовал дженерики, так как у вас зависимость от типа - в одном случае это 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)
}
→ Ссылка