Свой тип
- Создаем приложение
- Добавляем класс длина
- Добавляем конструктор
- Приступаем к проверке кода
- Добавляем операции
Задача:
- Создать класс реализующий операции в соответствии с заданием
- Протестировать операции
для следующего задания:
Мера длины, задаваемая в виде пары (значение, тип), допустимые типы: метры, километры, мили
- сложение
- вычитание
- умножение на число
- сравнение двух объемов
- вывод значения в любом типе
Создаем приложение
получаем
package com.company;
public class Main {
public static void main(String[] args) {
// write your code here
}
}
Добавляем класс длина
Тут нам нет большой необходимости создавать класс под логику, так как мы решаем архитектурную задачу, поэтому будем создавать класс, который можно будет протестить.
И так создаем класс:
нзываем его Length
получим
package com.company;
public class Length {
}
Добавляем конструктор
У нас свой тип, который есть комбинация более простых типов. Со значением все просто это число, пусть будет типа double, а вот с мерой сложнее. Так как у нас комбинации меры представляет собой некоторый ограниченный набор значений то самым очевидным способом работы с ними будет использование перечислимого типа:
- “m” – измеряем в метрах
- “km” – измеряем в километрах
- “mi” – миля (англ. mile)
корректируем код:
package com.company;
public class Length {
/**
* добавим перечислимый тип прям внутрь класса
*/
public enum Measure {
m, // метры
km, // километры
mi // мили
};
}
Теперь что касается конструктора, нам понадобится два поля:
- double value – под значение
- Measure type – под тип меры
добавляем
public class Length {
public enum Measure { /* ... */ };
/**
* два новых приватных поля
*/
private double value;
private Measure type;
/**
* конструктор
*/
public Length(double value, Measure type) {
this.value = value;
this.type = type;
}
}
Давайте попробуем вывести что-нибудь в классе Main:
public class Main {
public static void main(String[] args) {
Length length = new Length(100, Length.Measure.km);
System.out.println(length);
}
}
получим
вот этот com.company.Length@7ef20235 это то как сейчас выводится наш класс в виде строкового значения, по умолчанию это имя класса + внутрений хэшкод объекта
теперь давайте добавим метод, который позволит нам выводить длину в более симпатичном виде. Для этого нам надо переопределить метод toString, делаем это:
public class Length {
public enum Measure { /* ... */ };
private double value;
private Measure type;
public Length(double value, Measure type) { /* ... */ }
/* переопределяем базовый метод toString */
@Override
public String toString() {
// так как тип у нас хранится в виде английской аббревиатуры,
// то тут преобразуем ее в русскую
String typeAsString = "";
switch (this.type) {
case m:
typeAsString = "м";
break;
case km:
typeAsString = "км";
break;
case mi:
typeAsString = "миля";
break;
}
/**
* Используем форматирование для вывода строки в виде 1м., 2км., 3мл.
* в принципе, можно написать this.value + typeAsString,
* но как только захочется сделать всякие пробелы все это будет превращается
* в уродливые this.value + " " + typeAsString
*/
return String.format("%s %s", this.value, typeAsString);
}
}
а теперь попробуем снова запустить класс Main, получим:
уже намного лучше.
Приступаем к проверке кода
Давайте добавим парочку тестов, которые проверит что на разных типах мер, выводится ожидаемый результат.
Сначала делаем зелененькую папку под тесты:
а дальше уже добавляем тесты
получаем
package com.company;
import static org.junit.jupiter.api.Assertions.*;
class LengthTest {
}
добавим тесты на все виды мер
class LengthTest {
@Test
public void testMToString() {
Length length = new Length(100, Length.Measure.m);
assertEquals("100.0 м.", length.toString());
}
@Test
public void testKmToString() {
Length length = new Length(100, Length.Measure.km);
assertEquals("100.0 км.", length.toString());
}
@Test
public void testMiToString() {
Length length = new Length(100, Length.Measure.mi);
assertEquals("100.0 мл.", length.toString());
}
}
Добавляем операции
Операция сложение с числом
Начнем с простого, с добавления числа. Операция будет работать по следующему правилу: добавление числа к расстоянию увеличивает значение на это число. Например:
- 1 м + 6.25 = 7.25м
- 1 км + 4 = 5км
Добавим операцию в наш класс, к сожалению, java не поддерживает перегрузку операторов так что придется обходится функциями. И так, добавляем
public class Length {
// ...
/**
* операция сложения с числом
*/
public Length add(double number) {
Length newLength = new Length(this.value + number, this.type);
return newLength;
}
}
Теперь добавим тест:
@Test
public void testAddNumber() {
Length length = new Length(100, Length.Measure.m);
length = length.add(5);
assertEquals("105.0 м.", length.toString());
}
запускаем и проверяем:
красота!
Операции вычитания, умножения, деления с числом
Делаем по аналогии:
public class Length
{
//...
/**
* операция вычитания числа
*/
public Length subtract(double number) {
return new Length(this.value - number, this.type);
}
/**
* операция умножения на число
*/
public Length multiply(double number) {
return new Length(this.value * number, this.type);
}
/**
* операция деления на число
*/
public Length divide(double number) {
return new Length(this.value / number, this.type);
}
}
ну и тесты соответственно
class LengthTest {
// ...
@Test
public void testAddNumber() { /* ... */ }
@Test
public void testSubtractNumber() {
Length length = new Length(100, Length.Measure.m);
length = length.subtract(5);
assertEquals("95.0 м.", length.toString());
}
@Test
public void testMultiplyByNumber() {
Length length = new Length(100, Length.Measure.m);
length = length.multiply(5);
assertEquals("500.0 м.", length.toString());
}
@Test
public void testDivideByNumber() {
Length length = new Length(100, Length.Measure.m);
length = length.divide(5);
assertEquals("20.0 м.", length.toString());
}
}
запускаем:
Преобразование в другой тип
Прежде чем сразу начать писать операцию сложения длин, заданных в разных типах, лучше сначала написать функцию, которая позволит конвертировать один тип в другой.
Самый простой способ — это преобразовывать в какой-нибудь самый привычный тип (например, метры), а затем выполнять обратное преобразование, и так:
- 1 км == \(1000\) метров
- 1 миля == \(1609,34\) м
создадим метод, который будет называться to (то бишь в по-русски и добавим его в класс Length)
public class Length
{
//...
public Length to(Measure newType) {
// по умолчанию новое значение совпадает со старым
double newValue = this.value;
// если текущий тип -- метр
if (this.type == Measure.m) {
// в зависимости от того во что преобразовываем
switch (newType) {
case m: // если метры
newValue = this.value;
break;
case km: // если километры
newValue = this.value / 1000;
break;
case mi: // если мили
newValue = this.value / 1609.34;
break;
}
}
// ну и результат у нас это Length
return new Length(newValue, newType);
}
}
добавим тест для проверки конвертации из метра в другие величины:
class LengthTest {
// ...
@Test
public void testConvertMeterToAny() {
Length length;
length = new Length(1000, Length.Measure.m);
assertEquals("1.0 км.", length.to(Length.Measure.km).toString());
length = new Length(1609.34 * 2, Length.Measure.m);
assertEquals("2.0 мл.", length.to(Length.Measure.mi).toString());
}
}
запускаем, ликуем:
и идем дальше.
Давайте подумаем, нам надо уметь преобразовывать из любого типа в любой другой, то есть с учетом 3 типов, и преобразование из себя в самого себя мы считаем за конвертацию. Нам надо добавить еще 2 * 3 case конструкций. И в каждом правильно все рассчитать.
И если в данном примере всего 6 вариантов событий, то в заданиях количество вариантов может уходить за 10 и более. Иначе говоря, перспектива безрадостная. Поэтому воспользуемся головой, и вместо того чтобы решать задачу 6 раз воспользуемся древним математическим подходом: сведем задач к предыдущей.
То есть вместо того чтобы пытаться понять сколько миль в 1.176 километрах, и долго и нудно считать это на бумажке, мы преобразуем мили в метры, а потом уже метры в километры. То есть таким образом вместо того чтобы добавлять 6 различных случаев нам придется добавить только 3.
И так, добавляем:
public Length to(Measure newType) {
double newValue = this.value;
if (this.type == Measure.m) {
/* ... это не трогаем ... */
} else if (newType == Measure.m) {
// а это новый код, который добавляем
// в зависимости от того во что преобразовываем
switch (this.type) {
case m: // если в метры, кстати это лишний пункт, он никогда не случится
newValue = this.value;
break;
case km: // если километры
newValue = this.value * 1000;
break;
case mi: // если мили
newValue = this.value * 1609.34;
break;
}
}
// это оставляем
return new Length(newValue, newType);
}
мы уже почти все, добавляем тест:
@Test
public void testConvertAnyToMeter() {
Length length;
length = new Length(1, Length.Measure.km);
assertEquals("1000.0 м.", length.to(Length.Measure.m).toString());
length = new Length(1, Length.Measure.mi);
assertEquals("1609.34 м.", length.to(Length.Measure.m).toString());
}
ну и последний момент остается добавить преобразование из любого типа в любой другой. Для этого нам понадобится добавить всего пару строчек. То есть если мы преобразуем не из метров и не в метры. То тогда делаем так: меняем текущее значение сначала в метр, а потом уже в новый тип:
public Length to(Measure newType) {
// по умолчанию новое значение совпадает со старым
double newValue = this.value;
// если текущий тип -- метр
if (this.type == Measure.m) {
/* ... это не трогаем ... */
} else if (newType == Measure.m) {
/* ... это не трогаем ... */
} else {
// считаем новое значение, сначала перегоняем в метры
// затем перегоняем в новый тип,
// ну а затем выковыриваем значение
newValue = this.to(Measure.m).to(newType).value;
// в принципе можно было сразу написать так
// return this.to(Measure.m).to(newType)
}
// ну и результат у нас это Length
return new Length(newValue, newType);
}
Операция сложения двух разных длин
Ну и вот и добрались до самого интересного. Будем теперь прибавлять астрономические единицы к парсекам, и километры к астрономическим единицам. В общем как душе будет угодно.
В принципе, мы всю сложную работу сделали в предыдущем параграфе. Так что тут нам надо просто оговорить одно правило. Так как при сложении метров с километрами, или километров с метрами не понятно, что мы должны получить в результате, то я буду считать так
- км + м == км
- м + км = м
- и т.д.
То есть тип длины определяется первым слагаемым в сумме.
И так, добавляем:
public class Length {
// ...
/**
* операция сложения с другой длиной
*/
public Length add(Length otherLength) {
// преобразуем в тип длины с которой складываем
Length otherLengthConverted = otherLength.to(this.type);
// а потом просто воспользуемся операцией сложением со значением (по сути просто числом)
return this.add(otherLengthConverted.value);
}
/**
* операция вычитания с другой длиной
*/
public Length subtract(Length otherLength) {
// преобразуем в тип длины от которой отнимаем
Length otherLengthConverted = otherLength.to(this.type);
// а потом просто воспользуемся операцией вычитания со значением (по сути просто числом)
return this.subtract(otherLengthConverted.value);
}
// ...
}
Операция эквивалентности
Ну и прежде чем писать тесты на проверку корректности сложения двух длин, переопределим оператор равенства чтобы проверять равенство двух объектов было бы проще. Сделаем так, чтобы при сравнении двух длин, ту с которой сравнивают переводилась в тип той которую сравнивают и сравнивались уже без учета типов.
Таким образом у нас 1 км и 1000 м будут приниматься за одинаковые длины, что в принципе есть правда.
public class Length {
// ...
@Override
public boolean equals(Object obj) {
// так как equals позволяет сравниться с любым типом,
// то принято добавлять проверку, является ли obj правильного типа
// в нашем случае спрашиваем is obj instance of Length
if (!(obj instanceof Length)) {
// и если нет, возвращает false, то бишь объекты не равны
return false;
}
// этот так называемый unboxing (распаковка),
// в obj теоретически может лежать любой объект,
// но так как мы условием выше фильтруем лишние классы,
// получается что оказавшись на данной строке у нас в obj обязательно будет объект типа Length
// и чтобы помочь компилятору принимать его за правильный тип
// создаем дополнительную переменную которой присваиваем объект obj
// распакованный через конструкцию (Length) в класс Length
Length objLength = (Length) obj;
// ну а тут конвертим в тип this и сравниваем значения
return objLength.to(this.type).value == this.value;
}
}
теперь все это добро тестируем
class LengthTest {
// ...
@Test
public void testAddTwoLengths() {
Length length1 = new Length(1, Length.Measure.km);
Length length2 = new Length(500, Length.Measure.m);
assertEquals(length1.add(length2), new Length(1.5, Length.Measure.km));
assertEquals(length1.add(length2), new Length(1500, Length.Measure.m));
}
@Test
public void testSubtractTwoLengths() {
Length length1 = new Length(1, Length.Measure.km);
Length length2 = new Length(500, Length.Measure.m);
assertEquals(length1.subtract(length2), new Length(0.5, Length.Measure.km));
assertEquals(length1.subtract(length2), new Length(500, Length.Measure.m));
}
}
и запускаем:
вот и все.