Система расчета ставки на вызов код


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

Требование было построить командной строки расчета приложение, позволяющее заемщикам получить цитату из пула кредиторов по 36 месяц кредитования.

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

Он также должен предоставить заемщик с деталями ежемесячная сумма погашения и сумма выплаты. Суммы погашения должны отображаться до 2 десятичных знаков, ставка кредита должна быть показана до одного десятичного знака. Заемщик должен иметь возможность запросить кредит любого £100 шаг между £1000 и £15000 включительно. Если на рынке нет достаточного предложения от кредиторов на погашение кредита, то система должна сообщить заемщику, что невозможно предоставить цитату на тот момент.

Мой код:

RateCalculatorApp.java

import domain.LenderOffer;
import interest.CompoundInterestCalculator;
import ratefinder.MultipleLendersRateFinder;
import ratefinder.RateFinder;
import result.ResultDisplay;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.Optional;

import static csv.CsvReader.csvToLenderInfoList;
import static validation.InputValidator.validateArguments;

public class RateCalculatorApp {

    private static RateFinder rateFinder = new MultipleLendersRateFinder();

    public static void main(String[] args) {
        validateArguments(args);
        Collection<LenderOffer> lenderOffers = csvToLenderInfoList(args[0]);
        BigDecimal amount = new BigDecimal(args[1]);
        Optional<BigDecimal> rateOptional = rateFinder.findLowestRate(lenderOffers, amount);
        if (rateOptional.isPresent()) {
            CompoundInterestCalculator compoundInterestCalculator = new CompoundInterestCalculator(amount, rateOptional.get());
            ResultDisplay resultDisplay = new ResultDisplay(amount, rateOptional.get(),
                    compoundInterestCalculator.getMonthlyRepayment(), compoundInterestCalculator.getTotalRepayment());
            resultDisplay.display();
        } else {
            System.out.println("Is not possible to provide a quote at this time.");
        }
    }

}

MathConstants.java

package constants;
import java.math.BigDecimal;

public final class MathConstants {

    /**
     * Scale to be used on dividing BigDecimals when precision matters
     */
    public static final int DIVISION_SCALE = 10;

    /**
     * Rounding mode to be used on dividing BigDecimals when precision matters
     */
    public final static int ROUND_MODE = BigDecimal.ROUND_UP;

    private MathConstants() {
    }

}

CsvReader.java

package csv;
import domain.LenderOffer;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;

import static java.nio.file.Files.readAllLines;
import static java.nio.file.Paths.get;
import static java.util.stream.Collectors.toList;

public class CsvReader {

    public static Collection<LenderOffer> csvToLenderInfoList(String marketFile) {
        try {
            List<String> marketDataEntriesAsStrings = readAllLines(get(marketFile));
            return parseStringsToLenderInfo(marketDataEntriesAsStrings);
        } catch (IOException e) {
            // Needs proper exception handling/logging, depending on how we want to deal with such cases
            throw new RuntimeException("Couldn't read market data file", e);
        }
    }

    private static Collection<LenderOffer> parseStringsToLenderInfo(List<String> marketDataEntriesAsStrings) {
        return marketDataEntriesAsStrings.stream().skip(1).map(s -> {
            String[] lenderInfoAsArray = s.split(",");
            BigDecimal rate = new BigDecimal(lenderInfoAsArray[1]);
            BigDecimal available = new BigDecimal(lenderInfoAsArray[2]);
            return new LenderOffer(rate, available);
        }).collect(toList());
    }
}

LenderOffer.java

package domain;
import java.math.BigDecimal;

public class LenderOffer {

    private final BigDecimal rate;
    private final BigDecimal available;

    public LenderOffer(BigDecimal rate, BigDecimal available) {
        this.rate = rate;
        this.available = available;
    }

    public BigDecimal getRate() {
        return rate;
    }

    public BigDecimal getAvailable() {
        return available;
    }

}

CompoundInterestCalculator.java

package interest;
import java.math.BigDecimal;

import static constants.MathConstants.DIVISION_SCALE;
import static constants.MathConstants.ROUND_MODE;
import static java.math.BigDecimal.ONE;
import static java.math.BigDecimal.valueOf;

public class CompoundInterestCalculator {

    private static final Integer DEFAULT_NUMBER_MONTHS = 36;

    private final BigDecimal baseAmount;
    private final BigDecimal annualInterestRatePerOne;
    private final Integer months;

    public CompoundInterestCalculator(BigDecimal baseAmount, BigDecimal annualInterestRatePerOne) {
        this(baseAmount, annualInterestRatePerOne, DEFAULT_NUMBER_MONTHS);
    }

    public CompoundInterestCalculator(BigDecimal baseAmount, BigDecimal annualInterestRatePerOne, Integer months) {
        this.baseAmount = baseAmount;
        this.annualInterestRatePerOne = annualInterestRatePerOne;
        this.months = months;
    }

    private BigDecimal calculateTotalRepayment() {
        return ONE.add(annualInterestRatePerOne.divide(valueOf(12), DIVISION_SCALE, ROUND_MODE)).pow(months).multiply(baseAmount);
    }

    public BigDecimal getMonthlyRepayment() {
        return calculateTotalRepayment().divide(valueOf(months), DIVISION_SCALE, ROUND_MODE);
    }

    public BigDecimal getTotalRepayment() {
        return calculateTotalRepayment();
    }
}

RateFinder.java

package ratefinder;
import domain.LenderOffer;    
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Optional;

public interface RateFinder {

    /**
     * Find the lowest rate within the given offers that satisfies the specified amount
     *
     * @param lenderOffers the offer list containing rates and available amount
     * @param amount       the amount to be lent
     * @return an optional with the found rate, empty if no matching quote can be provided
     */
    Optional<BigDecimal> findLowestRate(Collection<LenderOffer> lenderOffers, BigDecimal amount);
}

MultipleLendersRateFinder.java

package ratefinder;
import domain.LenderOffer;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

import static constants.MathConstants.DIVISION_SCALE;
import static constants.MathConstants.ROUND_MODE;
import static java.math.BigDecimal.ZERO;

public class MultipleLendersRateFinder implements RateFinder {

    @Override
    public Optional<BigDecimal> findLowestRate(Collection<LenderOffer> lenderOffers, BigDecimal amount) {
        List<LenderOffer> lenderOfferList = getOffersAsSortedList(lenderOffers);
        BigDecimal resultRate = ZERO;
        BigDecimal cumulativeAmount = ZERO;
        for (int i = 0; i < lenderOfferList.size() && cumulativeAmount.compareTo(amount) < 0; i++) {
            LenderOffer lenderOffer = lenderOfferList.get(i);
            BigDecimal amountToBeLentThisOffer = lenderOffer.getAvailable().min(amount.subtract(cumulativeAmount));
            BigDecimal factorThisOffer = amountToBeLentThisOffer.divide(amount, DIVISION_SCALE, ROUND_MODE);
            resultRate = resultRate.add(lenderOffer.getRate().multiply(factorThisOffer));
            cumulativeAmount = cumulativeAmount.add(amountToBeLentThisOffer);
        }
        if (cumulativeAmount.compareTo(amount) < 0) {
            return Optional.empty();
        } else {
            return Optional.of(resultRate);
        }
    }

    private List<LenderOffer> getOffersAsSortedList(Collection<LenderOffer> lenderOffers) {
        List<LenderOffer> lenderOfferList = new ArrayList<>(lenderOffers);
        lenderOfferList.sort((o1, o2) -> o1.getRate().compareTo(o2.getRate()));
        return lenderOfferList;
    }
}

SingleLenderRateFinder.java

package ratefinder;
import domain.LenderOffer;    
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Optional;

public class SingleLenderRateFinder implements RateFinder {

    @Override
    public Optional<BigDecimal> findLowestRate(Collection<LenderOffer> lenderOffers, BigDecimal amount) {
        return lenderOffers.stream()
                .filter(lenderOffer -> lenderOffer.getAvailable().compareTo(amount) >= 0)
                .sorted((o1, o2) -> o1.getRate().compareTo(o2.getRate()))
                .map(LenderOffer::getRate)
                .findFirst();
    }
}

ResultDisplay.java

package result;
import java.math.BigDecimal;
import static constants.MathConstants.ROUND_MODE;

public class ResultDisplay {

    private final BigDecimal amount;
    private final BigDecimal rate;
    private final BigDecimal monthlyRepayment;
    private final BigDecimal totalRepayment;

    public ResultDisplay(BigDecimal amount, BigDecimal rate, BigDecimal monthlyRepayment, BigDecimal totalRepayment) {
        this.amount = amount;
        this.rate = rate;
        this.monthlyRepayment = monthlyRepayment;
        this.totalRepayment = totalRepayment;
    }

    public void display() {
        System.out.println("Requested amount: £" + amount.setScale(0, ROUND_MODE));
        System.out.println("Rate: " + rate.multiply(BigDecimal.valueOf(100)).setScale(1, ROUND_MODE) + "%");
        System.out.println("Monthly repayment: £" + monthlyRepayment.setScale(2, ROUND_MODE));
        System.out.println("Total repayment: £" + totalRepayment.setScale(2, ROUND_MODE));
    }
}

InputValidator.java

package validation;    
import java.util.Arrays;    
import static java.nio.file.Files.exists;
import static java.nio.file.Paths.get;

public class InputValidator {

    private static final int MINIMUM_AMOUNT = 1000;
    private static final int MAXIMUM_AMOUNT = 15000;
    private static final int FACTOR = 100;

    public static void validateArguments(String[] args) {
        validateGotTwoArgs(args);
        validateFileExists(args[0]);
        validateAmount(args[1]);
    }

    private static void validateGotTwoArgs(String[] args) {
        if (args.length != 2) {
            throw new IllegalArgumentException("App needs two arguments. Given was: " + Arrays.toString(args));
        }
    }

    private static void validateFileExists(String marketFileName) {
        if (!exists(get(marketFileName))) {
            throw new IllegalArgumentException("Couldn't find market data file in given path: " + marketFileName);
        }
    }

    private static void validateAmount(String amountAsString) {
        validateAmountIsANumber(amountAsString);
        Integer amount = Integer.valueOf(amountAsString);
        validateAmountIsInRange(amount);
        validateAmountIsMultiple(amount);
    }

    private static void validateAmountIsANumber(String amountAsString) {
        try {
            Integer.valueOf(amountAsString);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Given amount is not a number: " + amountAsString);
        }
    }

    private static void validateAmountIsInRange(Integer amount) {
        if (amount < MINIMUM_AMOUNT || amount > MAXIMUM_AMOUNT) {
            throw new IllegalArgumentException("Amount must be within [" + MINIMUM_AMOUNT + ", " + MAXIMUM_AMOUNT + "], was: " + amount);
        }
    }

    private static void validateAmountIsMultiple(Integer amount) {
        if (amount % FACTOR != 0) {
            throw new IllegalArgumentException("Amount must be a multiple of " + FACTOR + ", was: " + amount);
        }
    }
}

Я опустил тестовые классы, потому что это уже достаточно давно, но у меня 21 тестов, охватывающих 70% кода. У меня есть две реализации RateFinder потому что я не уверен, будут ли предложений может нужно прийти с одного кредитора или нескольких, и не получил объяснений до истечения срока. Обратная связь я получил от компании:

  • SoC-это не хорошо. RateFinder возвращается ставка, калькулятор делает расчеты, но они должны быть вместе. RateFinder должна возвращать набор кредиторами или что-то подобное

  • чрезмерное использование статического импорта, так сильно, что она становится запах код

  • Результаты далеки от ожидаемых

Я не знаю, что они имеют в виду "результаты далеки от ожидаемых", и не узнать, что ответили. Я уверен, что результаты функционально правильно, поэтому я предполагаю, что речь идет о качестве кода. Эти два онлайн калькуляторы дают тот же результат, что моя программа: https://www.thecalculatorsite.com/finance/calculators/compoundinterestcalculator.php и https://www.paisabazaar.com/compound-interest-calculator/



Комментарии
1 ответ

комментируя комментарий


RateFinder возвращается ставка, калькулятор делает расчеты, но они должны быть вместе.

Если вы посмотрите на петли в MultipleLendersRateFinder.java 3 из 5 линий доступа к свойствам LenderOffer. Это явный признак того, что эти 3 строки должны быть в LenderOffer.

Это "распределение ответственности" также происходит с InputValidator и CsvReader. ИМХО проверка на существование файла принадлежит CsvReader а потом InputValidator. После всех javas собственного FileReader thows в FileNotFoundException слишком...

Другое место main метод, когда вы делаете один System.out.println() хотя вы (могли) вы ResultDisplay рассматривавший это дело тоже.


RateFinder должна возвращать набор кредиторами или что-то подобное

Только гадать, но я думаю, что они хотели список кредиторов alomg с суммой кредитования и летней соответствии с общим результатом.


чрезмерное использование статического импорта, так сильно, что она становится запах код

Я решительно поддерживаю это.

Я использую static ключевое слово, только если у меня есть хорошая причина и возможность вызвать что-то из main не в счет здесь.

В код статический импорт наркомании обманул меня думать validateArguments(args); был способ в RateCalculatorApp.

другие выводы

странное решение мяч

На всем протяжении вашего кода, который вы используете Java8 потоки, но в MultipleLendersRateFinder.java вы падаете обратно в петли с номером (хотя по каждому элементу цикла может быть меньше "старомодный"...).

государство unnessessary

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


С другой стороны я могу себе представить подход, где это может быть полезно:
ResultDisplay могло быть интерфейс с одной реализации, как вы на самом деле сделал и другой NoResultDisplay что только выдает просила празем как анонимного экземпляра класса или даже лямда назначен постоянным в этом интерфейсе. Тогда ваш main может выглядеть так:

    ResulpDisplay resultDisplay = ResulpDisplay.NO_RESULT;
if (rateOptional.isPresent()) {
CompoundInterestCalculator compoundInterestCalculator =
new CompoundInterestCalculator(
amount,
rateOptional.get());
resultDisplay =
new LowestRateDisplay(
amount,
rateOptional.get(),
compoundInterestCalculator.getMonthlyRepayment(),
compoundInterestCalculator.getTotalRepayment());
}
resultDisplay.display();

и по одном уровне абстракции картины я бы даже пошевелиться, что в отдельный метод:

  private /*static*/ ResultDisplay createDisplay(
Optional<BigDecimal> rateOptional,
BigDecimal amount
) {
if (rateOptional.isPresent()) {
CompoundInterestCalculator compoundInterestCalculator =
new CompoundInterestCalculator(
amount,
rateOptional.get());
return new LowestRateDisplay(
amount,
rateOptional.get(),
compoundInterestCalculator.getMonthlyRepayment(),
compoundInterestCalculator.getTotalRepayment());
} else {
return ResultDisplay.NO_RESULT;
}
}

// main
ResultDisplay resultDisplay = createDisplay(
rateOptional,
amount);
resultDisplay.display();

именования

Выбрать имена из домена проблема (только). Е. Г. вы назвали переменную rateOptional но какое значение имеет технический аспект этой переменной имеет Optional объявление для кого-то hwho хочет ти понял ваш алгоритм?
Нет...

Чтобы избавиться от этой реализации, связанные с суффиксами, как *App, *Finder или *Calculator.

Постоянный контейнер класса

Вы создали постоянный контейнер класса для обеспечения системы с константами. Но есть только одна константа используется в более чем один класс. И даже это quiestionable если это должно быть время компиляции константы , что бы то, что мы хотим настроить на запуск программы.

Также вы создали еще один странный шар решение , имея статический импорт на постоянной контейнер класса и класса постоянно в один и тот же файл.

Так что не делайте этого.

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



Что касается статического импорта, что бы вы использовать вместо, скажем, импорт InputValidator.validateArguments()? Вы могли бы сделать методы не статические, а затем просто использовать ее в качестве new InputValidator().validateArguments(args)? – antonro

Да.

Я бы даже пойти на один шаг дальше и передать эти случаи, как конструктор параметров для RateCalculatorApp instancs:

public static void main(
String[] args
) {
RateCalculatorApp rateCalculator =
new RateCalculatorApp(
new InputValidator(),
new CsvReader());
rateCalculator.process(
args);
}
private void process(
String[] args
) {
inputValidator.validateArguments(
args);
Collection<LenderOffer> lenderOffers =
csvReader.csvToLenderInfoList(
args[0]);
//...
}

Это открывает простой способ, чтобы заменить этот дорогостоящий зависимостей с тестовых дублей (например, издевается , созданные с издевательской рамки) для модульного тестирования.

3
ответ дан 9 апреля 2018 в 09:04 Источник Поделиться