Preparando para o Spring: composição de objetos com Java - exercício resolvido

Foto do autor Nelio Alves
Nelio Alves
Compartilhar
Compartilhar no LinkedInCompartilhar no FacebookCompartilhar no XCompartilhar no WhatsApp
Imagem banner do post

Muitas pessoas que estão se preparando para trabalhar com frameworks como Spring se perguntam: "Será que eu realmente estou pronto para construir ou entender um código profissional?". É comum focar apenas em anotações, configurações e integrações, mas a verdade é que a base de tudo continua sendo modelagem de domínio e orientação a objetos bem aplicada. Neste artigo, vamos desenvolver um exercício completo sobre composição de objetos em Java, implementar toda a solução passo a passo e discutir cada decisão tomada. Este conteúdo será um excelente treinamento para quem deseja atuar profissionalmente com Spring.

Um exercício para praticar e reforçar seu conhecimento

Na era da inteligência artificial, torna-se ainda mais importante conhecer os fundamentos de programação, para que a IA se torne um acelerador do seu trabalho, e não uma muleta.

Neste conteúdo, vamos resolver um exercício de programação Java, para colocar em prática vários fundamentos importantes:

  • Modelagem orientada a objetos
  • Composição de objetos
  • Enumerações
  • Encapsulamento
  • Regra de negócio dentro do domínio
  • Uso da lib moderna de datas

Será um estudo de caso valioso para ajudar a consolidar seu conhecimento nesses fundamentos, que vão te dar a base para atuar profissionalmente.

Enunciado do exercício

Ler os dados de um trabalhador com N contratos (N fornecido pelo usuário). Depois, solicitar do usuário um mês/ano e mostrar qual foi o salário do funcionário nesse mês, conforme exemplo:

Copiar
Entre o nome do departamento: Design
Entre os dados do trabalhador:
Nome: Alex
Nivel: MID_LEVEL
Salario base: 1200.00
Quantos contratos esse trabalhador tem? 3
Entre com os dados do contrato #1:
Data (DD/MM/YYYY): 20/08/2018
Valor por hora: 50.00
Duracao (horas): 20
Entre com os dados do contrato #2:
Data (DD/MM/YYYY): 13/06/2018
Valor por hora: 30.00
Duracao (horas): 18
Entre com os dados do contrato #3:
Data (DD/MM/YYYY): 25/08/2018
Valor por hora: 80.00
Duracao (horas): 10

Entre o ano e mes para calcular o ganho (MM/YYYY): 08/2018
Nome: Alex
Departamento: Design
Ganho para 08/2018: 3000.00

O resultado foi 3000 porque o Alex tem salário base de 1200, somado ao contrato #1 de 08/2018 no valor de 1000 (50 × 20) e ao contrato #3 de 08/2018 no valor de 800 (80 × 10). Assim:

1200 + 1000 + 800 = 3000

Modelo de domínio

A solução deverá implementar um modelo de domínio conforme o diagrama de classes a seguir. A implementação deste modelo e a posterior instanciação dos objetos será nossa aplicação prática de composição de objetos.

Aqui temos dois pontos fundamentais de composição:

  • Um Worker possui um Department.
  • Um Worker possui vários HourContract.

Isso indica que o trabalhador possui associações com departamento e contratos. Isso fará com que, ao instanciar os objetos de um trabalhador, seu departamento e seus contratos, isso resultará na memória do computador vários objetos associados entre si conforme imagem a seguir.

Esse arranjo de objetos associados será a aplicação prática da "composição de objetos" de nosso exercício.

Curiosidade: composição de objetos de dados vs. composição de objetos de componentes

Repare que, em nosso exercício, vamos fazer uma composição de objetos que carregam DADOS do domínio de nossa aplicação.

Porém, a composição de objetos também pode ser de objetos que representam COMPONENTES do sistema, ou seja, objetos que tem a responsabilidade não de carregar dados, mas sim de executar operações.

Por exemplo, podemos ter um componente InvoiceService responsável por realizar operações com faturas (invoice = fatura em inglês), por exemplo processar uma nova fatura, consultar faturas, e assim por diante. Se, por exemplo, na operação de processar uma fatura, for preciso enviar um email para os envolvidos, então o provavelmente vai haver uma composição do componente InvoiceService com outro componente EmailService responsável por enviar o email.

Desta forma, fazemos a composição de vários componentes do sistema para realizar as operações de negócio deste sistema.

Em Spring você verá isso o tempo todo: composição de objetos de dados, e composição de objetos de componentes. Mas isso é assunto para outro momento 😊

Começando a solução do nosso exercício: criando o projeto

Neste momento, eu vou presumir que você já tem um conhecimento básico de Java, e já sabe criar um projetinho básico Java na IDE de sua preferência.

Crie um novo projeto Java na sua IDE.

Implementando Department

Agora vamos implementar os tipos exatamente como definidos no modelo. Vamos começar pela classe Department, dentro de um pacote entities. É uma classe simples que representa um conceito do domínio. Observe o encapsulamento com atributo privado e getters/setters.

Copiar
package entities;

public class Department {

	private String name;

	public Department() {
	}

	public Department(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

Implementando HourContract

Agora vamos implementar a classe HourContract, conforme projeto no diagrama. Esta classe representa um contrato de horas de um trabalhador. Aqui usamos LocalDate, reforçando boas práticas modernas da API java.time. Repare também que, além dos métodos getters/setters, há também o método totalValue que retorna o valor total do contrato.

Copiar
package entities;

import java.time.LocalDate;

public class HourContract {

	private LocalDate date;
	private Double valuePerHour;
	private Integer hours;
	
	public HourContract() {
	}

	public HourContract(LocalDate date, Double valuePerHour, Integer hours) {
		this.date = date;
		this.valuePerHour = valuePerHour;
		this.hours = hours;
	}

	public LocalDate getDate() {
		return date;
	}

	public void setDate(LocalDate date) {
		this.date = date;
	}

	public Double getValuePerHour() {
		return valuePerHour;
	}

	public void setValuePerHour(Double valuePerHour) {
		this.valuePerHour = valuePerHour;
	}

	public Integer getHours() {
		return hours;
	}

	public void setHours(Integer hours) {
		this.hours = hours;
	}
	
	public double totalValue() {
		return valuePerHour * hours;
	}
}

Implementando WorkerLevel

Agora vamos implementar o tipo enumerado WorkerLevel, que representa um conjunto fechado de valores possíveis para o nível de um trabalhador. Muito comum em sistemas reais.

Copiar
package entities;

public enum WorkerLevel {

	JUNIOR,
	MID_LEVEL,
	SENIOR;
}

Implementando Worker

Agora vamos implementar a classe Worker, que representa um trabalhador. Aqui está o coração do exercício. Observe que:

  • Worker possui um atributo department do tipo Department.
  • Worker possui um atributo contracts do tipo List<HourContract>, ou seja, uma lista de contratos.
  • O método income encapsula a regra de cálculo de quanto o trabalhador ganhou em um dado ano e mês.
Copiar
package entities;

import java.util.ArrayList;
import java.util.List;

public class Worker {

	private String name;
	private WorkerLevel level;
	private Double baseSalary;
	
	private Department department;
	private List<HourContract> contracts = new ArrayList<>();
	
	public Worker() {
	}

	public Worker(String name, WorkerLevel level, Double baseSalary, Department department) {
		this.name = name;
		this.level = level;
		this.baseSalary = baseSalary;
		this.department = department;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public WorkerLevel getLevel() {
		return level;
	}

	public void setLevel(WorkerLevel level) {
		this.level = level;
	}

	public Double getBaseSalary() {
		return baseSalary;
	}

	public void setBaseSalary(Double baseSalary) {
		this.baseSalary = baseSalary;
	}

	public Department getDepartment() {
		return department;
	}

	public void setDepartment(Department department) {
		this.department = department;
	}

	public List<HourContract> getContracts() {
		return contracts;
	}

	public void addContract(HourContract contract) {
		contracts.add(contract);
	}
	
	public void removeContract(HourContract contract) {
		contracts.remove(contract);
	}
	
	public double income(int year, int month) {
		double sum = baseSalary;
		for (HourContract c : contracts) {
			if (year == c.getDate().getYear() && month == c.getDate().getMonthValue()) {
				sum += c.totalValue();
			}
		}
		return sum;
	}
}

Programa de teste rápido

Agora que nosso modelo de domínio está todo implementado, vamos criar um pequeno programa principal, por meio de uma classe Program, dentro de um pacote app, para testar rapidamente a instanciação dos objetos, bem como o cálculo do ganho do trabalhador em um dado ano e mês.

Vamos instanciar os objetos diretamente no código para facilitar esta etapa. Repare que estamos instanciando exatamente o modelo de objetos mostrato anteriormente, com o departamento "Design", o trabalhador "Alex" e seus três contratos.

Estamos usando aqui o padrão de código válido a partir do Java 25, usando IO ao invés de System.out, e também a notação simplificada void main() para o método main do programa principal.

Execute este pequeno programa e verifique se o programa vai apresentar a saída correta.

Copiar
package app;

import java.time.LocalDate;
import java.util.Locale;

import entities.Department;
import entities.HourContract;
import entities.Worker;
import entities.WorkerLevel;

public class Program {

	void main() {

		Locale.setDefault(Locale.US);

		Department dept = new Department("Design");

		Worker worker = new Worker("Alex", WorkerLevel.MID_LEVEL, 1200.0, dept);
		
		HourContract contract1 = new HourContract(LocalDate.of(2018, 8, 20), 50.0, 20);
		HourContract contract2 = new HourContract(LocalDate.of(2018, 6, 13), 30.0, 18);
		HourContract contract3 = new HourContract(LocalDate.of(2018, 8, 25), 80.0, 10);
		
		worker.addContract(contract1);
		worker.addContract(contract2);
		worker.addContract(contract3);

		IO.println("Nome: " + worker.getName());
		IO.println("Departamento: " + worker.getDepartment().getName());
		IO.println("Ganho para 08/2018: " + String.format("%.2f", worker.income(2018, 8)));
	}
}

Versão final com entrada do usuário

Agora que nosso modelo de domínio foi validado, vamos atualizar o programa principal com a lógica para interagir com o usuário, solicitando os dados de entrada.

Execute o programa, informe os dados do exemplo fornecido anteriormente, e confira os resultados.

Copiar
package app;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Scanner;

import entities.Department;
import entities.HourContract;
import entities.Worker;
import entities.WorkerLevel;

public class Program {

	void main() {

		Locale.setDefault(Locale.US);
		Scanner sc = new Scanner(System.in);
		DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
		
		IO.print("Entre o nome do departamento: ");
		String departmentName = sc.nextLine();
		
		Department dept = new Department(departmentName);
		
		IO.println("Entre os dados do trabalhador:");
		IO.print("Nome: ");
		String workerName = sc.nextLine();
		IO.print("Nivel: ");
		WorkerLevel workerLevel = WorkerLevel.valueOf(sc.nextLine());
		IO.print("Salario base: ");
		double baseSalary = sc.nextDouble();
		
		Worker worker = new Worker(workerName, workerLevel, baseSalary, dept);
		
		IO.print("Quantos contratos esse trabalhador tem? ");
		int n = sc.nextInt();
		
		for (int i=1; i<=n; i++) {
			IO.println("Entre com os dados do contrato #" + i + ":");
			IO.print("Data (DD/MM/YYYY): ");
			LocalDate contractDate = LocalDate.parse(sc.next(), fmt);
			IO.print("Valor por hora: ");
			double valuePerHour = sc.nextDouble();
			IO.print("Duracao (horas): ");
			int hours = sc.nextInt();
			HourContract contract = new HourContract(contractDate, valuePerHour, hours);
			worker.addContract(contract);
		}
		
		IO.println();
		IO.print("Entre o ano e mes para calcular o ganho (MM/YYYY): ");
		String monthAndYear = sc.next();
		
		int month = Integer.parseInt(monthAndYear.substring(0, 2));
		int year = Integer.parseInt(monthAndYear.substring(3));
		
		IO.println("Nome: " + worker.getName());
		IO.println("Departamento: " + worker.getDepartment().getName());
		IO.println("Ganho para " + monthAndYear + ": " + String.format("%.2f", worker.income(year, month)));
		
		sc.close();
	}
}

Baixar o projeto no Github

Você pode pegar o projeto completo no Github.

Conclusão: por que isso é fundamental para Spring?

Novamente, neste exercício você praticou MUITOS fundamentos importantes:

  • Modelagem orientada a objetos
  • Composição de objetos
  • Enumerações
  • Encapsulamento
  • Regra de negócio dentro do domínio
  • Uso da lib moderna de datas

Em aplicações Spring reais, você terá entidades com relacionamentos, agregações, listas, enums e regras encapsuladas, exatamente como aqui.

Frameworks como Spring não substituem fundamentos. Eles potencializam fundamentos.

Se você consegue modelar, compor objetos e estruturar corretamente o domínio, você está no caminho certo para escrever código profissional.

Continue praticando. Continue modelando. Continue fortalecendo a base. É isso que diferencia quem apenas usa Spring de quem realmente domina o que está fazendo.

Foto do autor Nelio Alves
Nelio Alves
Desenvolvedor e Professor
Olá, meu nome é Nelio Alves. Sou graduado em Ciência da Computação e possuo mestrado e doutorado em Engenharia de Software pela Universidade Federal de Uberlândia. Trabalho como desenvolvedor e professor de programação há mais de 20 anos, e sou um dos educadores de tecnologia mais influentes da Internet com mais de 500 mil alunos online.