Skip to content

Instantly share code, notes, and snippets.

@justiandre
Last active March 15, 2022 11:55
Show Gist options
  • Save justiandre/d8892c40e22eb0cd5cc8907599161b6e to your computer and use it in GitHub Desktop.
Save justiandre/d8892c40e22eb0cd5cc8907599161b6e to your computer and use it in GitHub Desktop.

Nível de indentação e complexidade em funções GO

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.


Problema

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.


Algumas práticas que podem melhorar 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

Exemplos

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
}

Refatorado:

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
}

Funções com responsabilidade única

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.

Evitar o uso de else

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)
}

Evitar o uso de if encadeado

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)
}

Executar casos de uso mais simples e curtos primeiros

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
}

Alguns sinais de funções com mais de uma responsabilidade

  • 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

Mais algumas recomendações para nossos projetos

  • 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

Algumas ferramentas que podem ser usadas para monitorar o código:

  • 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.

@justiandre
Copy link
Author

Adicionar referencias!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment