Свой тип

Задача:

  1. Создать класс реализующий операции в соответствии с заданием
  2. Протестировать операции

для следующего задания:

0

Мера длины, задаваемая в виде пары (значение, тип), допустимые типы: метры, километры, мили

  • сложение
  • вычитание
  • умножение на число
  • сравнение двух объемов
  • вывод значения в любом типе

Создаем приложение

Imgur

Imgur

Imgur

Imgur

получаем

package com.company;

public class Main {

    public static void main(String[] args) {
	// write your code here
    }
}

Добавляем класс длина

Тут нам нет большой необходимости создавать класс под логику, так как мы решаем архитектурную задачу, поэтому будем создавать класс, который можно будет протестить.

И так создаем класс:

Imgur

нзываем его Length

Imgur

получим

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

получим

Imgur

вот этот 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, получим:

Imgur

уже намного лучше.

Приступаем к проверке кода

Давайте добавим парочку тестов, которые проверит что на разных типах мер, выводится ожидаемый результат.

Сначала делаем зелененькую папку под тесты:

Imgur

а дальше уже добавляем тесты

Imgur

получаем

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

запускаем и проверяем:

Imgur

красота!

Операции вычитания, умножения, деления с числом

Делаем по аналогии:

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

запускаем:

Imgur

Преобразование в другой тип

Прежде чем сразу начать писать операцию сложения длин, заданных в разных типах, лучше сначала написать функцию, которая позволит конвертировать один тип в другой.

Самый простой способ — это преобразовывать в какой-нибудь самый привычный тип (например, метры), а затем выполнять обратное преобразование, и так:

  • 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());
    }
}

запускаем, ликуем:

Imgur

и идем дальше.

Давайте подумаем, нам надо уметь преобразовывать из любого типа в любой другой, то есть с учетом 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));
    }
}

и запускаем:

Imgur

вот и все.