Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[SnowOne 2023] Валерия Юрковлянец: Как разработчику не потерять себя при работе на легаси-проекте

jugnsk
March 17, 2023

[SnowOne 2023] Валерия Юрковлянец: Как разработчику не потерять себя при работе на легаси-проекте

Что делать, если попал в легаси, но совсем не в Хогвартс?
Разберемся, как опознать легаси, откуда они берутся и самое главное — как приобрести релевантный опыт после работы на таком проекте

jugnsk

March 17, 2023
Tweet

More Decks by jugnsk

Other Decks in Programming

Transcript

  1. Как разработчику не потерять
    себя на легаси проекте
    Лера Юрковлянец, CDEK IT

    View Slide

  2. 2
    @Slf4j
    @Service
    public class PodrygkaOrderStatusSendService {
    private final UpdateDeliveryStatusConnector connector;
    private final PodrygkaOrderStatusRepository statusRepository;
    public PodrygkaOrderStatusSendService(
    UpdateDeliveryStatusConnector connector,
    PodrygkaOrderStatusRepository statusRepository) {
    this.connector = connector;
    this.statusRepository = statusRepository;
    }
    public void sendOrderStatusList(List statuses) {
    if(CollectionUtils.isEmpty(statuses)){
    return;
    }
    statuses.stream()
    .sorted(Comparator.comparing(PodrygkaOrderStatus::getCreated))
    .forEach(status -> {
    //передаем терминальные статусы
    if (ORDER_ISSUED_TO_RECIPIENT.equals(status.getPodrygkaStatus()) || NOT_DELIVERED.equals(status.getCdekStatus())) {
    if (isOlderThenDay(status)) {
    sendAndValidateResponse(status);
    }
    //передаем промежуточные статусы
    } else
    if (isOlderThenHour(status)) {
    sendAndValidateResponse(status);
    }
    });
    }
    }

    View Slide

  3. 3
    private void processColumns(
    boolean mainGraphs, GraphRequestData grd, Set graphNames, Set defaultGraphName,
    Map periodToRowForAvgCalculation, EventStatsRow row, LocalDate today,
    CompanyGraphData resultRow, StatisticsResponseDto statResult, LocalDate rowDate,
    GraphData calcData, BiFunction periodMinusFunc,
    Map graphNameToPartialPeriods, int daysInPeriod, Set otherGraphNamesToCheck,
    DateRangeOptionsDto dateRangeOptionsDto) {
    Set types = mainGraphs ? grd.getMainTypes() : grd.getOtherTypes();
    for (PackType type : types) {
    switch (type) {
    case CPI ->
    processCpiColumn(grd, graphNames, defaultGraphName, row, today, resultRow,
    rowDate, calcData, periodMinusFunc, graphNameToPartialPeriods, kpiType);
    case RR ->
    processRrColumn(grd, graphNames, defaultGraphName, periodToRowForAvgCalculation, row, today,
    resultRow, statResult, rowDate, calcData, periodMinusFunc, graphNameToPartialPeriods, daysInPeriod, kpiType,
    appEventCohortDays, dateRangeOptionsDto);
    case ROAS ->
    processRoasColumn(grd, graphNames, defaultGraphName, periodToRowForAvgCalculation, row, today, resultRow,
    statResult, rowDate, calcData, periodMinusFunc, graphNameToPartialPeriods, daysInPeriod, kpiType, appEventCohortDays,
    dateRangeOptionsDto);
    case CVR ->
    processCvrColumn(grd, graphNames, defaultGraphName, periodToRowForAvgCalculation, row, today, resultRow,
    statResult, rowDate, calcData, periodMinusFunc, graphNameToPartialPeriods, daysInPeriod, kpiType, dynamicCohortDays,
    otherGraphNamesToCheck, dateRangeOptionsDto);
    case Cost ->
    processCostColumn(grd, graphNames, defaultGraphName, periodToRowForAvgCalculation, row, today, resultRow,
    statResult, rowDate, calcData, periodMinusFunc, graphNameToPartialPeriods, daysInPeriod, kpiType, dynamicCohortDays,
    otherGraphNamesToCheck, dateRangeOptionsDto);
    case CPA ->
    processCPAColumn(grd, graphNames, defaultGraphName, periodToRowForAvgCalculation, row, today, resultRow, statResult,
    rowDate, calcData, periodMinusFunc, graphNameToPartialPeriods, daysInPeriod, kpiType, dateRangeOptionsDto);
    }
    }

    View Slide

  4. 4
    150
    строк

    View Slide

  5. 5
    700+
    строк

    View Slide

  6. Дисклеймер
    6

    View Slide

  7. Здесь и далее — реальный код, который крутится
    на продакшене и приносит деньги (большие)
    7
    Дисклеймер

    View Slide

  8. Обо мне
    Java-разработчик в CDEK IT
    Блок интеграции
    НГТУ, РЭФ
    Адепт чистого кода и чистой архитектуры
    8

    View Slide

  9. Что по Легаси?
    9

    View Slide

  10. Легаси — устаревший код, который слабо поддерживается,
    но используется.
    Что по Легаси?
    10

    View Slide

  11. Легаси — устаревший код, который слабо поддерживается,
    но используется.
    Плохой код — сложный для понимания код,
    нетолерантный к изменениям
    Что по Легаси?
    11

    View Slide

  12. Легаси — плохой код, который написали
    ваши коллеги…
    12
    Что по Легаси?

    View Slide

  13. Легаси — плохой код, который написали
    ваши коллеги…
    13
    Что по Легаси?

    View Slide

  14. Что по Легаси?
    Легаси — плохой код, который написали
    ваши коллеги…
    …и это то, как рано или поздно назовут ваш код,
    если не писать сразу хорошо
    14

    View Slide

  15. Откуда вообще берется этот легаси?
    15

    View Slide

  16. Почему хорошие разработчики пишут плохой код:
    16
    Откуда вообще берется этот легаси?

    View Slide

  17. Почему хорошие разработчики пишут плохой код:
    ● высокая срочность
    17
    Откуда вообще берется этот легаси?

    View Slide

  18. Почему хорошие разработчики пишут плохой код:
    ● высокая срочность
    ● изменчивые требования
    18
    Откуда вообще берется этот легаси?

    View Slide

  19. Почему хорошие разработчики пишут плохой код:
    ● высокая срочность
    ● изменчивые требования
    ● лень
    19
    Откуда вообще берется этот легаси?

    View Slide

  20. Почему хорошие разработчики пишут плохой код:
    ● высокая срочность
    ● изменчивые требования
    ● лень
    ● не налаженная процедура ревью
    20
    Откуда вообще берется этот легаси?

    View Slide

  21. Почему хорошие разработчики пишут плохой код:
    ● высокая срочность
    ● изменчивые требования
    ● лень
    ● не налаженная процедура ревью
    ● отсутствие опыта
    21
    Откуда вообще берется этот легаси?

    View Slide

  22. 22
    А что тогда хорошо?

    View Slide

  23. ● лёгкий для прочтения
    и понимания
    23
    А что тогда хорошо?

    View Slide

  24. ● лёгкий для прочтения
    и понимания
    24
    А что тогда хорошо?

    View Slide

  25. ● лёгкий для прочтения
    и понимания
    ● лёгкий для поддержания
    25
    А что тогда хорошо?

    View Slide

  26. ● лёгкий для прочтения
    и понимания
    ● лёгкий для поддержания
    ● толерантный к модификациям
    26
    А что тогда хорошо?

    View Slide

  27. ● лёгкий для прочтения
    и понимания
    ● лёгкий для поддержания
    ● толерантный к модификациям
    ● …и тесты
    27
    А что тогда хорошо?

    View Slide

  28. А что тогда хорошо?
    ● лёгкий для прочтения
    и понимания
    ● лёгкий для поддержания
    ● толерантный к модификациям
    ● …и тесты
    28

    View Slide

  29. 29
    Всё до нас уже придумали

    View Slide

  30. SOLID — принципы проектирования непосредственно
    программного кода
    30
    Всё до нас уже придумали

    View Slide

  31. SOLID — принципы проектирования непосредственно
    программного кода
    Цель принципов — создать программные структуры, которые:
    ● терпимы к изменениям
    ● просты и понятны
    ● образуют основу для компонентов, которые могут
    использоваться во многих программных системах
    31
    Всё до нас уже придумали

    View Slide

  32. Немного о SOLID
    S — single responsibility principle / SRP
    O — open-closed principle / OCP
    L — Liskov substitution principle / LSP
    I — interface segregation principle / ISP
    D — dependency inversion principle / DIP
    32

    View Slide

  33. 33
    protected CompanyService (
    CompanyRepository repository ,
    GDPRCountries gdprCountries , CreativeService creativeService ,
    CountryService countryService ,
    SSPService sspService ,
    IABCategoryService iabCategoryService ,
    BetAlgorithmService betAlgorithmService ,
    BetAlgorithmLinkService betAlgorithmLinkService ,
    @Lazy CompanyGlobalLimitService companyLimitService ,
    AudienceService audienceService ,
    AppEventService appEventService ,
    @Lazy PackService packService ,
    NamedParameterJdbcTemplate businessJdbcTemplate ,
    NamedParameterJdbcTemplate statsJdbcTemplate , CompanyCriteriaRepository companyCriteriaRepository ,
    SegmentService segmentService ,
    PacingService pacingService , SettingsService settingsService ,
    CreativeSelectionStrategyService creativeSelectionStrategyService ,
    AutomaticModelTaskService automaticModelTaskService ,
    CompanyBetPriceService companyBetPriceService ,
    PmmlDataLoadRepository pmmlDataLoadRepository ,
    CompanySspPackWhiteListService companySspPackWhiteListService ,
    @Lazy SSPGroupService sspGroupService , SkanMappingService skanMappingService ,
    RLAgentService rlAgentService ,
    CompanyPredefinedTimeService companyPredefinedTimeService ,
    GlobalModelInputSettingsService globalModelInputSettingsService ,
    CompanyNotificationService companyNotificationService) {
    super (repository) ;
    this .gdprCountries = gdprCountries ;
    this .creativeService = creativeService ;
    this .countryService = countryService ;
    this .sspService = sspService ;
    this .iabCategoryService = iabCategoryService ;
    this .betAlgorithmService = betAlgorithmService ;
    this .betAlgorithmLinkService = betAlgorithmLinkService ;
    this .companyLimitService = companyLimitService ;
    this .audienceService = audienceService ;
    this .appEventService = appEventService ;
    this .packService = packService ;
    this .businessJdbcTemplate = businessJdbcTemplate ;
    this .statsJdbcTemplate = statsJdbcTemplate ;
    this .companyCriteriaRepository = companyCriteriaRepository ;
    this .segmentService = segmentService ;
    this .pacingService = pacingService ;
    this .settingsService = settingsService ;
    this .creativeSelectionStrategyService = creativeSelectionStrategyService ;
    this .automaticModelTaskService = automaticModelTaskService ;
    this .companyBetPriceService = companyBetPriceService ;
    this .pmmlDataLoadRepository = pmmlDataLoadRepository ;
    this .companySspPackWhiteListService = companySspPackWhiteListService ;
    this .sspGroupService = sspGroupService ;
    this .skanMappingService = skanMappingService ;
    this .rlAgentService = rlAgentService ;
    this .companyPredefinedTimeService = companyPredefinedTimeService ;
    this .globalModelInputSettingsService = globalModelInputSettingsService ;
    this .companyNotificationService = companyNotificationService ;
    }
    25
    зависимостей

    View Slide

  34. SRP
    ● Принцип единой ответственности
    34

    View Slide

  35. SRP
    ● Принцип единой ответственности
    ● У кода должна быть единственная цель
    и единственный актор
    35

    View Slide

  36. public class PaymentService {
    private final TerminalDao terminalDao ;
    private final AgentDao agentDao ;
    private final EmployeeService employeeService ;
    private final PostamatCashRegisterDao postamatCashRegisterDao ;
    private final OperationPaymentsRefundsDao operationPaymentsRefundsDao ;
    private final OperationDao operationDao ;
    private final CashNfcPayerService cashNfcPayerService ;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    private Agent getPaymentAgentByCashboxSettings (CreationInput input) {
    if (input.getOrigin().getCashierShift() == null) {
    return getPaymentAgentByCashbox(input.getOrigin().getCash()) ;
    }
    return getPaymentAgentByCashRegisterId(input.getOrigin().getCashierShift().getCashRegister().getId()) ;
    }
    private Agent getPaymentAgentByCashboxSettings (EditionInput editInput) {
    if (editInput.getCashierShift() == null) {
    return getPaymentAgentByCashbox(editInput.getOldOperation().getCash()) ;
    }
    return getPaymentAgentByCashRegisterId(editInput.getCashierShift().getCashRegister().getId()) ;
    }
    @Nullable
    public Agent getPaymentAgent (CreationInput input) {
    if (input.getBaseOperationInfoForRefunds() != null || input.getType().isRefund()) {
    return getPaymentAgentFromRefund(input) ;
    }
    // …
    36
    Нормальный, на первый взгляд, класс

    View Slide

  37. public class PaymentService {
    private final TerminalDao terminalDao ;
    private final AgentDao agentDao ;
    private final EmployeeService employeeService ;
    private final PostamatCashRegisterDao postamatCashRegisterDao ;
    private final OperationPaymentsRefundsDao operationPaymentsRefundsDao ;
    private final OperationDao operationDao ;
    private final CashNfcPayerService cashNfcPayerService ;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    private Agent getPaymentAgentByCashboxSettings (CreationInput input) {
    if (input.getOrigin().getCashierShift() == null) {
    return getPaymentAgentByCashbox(input.getOrigin().getCash()) ;
    }
    return getPaymentAgentByCashRegisterId(input.getOrigin().getCashierShift().getCashRegister().getId()) ;
    }
    private Agent getPaymentAgentByCashboxSettings (EditionInput editInput) {
    if (editInput.getCashierShift() == null) {
    return getPaymentAgentByCashbox(editInput.getOldOperation().getCash()) ;
    }
    return getPaymentAgentByCashRegisterId(editInput.getCashierShift().getCashRegister().getId()) ;
    }
    @Nullable
    public Agent getPaymentAgent (CreationInput input) {
    if (input.getBaseOperationInfoForRefunds() != null || input.getType().isRefund()) {
    return getPaymentAgentFromRefund(input) ;
    }
    // …
    37
    Нормальный, на первый взгляд, класс

    View Slide

  38. OSP
    ● Принцип открытости-закрытости
    ● Код должен быть открыт для расширения но закрыт для
    модификаций
    38

    View Slide

  39. 39
    13
    условных
    операторов

    View Slide

  40. 40

    View Slide

  41. public class OperationEditPaymentService {
    private final TerminalDao terminalDao;
    private final AgentDao agentDao;
    private final PostamatCashRegisterDao postamatCashRegisterDao;
    private final OperationPaymentsRefundsDao operationPaymentsRefundsDao;
    private final OperationDao operationDao;
    private final CashNfcPayerService cashNfcPayerService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(EditionInput editInput) {
    if (editInput.getOldOperation().getType().isRefund()) {
    return getPaymentAgentFromRefund(editInput);
    }
    if (isQrCodePayment(editInput)) {
    return getPaymentAgentByUuid(editInput.getSbpMerchant().getAgentUuid());
    }
    if (editInput.getCardPayment() != null &&
    CardPaymentSystemType.isNfc(editInput.getCardPayment().getPaymentSystem())) {
    return getNfcPaymentAgentByCashbox(editInput.getCash());
    }
    switch (editInput.getOldOperation().getSourceType()){
    case OFFICE:
    case COURIER:
    if (editInput.getCourierCode() == null) {
    CourierRevise courierRevise = editInput.getOldOperation().getCourierRevise();
    if (courierRevise == null) {
    return getOfficePaymentAgent(editInput);
    } else {
    return getPaymentAgentByCourier(courierRevise.getCourierCode());
    ...
    41

    View Slide

  42. 42
    public class OperationCreatePaymentService {
    private final TerminalDao terminalDao;
    private final AgentDao agentDao;
    private final EmployeeService employeeService;
    private final PostamatCashRegisterDao postamatCashRegisterDao;
    private final CashNfcPayerService cashNfcPayerService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(CreationInput input) {
    if (input.getBaseOperationInfoForRefunds() != null || input.getType().isRefund()) {
    return getPaymentAgentFromRefund(input);
    }
    if (isQrCodePayment(input)) {
    return getPaymentAgentByUuid(input.getSbpMerchant().getAgentUuid());
    }
    if (input.getCardPaymentInfo() != null &&
    CardPaymentSystemType.isNfc(input.getCardPaymentInfo().getPaymentSystemType())) {
    return getNfcPaymentAgentByCashbox(input.getOrigin().getCash());
    }
    switch (input.getOperationSourceType()) {
    case OFFICE:
    return getPaymentAgentByCashboxSettings(input);
    case SITE:
    return getPaymentAgentByPaymentSystem(input.getCardPaymentInfo().getPaymentSystemType());
    case COURIER:
    return getPaymentAgentByMobileCourier((MobileCourier) input.getSourceDocument(),
    input.getOrigin().getCash());
    ...

    View Slide

  43. 43
    public class OperationCreatePaymentService {
    private final TerminalDao terminalDao;
    private final AgentDao agentDao;
    private final EmployeeService employeeService;
    private final PostamatCashRegisterDao postamatCashRegisterDao;
    private final CashNfcPayerService cashNfcPayerService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(CreationInput input) {
    if (input.getBaseOperationInfoForRefunds() != null || input.getType().isRefund()) {
    return getPaymentAgentFromRefund(input);
    }
    if (isQrCodePayment(input)) {
    return getPaymentAgentByUuid(input.getSbpMerchant().getAgentUuid());
    }
    if (input.getCardPaymentInfo() != null &&
    CardPaymentSystemType.isNfc(input.getCardPaymentInfo().getPaymentSystemType())) {
    return getNfcPaymentAgentByCashbox(input.getOrigin().getCash());
    }
    switch (input.getOperationSourceType()) {
    case OFFICE:
    return getPaymentAgentByCashboxSettings(input);
    case SITE:
    return getPaymentAgentByPaymentSystem(input.getCardPaymentInfo().getPaymentSystemType());
    case COURIER:
    return getPaymentAgentByMobileCourier((MobileCourier) input.getSourceDocument(),
    input.getOrigin().getCash());
    ...
    switch (input.getOperationSourceType()) {
    case OFFICE:
    return getPaymentAgentByCashboxSettings(input);
    case SITE:
    return getPaymentAgentByPaymentSystem(input.getCardPaymentInfo().getPaymentSystemType());
    case COURIER:
    UUID sourceUuid = input.getSourceDocument().getSourceUuid();
    String courierCode = ((CourierDelivery) input.getSourceDocument()).getCourierCode();
    return getPaymentAgentByMobileCourier(courierCode, sourceUuid,
    input.getOrigin().getCash();
    default:
    return null;
    }

    View Slide

  44. 44
    public class OperationCreatePaymentService {
    private final TerminalDao terminalDao;
    private final AgentDao agentDao;
    private final EmployeeService employeeService;
    private final PostamatCashRegisterDao postamatCashRegisterDao;
    private final CashNfcPayerService cashNfcPayerService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(CreationInput input) {
    // todo в оффлайн режиме не проставляется базовая операция, но и в isRefund нет нефискальных БО
    // будет исправлено в CASH-1512
    if (input.getBaseOperationInfoForRefunds() != null || input.getType().isRefund()) {
    return getPaymentAgentFromRefund(input);
    }
    // Оплата через СБП (с помощью QR кода) может быть как с OFFICE с МК или Сайта
    if (isQrCodePayment(input)) {
    return getPaymentAgentByUuid(input.getSbpMerchant().getAgentUuid());
    }
    // Если оплата через NFC то подтягиваем связанного с кассой NFC эквайера (может быть как с МК так и с OFFICE)
    if (input.getCardPaymentInfo() != null &&
    CardPaymentSystemType.isNfc(input.getCardPaymentInfo().getPaymentSystemType())) {
    return getNfcPaymentAgentByCashbox(input.getOrigin().getCash());
    }
    switch (input.getOperationSourceType()) {
    case OFFICE:
    return getPaymentAgentByCashboxSettings(input);
    case SITE:
    return getPaymentAgentByPaymentSystem(input.getCardPaymentInfo().getPaymentSystemType());
    case COURIER:
    return getPaymentAgentByMobileCourier((MobileCourier) input.getSourceDocument(),
    input.getOrigin().getCash());
    ...
    switch (input.getOperationSourceType()) {
    case OFFICE:
    return getPaymentAgentByCashboxSettings(input);
    case SITE:
    return getPaymentAgentByPaymentSystem(input.getCardPaymentInfo().getPaymentSystemType());
    case COURIER:
    UUID sourceUuid = input.getSourceDocument().getSourceUuid();
    String courierCode = ((CourierDelivery) input.getSourceDocument()).getCourierCode();
    return getPaymentAgentByMobileCourier(courierCode, sourceUuid,
    input.getOrigin().getCash(),
    input.getOrigin().getLang());
    case POSTAMAT:
    return getPaymentAgentByPostamatCashRegister(
    ((PostomatPaymentRequest) input.getSourceDocument()).getOfficeUuid());
    case EVENT_DELIVERY_DETAIL:
    return getPaymentAgentByCashbox(
    (input.getOrigin().getCash()));
    case PARTNER:
    return getPaymentAgent(input);
    default:
    return null;
    }

    View Slide

  45. 45
    public class CourierOperationCreatePaymentService implements OperationCreatePaymentService{
    private final TerminalDao terminalDao;
    private final AgentService agentService;
    private final EmployeeService employeeService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(CreationInput input) {
    UUID sourceUuid = input.getSourceDocument().getSourceUuid();
    String courierCode = ((CourierDelivery) input.getSourceDocument()).getCourierCode();
    return getPaymentAgentByMobileCourier(courierCode, sourceUuid,
    input.getOrigin().getCash(),
    input.getOrigin().getLang());
    }
    private Agent getPaymentAgentByMobileCourier(String courierCode, UUID sourceUuid, Cash cash, String lang) {
    Employee courier = employeeService.getEmployee(courierCode, lang)
    .orElseThrow(() -> new CdekReferenceException(ErrorType.COURIER_NOT_FOUND,
    "Courier not found for code ", courierCode));
    List payerUuids = terminalDao.getPayerUuidsByCourierCode(courier.getCode());
    if (!courier.isActive() && payerUuids.isEmpty()) {
    return getPaymentAgentByCashbox(cash);
    }
    if (payerUuids.size() > TERMINAL_ACQUIRE_CODES_COUNT) {
    throw new CdekCashboxException(
    ErrorType.MORE_THAN_ONE_TERMINAL_COURIER,
    "There are more than one relation with the terminal and courier with id = " +
    courierCode,
    sourceUuid,
    courierCode
    );
    } ...

    View Slide

  46. 46
    public class CourierOperationCreatePaymentService implements OperationCreatePaymentService{
    private final TerminalDao terminalDao;
    private final AgentService agentService;
    private final EmployeeService employeeService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(CreationInput input) {
    UUID sourceUuid = input.getSourceDocument().getSourceUuid();
    String courierCode = ((CourierDelivery) input.getSourceDocument()).getCourierCode();
    return getPaymentAgentByMobileCourier(courierCode, sourceUuid,
    input.getOrigin().getCash(),
    input.getOrigin().getLang());
    }
    private Agent getPaymentAgentByMobileCourier(String courierCode, UUID sourceUuid, Cash cash, String lang) {
    Employee courier = employeeService.getEmployee(courierCode, lang)
    .orElseThrow(() -> new CdekReferenceException(ErrorType.COURIER_NOT_FOUND,
    "Courier not found for code ", courierCode));
    List payerUuids = terminalDao.getPayerUuidsByCourierCode(courier.getCode());
    if (!courier.isActive() && payerUuids.isEmpty()) {
    return getPaymentAgentByCashbox(cash);
    }
    if (payerUuids.size() > TERMINAL_ACQUIRE_CODES_COUNT) {
    throw new CdekCashboxException(
    ErrorType.MORE_THAN_ONE_TERMINAL_COURIER,
    "There are more than one relation with the terminal and courier with id = " +
    courierCode,
    sourceUuid,
    courierCode
    );
    } ...
    public interface OperationCreatePaymentService {
    Agent getPaymentAgent(CreationInput input);
    }

    View Slide

  47. 47
    class CourierOperationCreatePaymentService
    class SiteOperationCreatePaymentService
    class PostamatOperationCreatePaymentService
    class OfficeOperationCreatePaymentService
    class PartnerOperationCreatePaymentService

    View Slide

  48. 48
    class CourierOperationCreatePaymentService
    class SiteOperationCreatePaymentService
    class PostamatOperationCreatePaymentService
    class OfficeOperationCreatePaymentService
    class PartnerOperationCreatePaymentService
    public interface PaymentServiceOperationSourceFactory {
    /**
    * Получить источник сервис Плательщика
    * по типу операции
    * @param type Тип источника операции
    * @return сервис
    */
    PaymentService get(OperationSourceType type);
    }

    View Slide

  49. 49

    View Slide

  50. ISP
    ● Принцип разделения интерфейсов
    ● Объекты не должны зависеть от интерфейсов, которые они не
    используют
    50

    View Slide

  51. public class CreationInput {
    private final Origin origin;
    private OperationType type;
    private OperationSubtype operationSubtype;
    private String cashFlowItemNumber;
    private Instant creationDate;
    private LocalDate date;
    private final UnderlyingDocument underlyingDocument;
    @Nullable
    private final Agent agent;
    @Nullable
    private Individual individual;
    private Contract contract;
    private final Person person;
    private final String description;
    private ReceiptInfo receiptInfo;
    private ReceiptData receiptData;
    private final CompositePayment paid;
    private OperationSourceType operationSourceType;
    private OperationSource sourceDocument;
    private boolean fromDraft;
    private Cash destinationCashbox;
    private CardPaymentInfo cardPaymentInfo;
    private LocalDate cashboxPeriodClosingMoment; 51

    View Slide

  52. public class CreationInput {
    private final Origin origin;
    private OperationType type;
    private OperationSubtype operationSubtype;
    private String cashFlowItemNumber;
    private Instant creationDate;
    private LocalDate date;
    private final UnderlyingDocument underlyingDocument;
    @Nullable
    private final Agent agent;
    @Nullable
    private Individual individual;
    private Contract contract;
    private final Person person;
    private final String description;
    private ReceiptInfo receiptInfo;
    private ReceiptData receiptData;
    private final CompositePayment paid;
    private OperationSourceType operationSourceType;
    private OperationSource sourceDocument;
    private boolean fromDraft;
    private Cash destinationCashbox;
    private CardPaymentInfo cardPaymentInfo;
    private LocalDate cashboxPeriodClosingMoment; 52
    30
    полей
    в сущности

    View Slide

  53. public class CreationInput {
    private final Origin origin;
    private OperationType type;
    private OperationSubtype operationSubtype;
    private String cashFlowItemNumber;
    private Instant creationDate;
    private LocalDate date;
    private final UnderlyingDocument underlyingDocument;
    @Nullable
    private final Agent agent;
    @Nullable
    private Individual individual;
    private Contract contract;
    private final Person person;
    private final String description;
    private ReceiptInfo receiptInfo;
    private ReceiptData receiptData;
    private final CompositePayment paid;
    private OperationSourceType operationSourceType;
    private OperationSource sourceDocument;
    private boolean fromDraft;
    private Cash destinationCashbox;
    private CardPaymentInfo cardPaymentInfo;
    private LocalDate cashboxPeriodClosingMoment;
    private SbpMerchant sbpMerchant;
    private String qrSourceId;
    private String courierCode;
    @Nullable
    private PointsPaymentData pointsPaymentData;
    private List previous;
    private boolean hasSiteOperation;
    private CashAvailablePeriod cashAvailablePeriod;
    @Nullable
    private LegalEntity merchantLegalEntity;
    @Nullable
    private UUID cashQrPayerUuid;
    }
    53
    30
    полей
    в сущности

    View Slide

  54. 54
    public class CourierOperationCreatePaymentService implements OperationCreatePaymentService{
    private final TerminalDao terminalDao;
    private final AgentService agentService;
    private final EmployeeService employeeService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(OperationPayerContext input) {
    UUID sourceUuid = input.getSourceDocument().getSourceUuid();
    String courierCode = ((CourierDelivery) input.getSourceDocument()).getCourierCode();
    return getPaymentAgentByMobileCourier(courierCode, sourceUuid,
    input.getOrigin().getCash(),
    input.getOrigin().getLang());
    }
    private Agent getPaymentAgentByMobileCourier(String courierCode, UUID sourceUuid, Cash cash, String lang) {
    Employee courier = employeeService.getEmployee(courierCode, lang)
    .orElseThrow(() -> new CdekReferenceException(ErrorType.COURIER_NOT_FOUND,
    "Courier not found for code ", courierCode));
    List payerUuids = terminalDao.getPayerUuidsByCourierCode(courier.getCode());
    if (!courier.isActive() && payerUuids.isEmpty()) {
    return getPaymentAgentByCashbox(cash);
    }
    if (payerUuids.size() > TERMINAL_ACQUIRE_CODES_COUNT) {
    throw new CdekCashboxException(
    ErrorType.MORE_THAN_ONE_TERMINAL_COURIER,
    "There are more than one relation with the terminal and courier with id = " +
    courierCode,
    sourceUuid,
    courierCode
    );
    } ...
    public interface OperationPayerContext {
    OperationSource getSourceDocument();
    Origin getOrigin();
    UUID getCashQrPayerUuid();
    CardPaymentInfo getCardPaymentInfo();
    }

    View Slide

  55. 55
    public class CourierOperationCreatePaymentService implements OperationCreatePaymentService{
    private final TerminalDao terminalDao;
    private final AgentService agentService;
    private final EmployeeService employeeService;
    private static final int TERMINAL_ACQUIRE_CODES_COUNT = 1;
    @Nullable
    public Agent getPaymentAgent(OperationPayerContext input) {
    UUID sourceUuid = input.getSourceDocument().getSourceUuid();
    String courierCode = ((CourierDelivery) input.getSourceDocument()).getCourierCode();
    return getPaymentAgentByMobileCourier(courierCode, sourceUuid,
    input.getOrigin().getCash(),
    input.getOrigin().getLang());
    }
    private Agent getPaymentAgentByMobileCourier(String courierCode, UUID sourceUuid, Cash cash, String lang) {
    Employee courier = employeeService.getEmployee(courierCode, lang)
    .orElseThrow(() -> new CdekReferenceException(ErrorType.COURIER_NOT_FOUND,
    "Courier not found for code ", courierCode));
    List payerUuids = terminalDao.getPayerUuidsByCourierCode(courier.getCode());
    if (!courier.isActive() && payerUuids.isEmpty()) {
    return getPaymentAgentByCashbox(cash);
    }
    if (payerUuids.size() > TERMINAL_ACQUIRE_CODES_COUNT) {
    throw new CdekCashboxException(
    ErrorType.MORE_THAN_ONE_TERMINAL_COURIER,
    "There are more than one relation with the terminal and courier with id = " +
    courierCode,
    sourceUuid,
    courierCode
    );
    } ...
    public interface OperationPayerContext {
    OperationSource getSourceDocument();
    Origin getOrigin();
    UUID getCashQrPayerUuid();
    CardPaymentInfo getCardPaymentInfo();
    }

    View Slide

  56. 56
    Распространенные ошибки

    View Slide

  57. ● Длинные и неразборчивые типы
    ● Хардкодинг и магические числа
    ● Недостаточное ревью
    57
    Распространенные ошибки

    View Slide

  58. Длинные неразборчивые типы
    public class EntityGraphCounterDTO {
    Map> dateToCounters;
    Map> changes;
    }
    58
    Map maxChangelogIdByCid = new HashMap<>();
    Map>> changelogIdToData = new HashMap<>();
    if (CollectionUtils.isNotEmpty(lastCompanyChanges)) {
    for (int i = 0; i < lastCompanyChanges.size() - 1; i++) {
    final ChangeLog currentChange = lastCompanyChanges.get(i);
    final ChangeLog prevChange = lastCompanyChanges.get(i + 1);
    CompanyDto currentCompany;
    CompanyDto prevCompany;
    try {
    currentCompany = OBJECT_MAPPER.readValue(currentChange.getContent(), CompanyDto.class);
    prevCompany = OBJECT_MAPPER.readValue(prevChange.getContent(), CompanyDto.class);
    } catch (Exception e) {
    throw new UIException("changelog.parse.error", HttpStatus.INTERNAL_SERVER_ERROR);
    }
    if (!Objects.equals(currentCompany.getId(), prevCompany.getId())) {
    // we shouldn't check changes between different companies
    continue;
    }

    View Slide

  59. Длинные неразборчивые типы
    public class EntityGraphCounterDTO {
    Map> dateToCounters;
    Map> changes;
    }
    59
    Map maxChangelogIdByCid = new HashMap<>();
    Map>> changelogIdToData = new HashMap<>();
    if (CollectionUtils.isNotEmpty(lastCompanyChanges)) {
    for (int i = 0; i < lastCompanyChanges.size() - 1; i++) {
    final ChangeLog currentChange = lastCompanyChanges.get(i);
    final ChangeLog prevChange = lastCompanyChanges.get(i + 1);
    CompanyDto currentCompany;
    CompanyDto prevCompany;
    try {
    currentCompany = OBJECT_MAPPER.readValue(currentChange.getContent(), CompanyDto.class);
    prevCompany = OBJECT_MAPPER.readValue(prevChange.getContent(), CompanyDto.class);
    } catch (Exception e) {
    throw new UIException("changelog.parse.error", HttpStatus.INTERNAL_SERVER_ERROR);
    }
    if (!Objects.equals(currentCompany.getId(), prevCompany.getId())) {
    // we shouldn't check changes between different companies
    continue;
    }

    View Slide

  60. Хардкодинг
    if (dto.isOverridePackUrls()) {
    checkTrue(StringUtils.isNotBlank(dto.getClickURL()), "company.custom.url.required", HttpStatus.BAD_REQUEST, "click");
    checkTrue(StringUtils.isNotBlank(dto.getDestinationURL()), "company.custom.url.required", HttpStatus.BAD_REQUEST, "destination");
    if (!skipAdvertiserIdUrlCheck) {
    checkTrue(UIDtosUtil.validateAdvertiserIdURL(dto.getClickURL(), isGdprCountry), isGdprCountry ? "company.url.gdpr.invalid" :
    "company.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    }
    if (StringUtils.isNotBlank(dto.getImpressionURL())) {
    if (!skipAdvertiserIdUrlCheck) {
    checkTrue(UIDtosUtil.validateAdvertiserIdURL(dto.getImpressionURL(), isGdprCountry), isGdprCountry ? "company.url.gdpr.invalid" :
    "company.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    }
    checkNotNull(dto.getImpressionVideoEventTrigger(), "impression.video-event-trigger-required", HttpStatus.BAD_REQUEST);
    }
    CreativeService.validateUrlEncodeTags(dto.getDestinationURL(), "Destination");
    CreativeService.validateUrlEncodeTags(dto.getClickURL(), "Click");
    CreativeService.validateUrlEncodeTags(dto.getImpressionURL(), "Impression");
    } else {
    if (!skipAdvertiserIdUrlCheck) {
    Pack pack = packService.findByName(dto.getPackId());
    checkTrue(UIDtosUtil.validateAdvertiserIdURL(pack.getClickURL(), isGdprCountry),
    isGdprCountry ? "pack.url.gdpr.invalid" : "pack.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    checkTrue(pack.getServerClickURL() == null || UIDtosUtil.validateAdvertiserIdURL(pack.getServerClickURL(), isGdprCountry),
    isGdprCountry ? "pack.url.gdpr.invalid" : "pack.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    checkTrue(pack.getImpressionURL() == null || UIDtosUtil.validateAdvertiserIdURL(pack.getImpressionURL(), isGdprCountry),
    isGdprCountry ? "pack.url.gdpr.invalid" : "pack.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    }
    }
    60

    View Slide

  61. Хардкодинг
    if (dto.isOverridePackUrls()) {
    checkTrue(StringUtils.isNotBlank(dto.getClickURL()), "company.custom.url.required", HttpStatus.BAD_REQUEST, "click");
    checkTrue(StringUtils.isNotBlank(dto.getDestinationURL()), "company.custom.url.required", HttpStatus.BAD_REQUEST, "destination");
    if (!skipAdvertiserIdUrlCheck) {
    checkTrue(UIDtosUtil.validateAdvertiserIdURL(dto.getClickURL(), isGdprCountry), isGdprCountry ? "company.url.gdpr.invalid" :
    "company.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    }
    if (StringUtils.isNotBlank(dto.getImpressionURL())) {
    if (!skipAdvertiserIdUrlCheck) {
    checkTrue(UIDtosUtil.validateAdvertiserIdURL(dto.getImpressionURL(), isGdprCountry), isGdprCountry ? "company.url.gdpr.invalid" :
    "company.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    }
    checkNotNull(dto.getImpressionVideoEventTrigger(), "impression.video-event-trigger-required", HttpStatus.BAD_REQUEST);
    }
    CreativeService.validateUrlEncodeTags(dto.getDestinationURL(), "Destination");
    CreativeService.validateUrlEncodeTags(dto.getClickURL(), "Click");
    CreativeService.validateUrlEncodeTags(dto.getImpressionURL(), "Impression");
    } else {
    if (!skipAdvertiserIdUrlCheck) {
    Pack pack = packService.findByName(dto.getPackId());
    checkTrue(UIDtosUtil.validateAdvertiserIdURL(pack.getClickURL(), isGdprCountry),
    isGdprCountry ? "pack.url.gdpr.invalid" : "pack.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    checkTrue(pack.getServerClickURL() == null || UIDtosUtil.validateAdvertiserIdURL(pack.getServerClickURL(), isGdprCountry),
    isGdprCountry ? "pack.url.gdpr.invalid" : "pack.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    checkTrue(pack.getImpressionURL() == null || UIDtosUtil.validateAdvertiserIdURL(pack.getImpressionURL(), isGdprCountry),
    isGdprCountry ? "pack.url.gdpr.invalid" : "pack.url.non-gdpr.invalid", HttpStatus.BAD_REQUEST);
    }
    }
    61

    View Slide

  62. Недостаточное ревью
    private List detectRequiredPaymentChanges(PaymentDTO currentPayment, PaymentDto previousPayment) {
    List result = new ArrayList<>();
    if (!Objects.equals(currentCreative.getMinCPM(), previousCreative.getMinCPM())
    || !Objects.equals(currentCreative.getMaxCPM(), currentCreative.getMaxCPM())) {
    result.add(GraphChangesType.CreativeBet);
    }
    return result;
    }
    62

    View Slide

  63. Недостаточное ревью
    private List detectRequiredPaymentChanges(PaymentDTO currentPayment, PaymentDto previousPayment) {
    List result = new ArrayList<>();
    if (!Objects.equals(currentCreative.getMinCPM(), previousCreative.getMinCPM())
    || !Objects.equals(currentCreative.getMaxCPM(), currentCreative.getMaxCPM())) {
    result.add(GraphChangesType.CreativeBet);
    }
    return result;
    }
    63

    View Slide

  64. 64
    Почему легаси — плохо

    View Slide

  65. ● Разработка дольше
    65
    Почему легаси — плохо

    View Slide

  66. ● Разработка дольше
    ● Код бажный и непредсказуемый
    66
    Почему легаси — плохо

    View Slide

  67. ● Разработка дольше
    ● Код бажный и непредсказуемый
    ● Падают скилы
    67
    Почему легаси — плохо

    View Slide

  68. Почему легаси — плохо
    ● Разработка дольше
    ● Код бажный и непредсказуемый
    ● Падают скилы
    68

    View Slide

  69. 69

    View Slide

  70. 70
    Что делать

    View Slide

  71. ● Писать тесты
    71
    Что делать

    View Slide

  72. ● Писать тесты
    ● Анализировать код
    72
    Что делать

    View Slide

  73. ● Писать тесты
    ● Анализировать код
    73
    Что делать

    View Slide

  74. ● Писать тесты
    ● Анализировать код
    ● Применять правило бой-скаутов
    74
    Что делать

    View Slide

  75. ● Писать тесты
    ● Анализировать код
    ● Применять правило бой-скаутов
    ● Держать в голове стандарты кода
    75
    Что делать

    View Slide

  76. Что делать
    ● Писать тесты
    ● Анализировать код
    ● Применять правило бой-скаутов
    ● Держать в голове стандарты кода
    ● Задавать вопросы
    76

    View Slide

  77. Что ещё почитать
    77

    View Slide

  78. Что ещё почитать
    ● принципы проектирования
    KISS —
    DRY —
    YAGNI —
    78

    View Slide

  79. ● принципы проектирования
    KISS — Keep It Simple, Stupid
    DRY —
    YAGNI —
    79
    Что ещё почитать

    View Slide

  80. ● принципы проектирования
    KISS — Keep It Simple, Stupid
    DRY — Don’t Repeat Yourself
    YAGNI —
    80
    Что ещё почитать

    View Slide

  81. ● принципы проектирования
    KISS — Keep It Simple, Stupid
    DRY — Don’t Repeat Yourself
    YAGNI — You Aren’t Gonna Need It
    81
    Что ещё почитать

    View Slide

  82. ● принципы проектирования
    KISS — Keep It Simple, Stupid
    DRY — Don’t Repeat Yourself
    YAGNI — You Aren’t Gonna Need It
    ● Чистый код и Чистая архитектура
    82
    Что ещё почитать

    View Slide

  83. Спасибо!
    Вопросы
    83
    Лера Юрковлянец
    @yura_kovlyanets

    View Slide