[JAVA] Explain DI in an obese story

Introduction

This article is the 12th day article of Java Advent Calendar 2018.

It's been about half a year since I came into contact with Java DI. I was in a hurry to teach new members of the team, so I will summarize it in an article for my review.

Here is a sample project. https://github.com/segurvita/FatnessChecker

Target of this article

For those who understand classes and constructors, but don't know DI well.

I'll try to explain it with only classes and constructors so that I can understand it without knowing Bean or ʻInterface`.

I don't understand the meaning of "DI means dependency injection"

―― “DI is an abbreviation for Dependency Injection” ← Hmm Hmm ―― “Dependency Injection means Dependency Injection” ←? ?? ?? -"Strictly speaking, it is the injection of dependency objects" ←? ??

The purpose of this article is to understand the meaning of this ** Dependency Injection **.

DI means not using new in the class!

Don't be afraid to misunderstand, ** DI means don't use new in a class **. (I understand that it is not an accurate expression, but this article will be explained with an emphasis on clarity.)

Non-DI pattern

public class Hoge{
    //Field (member variable)
    Fuga piyo;
    
	//Constructor
	public Hoge() {
	    //I'm using new in my class.
		this.piyo = new Fuga();
	}
}

DI pattern

public class Hoge{
    //Field (member variable)
    Fuga piyo;
    
	//Constructor
	public Hoge(Fuga mogera) {
	    //Do not use new. I'm injecting from the outside.
		this.piyo = mogera;
	}
}

However, I do not understand the merit with this alone. From here, I will explain in a story format.

Episode 1: I went to an obesity judgment company

A story that starts suddenly

: man: Am I fat? That's it! Let's ask an obesity judgment company for inspection!

After several hours

: person_frowning: Welcome to the obesity assessment company. What kind of business do you have today?

: man: I want to judge the degree of obesity.

: person_frowning: You are a customer who judges the degree of obesity. Is it okay to ask for height and weight?

: man: Well, I'm 170 cm tall and weigh 70 kg.

: person_frowning: I'm clever. Please wait a moment. We will calculate BMI with our latest robot.

: robot: Calculation Shimasu ・ ・ ・ 70kg ÷ 1.70m de, BMI Ha ** 41.18 ** Death!

: person_frowning: If your BMI is 41.18 ... it's over 40, so it's ** 4 degrees obese **.

: man: Yeah! Such!

After several hours

: man: It's absolutely strange that I'm obese. Isn't that robot a bug?

What is BMI

BMI (body mass index) is calculated by weight (kg) ÷ height (m) ÷ height (m).

Is the calculation of: robot: wrong?

What is obesity judgment?

In Japan, people with a BMI of 25 or above are considered ** obese **. There are four stages of obesity, from 1 to 4 degrees.

Image quote: OMRON / vol.103 Revised diagnostic criteria for obesity for the first time in 11 years

Your role

The characters who appeared in the first episode are as follows.

-: Man: is a person who wants to know his or her obesity level. Since it is a user, we will call it ʻUser. -: Person_frowning: is the reception desk of the obesity judgment company. Do not calculate BMI yourself, but ask: robot: to do it. The degree of obesity can be determined based on the BMI value. Let's call it FatnessChecker. -: Robot: is a robot that calculates BMI. There is still a bug in the prototype. Let's call it BmiRobot`.

Problem of the case of the first episode

: robot: BmiRobot made a mistake in calculating BMI. Apparently there is a bug.

Based on this wrong calculation result,: person_frowning: FatnessChecker made an obesity judgment, so the judgment result was also wrong.

In other words, the problem is that: person_frowning: FatnessChecker relied on an unreliable robot called: robot: BmiRobot.

Episode 2: Appearance of BMI Sennin

: man: It's absolutely strange that I'm obese. Isn't that robot a bug?

: older_man: Is that person having trouble with BMI calculation?

: man: Who are you!

: older_man: I'm a BMI hermit. Please tell me the story if you like.

: man: Yeah, it's actually a secret ...

A few days later

: information_desk_person: Welcome to the obesity assessment company. (Omitted) May I ask you about your height and weight?

: man: Height is 170 cm and weight is 70 kg. However, I will ask this person to calculate the BMI, not the robot the other day.

: older_man: Hohoho, in this case 70kg ÷ 1.70m ÷ 1.70m, BMI is ** 24.22 **.

: information_desk_person: I'm clever. If your BMI is 24.22 ... it's more than 18.5 and less than 25, so it's ** normal weight **.

: man: I did it! I'm not obese at the last minute!

Your role

The characters who appeared in the second episode are as follows.

-: Man: is a person who wants to know his or her obesity level. Since it is a user, we will call it ʻUser. -: Older_man: is a master at calculating BMI. I've trained a lot, so I'll never make a mistake in the calculation. Let's call it BmiMaster. -: Information_desk_person: is a person who uses: older_man: to determine the degree of obesity. It is the same person as: person_frowning :, but for the sake of explanation, we will distinguish it and call it FatnessCheckerDi`.

DI solution

: man: ʻUserasked: information_desk_person:FatnessCheckerDi to determine the degree of obesity under the condition that: older_man: BmiMaster` should calculate the BMI.

As a result, the judgment result of: information_desk_person: FatnessCheckerDi depends on the reliable person:: older_man: BmiMaster.

This is ** DI ** (Dependency Injection)! In other words

: man: ʻUserinjects: older_man:BmiMaster into: information_desk_person: FatnessCheckerDi`!

Benefits of DI

The advantage of DI is that ** you can unit test the part under development even if the dependent part is not completed **.

In this case, there was a bug in: robot: BmiRobot, so if it didn't work, it would be okay if we did enough testing and reduced the bug in: robot: BmiRobot. ?? I think there is also an opinion. (I thought so at first.)

However, it will take some time to ** test enough **.

: person_frowning: FatnessChecker depends on: robot: BmiRobot, so you can't test: person_frowning: FatnessChecker until: robot: BmiRobot is complete. This will increase development time.

On the other hand,: information_desk_person: FatnessCheckerDi allows the dependent parts (: older_man: BmiMaster) to be injected from the outside (: man: ʻUser). By doing this, even if the dependent part (: older_man: BmiMaster) is not completed, if you inject a mock (test-only haribote) of the dependent part (: older_man: BmiMaster),: information_desk_person: Unit testing of FatnessCheckerDi` is possible.

Disadvantages of DI

The disadvantage is that ** the learning cost is high **.

In the case of team development, if DI is adopted in the project under development, all members will need to learn DI. Education costs can be non-negligible for teams with a lot of member turnover.

Sample code

I think that it is difficult to convey with just sentences, so I will explain it with Java code.

Here is a sample project. https://github.com/segurvita/FatnessChecker

: robot: When calculated with BmiRobot (before DI introduction)

First,: man: ʻUser` looks like this.

User.java


/**
 *People who want to know the degree of obesity
 */
public class User {
	/**
	 *Ask an obesity judgment company to make a judgment without a BMI master.
	 */
	public void runWithoutBmiMaster() {
		//Start a conversation with a person from an obesity judgment company.
		FatnessChecker fatnessChecker = new FatnessChecker();

		//Tell them your height and weight and ask them to determine your obesity.
		String result = fatnessChecker.check(170.0, 70.0);

		//The judgment result of the degree of obesity is displayed.
		System.out.println("Obesity judgment result (without BMI master):" + result);
	}
}

You are requesting work at fatnessChecker.check.

Then: person_frowning: FatnessChecker looks like this:

FatnessChecker.java


/**
 *Obesity judgment company (using BMI robot)
 */
public class FatnessChecker {
	/**
	 *Determine BMI
	 * @param height height[cm]
	 * @param weight weight[kg]
	 * @return obesity
	 */
	public String check(double height, double weight) {
		//Secure one of the latest BMI robots in the company.
		BmiRobot bmiRobot = new BmiRobot();

		//Ask the BMI robot to calculate BMI.
		double bmi = bmiRobot.calc(height, weight);

		//Determine the degree of obesity from the BMI calculation results.
		if (bmi < 18.5) {
			return "Underweight";
		} else if (bmi < 25.0) {
			return "Normal weight";
		} else if (bmi < 30.0) {
			return "Once obese";
		} else if (bmi < 35.0) {
			return "Twice obese";
		} else if (bmi < 40.0) {
			return "3rd degree obesity";
		} else {
			return "4th degree obesity";
		}
	}
}

In the check method,new BmiRobot ()is used to reserve one: robot: BmiRobot.

After that, BMI calculation is requested by BmiRobot.calc (height, weight), and the degree of obesity is judged based on the calculation result.

If there is a bug in: robot: BmiRobot, the judgment result will be wrong. : robot: Depends on BmiRobot.

That: robot: BmiRobot is implemented, for example:

BmiRobot.java


/**
 *BMI robot (with bugs)
 */
public class BmiRobot {
	/**
	 *Calculate BMI
	 * @param height height[cm]
	 * @param weight weight[kg]
	 * @return BMI
	 */
	public double calc(double height, double weight) {
		//body weight[kg]÷ Height[m]
		return weight * 100 / height;
	}
}

The formula is Weight [kg] ÷ Height [m], which is incorrect.

If you run the program in this state, the result will be

Execution result


Obesity judgment result (without BMI master): 4th degree obesity

It will be.

: older_man: When calculated with BmiMaster (after DI introduction)

First,: man: ʻUser` looks like this.

User.java


/**
 *People who want to know the degree of obesity
 */
public class User {
	/**
	 *With a BMI master, ask an obesity judgment company to make a judgment.
	 */
	public void runWithBmiMaster() {
		//Secure one BMI master.
		BmiMaster bmiCalculator = new BmiMaster();

		//Ask a person from an obesity assessment company to make the BMI calculation a BMI master.
		FatnessCheckerDi fatnessCheckerDi = new FatnessCheckerDi(bmiCalculator);

		//Tell them your height and weight and ask them to determine your obesity.
		String result = fatnessCheckerDi.check(170.0, 70.0);

		//The judgment result of the degree of obesity is displayed.
		System.out.println("Obesity judgment result (with BMI master):" + result);
	}
}

The big difference from the previous one is that: man: ʻUser uses new BmiMaster () and reserves one: older_man: BmiMaster. This is passed to: information_desk_person: FatnessCheckerDi with new FatnessCheckerDi (bmiCalculator) `.

Then: information_desk_person: FatnessCheckerDi looks like this:

FatnessCheckerDi.java


/**
 *Obesity judgment company (using BMI master)
 */
public class FatnessCheckerDi {
	/**
	 *BMI master
	 */
	final private BmiMaster bmiMaster;

	/**
	 *constructor
	 * @param bmiCalculator BMI master named by the user
	 */
	public FatnessCheckerDi(BmiMaster bmiMaster) {
		//We welcome the BMI master nominated by the user.
		this.bmiMaster = bmiMaster;
	}

	/**
	 *Determine BMI
	 * @param height height[cm]
	 * @param weight weight[kg]
	 * @return obesity
	 */
	public String check(double height, double weight) {
		//Ask the BMI master nominated by the user to calculate the BMI.
		double bmi = this.bmiMaster.calc(height, weight);

		//Determine the degree of obesity from the BMI calculation results.
		if (bmi < 18.5) {
			return "Underweight";
		} else if (bmi < 25.0) {
			return "Normal weight";
		} else if (bmi < 30.0) {
			return "Once obese";
		} else if (bmi < 35.0) {
			return "Twice obese";
		} else if (bmi < 40.0) {
			return "3rd degree obesity";
		} else {
			return "4th degree obesity";
		}
	}
}

The big difference from the previous one is the appearance of the constructor.

Earlier, in the check method, we reserved one: robot: BmiRobot, but this time we receive: older_man: BmiMaster as an argument of the constructor and use it as a field (member variable). ) Is assigned.

After that, we request BMI calculation with this.bmiMaster.calc (height, weight) and judge the degree of obesity based on the calculation result.

Now it depends on: older_man: BmiMaster.

That: older_man: BmiMaster is implemented, for example:

BmiMaster.java


/**
 *BMI Master (Master)
 */
public class BmiMaster {
	/**
	 *Calculate BMI
	 * @param height height[cm]
	 * @param weight weight[kg]
	 * @return BMI
	 */
	public double calc(double height, double weight) {
		//body weight[kg] ÷ (height[m])^2
		return weight * 10000 / (height * height);
	}
}

The formula is weight [kg] ÷ (height [m]) ^ 2. This is the correct formula.

If you run the program in this state, the result will be

Execution result


Obesity judgment result (with BMI master): Normal weight

It will be.

Bonus: Where on earth should I new?

In this story,: man: ʻUser prepares: older_man: BmiMaster (new) and injects it into: information_desk_person: FatnessCheckerDi`.

So if: man: ʻUseritself was also designed with a DI pattern, who would prepare: older_man:BmiMaster`?

: man: ʻWill there be another class outside of User`? What if even that class was designed with a DI pattern?

That's where the ** DI container ** comes in. Undertakes the processing of new at once.

In the case of Java Spring, BeanFactory corresponds to this.

at the end

What did you think. I tried to explain DI in a story style.

When I first touched DI half a year ago, I was buried in unknown terms such as ʻAutowired and BeanFactory`, and it became "DI difficult ...". (At that time, there was no distinction between DI and DI container.)

However, when I coded it for half a year and looked back, I thought, "Isn't it possible to explain DI without knowing that it's actually Bean? ", So I wrote this article for myself half a year ago. I tried it.

I hope it will be helpful to you.

Reference site

I referred to the following site.

-DI / DI container, do you understand properly ...?

Recommended Posts

Explain DI in an obese story
The story of an Illegal State Exception in Jetty.
[Docker] The story that an error occurred in docker-compose up