NOTA: Pense que sua função é como uma piscina, quanto mais longe e quanto mais tempo você fica longe da borda, mais chance você tem de se afogar!!!
Lembre-se, passamos muito mais tempo lendo os códigos do que os escrevendo então, muitas vezes, é justificável gastarmos mais tempo para escrever um código melhor, visto que o mesmo será lido várias vezes.
Normalmente o nível de indentação de um código está diretamente ligado a sua complexidade e complexidade ciclomática, quanto mais indentado, mais complexo e em GO isso não é diferente, GO ainda possui uma característica particular que pode tornar as funções um pouco mais longas e complexas, é o tratamento de erros. Nos nosso sistemas isso não é diferente, possuímos alguns códigos grandes e complexo e queremos debater algumas práticas evitar isso.
- Funções com responsabilidade única
- Funções com devem se divididas em dois tipos, orquestradores e funções com lógica, sempre separando o que se quer fazer, de como se faz (o que, do como)
- Evitar o uso de else
- Evitar o uso de if encadeado
- Executar casos de uso mais simples e curtos primeiros
Alguns exemplos baseados nos nossos sistemas.
Funções com devem se divididas em dois tipos, orquestradores e funções com lógica, sempre separando o que se quer fazer, de como se faz (o que, do como)
Atual:
func (s *PrinterService) print(ctx context.Context, device *DeviceResponse, payload io.Reader) (err error) {
if networkInterfaces := device.NetworkInterfaces; networkInterfaces != nil {
if validateError := s.validateHostAndType(device); validateError == nil {
ctx = context.WithValue(ctx, "device.name", device.Name)
port := 9100
capabilitySupported := false
capabilities := device.CapabilitiesId
var content []byte
if content, err = ioutil.ReadAll(payload); err != nil {
return errors.Wrap(decodingContentErrorCode, err).WithContext("device_id", device.ID)
}
for _, capability := range capabilities {
switch capability {
case zplPrinterType:
err = s.ZebraPrinterDevice.Print(ctx, networkInterfaces[0].Host, port, content)
capabilitySupported = true
break
case rawPrinterType:
err = s.LaserPrinterDevice.Print(ctx, networkInterfaces[0].Host, port, content)
capabilitySupported = true
break
}
}
if !capabilitySupported {
err = errors.New(unsupportedCapability).
WithStatus(http.StatusInternalServerError).
WithContext("capabilities", strings.Join(capabilities, ",")).
WithContext("device", device)
}
s.PublisherOperation.PublishEvent(ctx, content, buildDeviceByResponse(device), err) : // 4 = Publica status
if err != nil {
logger.Error(endpointLog).Log(err, "device_id", device.ID, "capabilities", strings.Join(capabilities, ","), "warehouse_id", device.LogisticCenterID)
}
return err
} else {
logger.Error(endpointLog).Log(validateError)
return validateError
}
}
err = errors.New(invalidPortErrorCode).
WithMessage(invalidHostPortMsgError).
WithStatus(http.StatusPreconditionFailed).
WithContext("device", device)
logger.Error(endpointLog).Log(err)
return
}
func (s *PrinterService) print(ctx context.Context, device *DeviceResponse, payload io.Reader) error {
if err := validatePrint(ctx, device); err != nil {
return err
}
err := executePrint(ctx, device, payload)
publishStatusPrinter(device, err)
return err
}
Atual:
func (s *PrinterService) print(ctx context.Context, device *DeviceResponse, payload io.Reader) (err error) {
if networkInterfaces := device.NetworkInterfaces; networkInterfaces != nil { // 1. Monta telemetria
telemetry.AddAttributes(ctx,
"logisticCenterId", device.LogisticCenterID,
"ip", device.NetworkInterfaces[0].Host,
"deviceName", device.Name,
"deviceID", device.ID,
"capabilities", strings.Join(device.CapabilitiesId, ", "),
)
if validateError := s.validateHostAndType(device); validateError == nil { // 2. Valida
ctx = context.WithValue(ctx, "device.name", device.Name) // 3. Monta request para impressora
port := 9100
capabilitySupported := false
capabilities := device.CapabilitiesId
rc, ok := payload.(io.ReadCloser)
if !ok && payload != nil {
rc = ioutil.NopCloser(payload)
}
defer rc.Close()
var content []byte
if content, err = ioutil.ReadAll(rc); err != nil {
return errors.Wrap(decodingContentErrorCode, err).WithContext("device_id", device.ID)
}
for _, capability := range capabilities {
switch capability {
case zplPrinterType: // 4. Imprime ZPL
err = s.ZebraPrinterDevice.Print(ctx, networkInterfaces[0].Host, port, content)
capabilitySupported = true
break
case rawPrinterType: // 5. Imprime Laser
err = s.LaserPrinterDevice.Print(ctx, networkInterfaces[0].Host, port, content)
capabilitySupported = true
break
}
}
if !capabilitySupported { // 2. Valida
err = errors.New(unsupportedCapability). // 7. Monta erro
WithStatus(http.StatusInternalServerError).
WithContext("capabilities", strings.Join(capabilities, ",")).
WithContext("device", device)
}
s.PublisherOperation.PublishEvent(ctx, content, buildDeviceByResponse(device), err) // 6. Publica status
if err != nil {
logger.Error(endpointLog).Log(err, "device_id", device.ID, "capabilities", strings.Join(capabilities, ","), "warehouse_id", device.LogisticCenterID)
}
return err
} else {
logger.Error(endpointLog).Log(validateError)
return validateError
}
}
// 7. Monta erro
err = errors.New(invalidPortErrorCode).
WithMessage(invalidHostPortMsgError).
WithStatus(http.StatusPreconditionFailed).
WithContext("device", device)
logger.Error(endpointLog).Log(err)
return
}
Refatorado:
Isso pode ser resolvido como o exemplo do tópico anterior, separando o que se quer fazer de como faz, quebrando as funções de como faz em sua responsabilidade única.
Atual:
// Aqui existe uma questão de concepção também, endpoint possui regra de negócio, qual deveria estar no service
func (h *httpHandler) PostByPrinterType(w http.ResponseWriter, r *http.Request) {
printerType := server.URLParam(r, "printertype")
if err := h.validatePrinterType(printerType, r); err == nil {
if printerRequest, err := h.validatePost(r); err == nil {
if err = h.service.PrintByWorkstation(r.Context(), printerRequest, printerType); err == nil {
logger.Info(endpointLog).Log("PrintByWorkstation Succeeded",
"shipment_id", printerRequest.ShipmentID,
"workstation_id", printerRequest.WorkstationID,
"warehouse_id", printerRequest.WarehouseID,
"status", http.StatusOK)
w.WriteHeader(http.StatusOK)
} else {
render.Render(w, r, err)
}
} else {
render.Render(w, r, err)
}
} else {
render.Render(w, r, err)
}
}
Refatorado:
func (h *httpHandler) PostByPrinterType(w http.ResponseWriter, r *http.Request) {
printerType := server.URLParam(r, "printertype")
if err := validatePrint(ctx, device); err != nil {
render.Render(w, r, err)
return
}
printerRequest, err := decodeBody(r)
if err != nil {
render.Render(w, r, err)
return
}
if err := h.service.PrintByWorkstation(r.Context(), printerType); err != nil {
render.Render(w, r, err)
return
}
logPrintSucceeded(printerRequest)
w.WriteHeader(http.StatusOK)
}
Refatorado com alteração para o service ter regra de negócio:
func (h *httpHandler) PostByPrinterType(w http.ResponseWriter, r *http.Request) {
printerType := server.URLParam(r, "printertype")
printerRequest, err := decodeBody(r)
if err != nil {
render.Render(w, r, err)
return
}
if err := h.service.PrintByWorkstation(r.Context(), printerType); err != nil {
render.Render(w, r, err)
return
}
logPrintSucceeded(printerRequest)
w.WriteHeader(http.StatusOK)
}
Atual:
func (h *httpHandler) PostByDeviceId(w http.ResponseWriter, r *http.Request) {
if idDevice, err := strconv.Atoi(server.URLParam(r, "deviceid")); err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
} else {
if err := h.service.PrintByDeviceId(r.Context(), idDevice, r.Body); err != nil {
render.Render(w, r, err)
} else {
logger.Info(endpointLog).Log("PrintByDeviceId Succeeded", "deviceId", idDevice)
w.WriteHeader(http.StatusOK)
}
}
}
Refatorado:
func (h *httpHandler) PostByDeviceId(w http.ResponseWriter, r *http.Request) {
idDevice, err := strconv.Atoi(server.URLParam(r, "deviceid"))
if err != nil {
w.WriteHeader(http.StatusUnprocessableEntity)
return
}
if err := h.service.PrintByDeviceId(r.Context(), idDevice, r.Body); err != nil {
render.Render(w, r, err)
return
}
logger.Info(endpointLog).Log("PrintByDeviceId Succeeded", "deviceId", idDevice)
w.WriteHeader(http.StatusOK)
}
Atual:
func (p *Publisher) Publish(ctx context.Context, routeData *Data) (notified bool, err error) {
var errors []error
for _, route := range routeData.RouteItems {
if notifiedSorter, err := p.notifySorters(ctx, route); err != nil {
tags := BuildRouteItemTags(route)
tags = append(tags, "route_posted", "error")
tags = append(tags, "error", err)
loggerctx.Error(ctx).Log("route", tags...)
errors = append(errors, err)
} else if notifiedSorter {
notified = true
}
}
if len(errors) == 0 {
return notified, nil
}
err = p.parseNotifyItemsSortersErrors(errors)
return notified, err
}
Refatorado:
func (p *Publisher) Publish(ctx context.Context, routeData *Data) (notified bool, err error) {
var errors []error
for _, route := range routeData.RouteItems {
if notifiedSorter, err := p.notifySorters(ctx, route); err == nil {
notified = true
continue
}
tags := BuildRouteItemTags(route)
tags = append(tags, "route_posted", "error")
tags = append(tags, "error", err)
loggerctx.Error(ctx).Log("route", tags...)
errors = append(errors, err)
}
if len(errors) == 0 {
return notified, nil
}
err = p.parseNotifyItemsSortersErrors(errors)
return notified, err
}
- Muitas linhas
- Passagem de argumentos booleanos
- Mais de 1 ou 2 linhas dentro do For
- Níveis lógicos diferentes, juntando "o que faz" com "como faz", mesma função que orquestra coisas, faz coisas
- Além dos pontos antes citados:
- Funções com responsabilidade única
- Funções com devem se divididas em dois tipos, orquestradores e funções com lógica, sempre separando o que se quer fazer, de como se faz (o que, do como)
- Evitar o uso de else
- Evitar o uso de if encadeado
- Executar casos de uso mais simples e curtos primeiros
- Algumas outras recomendações:
- Funções com máximo 10 linhas
- Funções com máximo 2 níveis de indentação
- Funções com complexidade ciclomática maxima 5
- abcgo - métricas ABC para Go
- gocyclo - Computa e verifica a complexidade ciclomática das funções
- splint - Localiza qualquer função que seja muito longa ou que tenha muitos parâmetros ou resultados
- go-checkstyle - Ferramenta de verificação de estilo como o java checkstyle. Esta ferramenta inspirada em java checkstyle, golint. O estilo se refere a alguns pontos em já bem difundidos em Go
- go-cleanarch - Valida as regras de Arquitetura Limpa, como a Regra de Dependência e a interação entre pacotes em Go
- gosimple - Linter para o código fonte do Go, especializado na simplificação de código
- nofuncflags - Mostra as funções com parâmetros booleanos (flags)
- go-mnd - Detector de números mágicos para Go
Lembre-se, filho feio não tem pai e da regra do escoteiro: Deixe o código mais limpo do que estava antes de você mexer nele.
Adicionar referencias!