Как не стоит писать код: разбираем ошибки
Вторая часть цикла статей про чистый код. В ней покажем пример некачественного кода и разберём основные ошибки.
В прошлой части мы рассказали, что такое чистый код и какие принципы нужно соблюдать, чтобы его написать. В новой — поговорим про плохой код: перечислим проблемы и покажем, как их решать.
Эта статья — «прожарка». Она не отвечает на все вопросы и не содержит иллюстрации ко всей критике. В будущем мы шаг за шагом более атомарно будем всё разгребать.
Максим Морев
Автор статьи, Software Craftsmanship энтузиаст, CTO
Ваганов Вадим
Software Engineer, Head of Profession backend-разработки
Сперва «высечем» тезисы: как нельзя писать
Всё, рассказанное ниже, база, которую мы взяли из своего опыта, и теперь хотим поделиться.
-
НЕ пишите, если не нравится — круто, если код вас увлекает, и время в компании IDE летит. Но если нет — выберите что-то кроме программирования. В IT много интересной работы.
-
НЕ пишите слепо по ТЗ аналитика — лучше прописывать требования совместно (или хотя бы разобраться, что конкретно хочет аналитик). Потому что код — ответственность разработчика, и разгребать проблемы придется именно ему.
-
НЕ пишите, пока кратко и четко не описали проблему — её нужно зафиксировать в README репозитория на понятном пользователю языке. В идеале — описать поведение системы так, чтобы его можно было легко переложить в тесты. Это поможет смотреть на систему с перспективы её поведения.
-
НЕ пишите без предварительного проектирования — можно заранее визуализировать архитектуру приложения. Спросите себя, из каких компонентов будет состоять приложение, почему именно из них, какие роли будут исполнять классы на каждом уровне.
А теперь — сам «плохой код»
Представьте, вы — разработчик. Только пришли в команду, познакомились с классными коллегами. И увидели… сервис. Такой, как этот. Читать код невозможно, тестов почти нет, каждый класс «кусается» (ещё и на ChatGPT не свалишь).
Говорить про тесты здесь не будем — это большая тема, которой лучше посвятить отдельный текст.
fun emerge(
httpRequestEnvelope: Result<HttpRequestEnvelope>,
restConfiguration: RestConfiguration
): Result<RestServiceRequest> =
httpRequestEnvelope.fold(
onSuccess = { httpRequest ->
val restCfg = restConfiguration.configs[httpRequest.service]
val substitutor = StringSubstitutor(httpRequest.headers)
val validatedRequest = validateRequest(httpRequest, restConfiguration, restCfg)
validatedRequest.fold(
onSuccess = {
Result.success(
RestServiceRequest(
restCfg!!,
httpRequest,
substitutor.replace(restCfg.operations[httpRequest.operation]!!)
)
)
},
onFailure = { error -> Result.failure(error) }
)
},
onFailure = { error -> Result.failure(error) }
)
Если вы читали Роберта Мартина или, как минимум, мою предыдущую статью — появится мысль: «Нужно срочно покинуть эту команду». Но лучше выдохнуть и проанализировать код — его можно превратить в гармоничное приложение, которое легко прочитать, понять и, если нужно, доработать.
Сканируем методы, классы и структуру пакетов.
Мы постарались сделать «прожарку» максимально понятной. Но всё равно может быть тяжеловато.
Пакет client.RESTClient
Довольно простой REST-клиент на базе WebClient. По сигнатуре функция sendRequest
соответствует нашему гайду. Но 82 строки для подобного класса — это много. Так что стоит упростить: разбить код на блоки и вытащить каждый в отдельную функцию (метод класса в ООП).
Также RESTClient важно покрыть тестами.
class RESTClient(private val webClient: WebClient) {
private val SERVICE = "mq-rest-sync-adapter"
fun sendRequest(request: RestServiceRequest): Mono<Result<RestServiceResponse>> {...}
companion object {
val CONTENT_TYPE = "${MediaType.APPLICATION_JSON};charset=${Charsets.UTF_8}"
}
}
Пакет configuration
MqConfiguration
— контейнер конфигурации, который содержит класс MqServiceCfg
. Захардкоженные значения полей timeout
, messagePipeline
и type
смущают. Лучше вынести значения в конфиг-файл.
Класс WebClientConfiguration стоит проверить тестами.
data class MqServiceCfg(
var REQ: String = "",
var RES: String = "",
var RECIEPT: String ="",
var timeout: Long = 60000,
var correlationID: String = "",
var messagePipeline: PipelineType = PipelineType.StandardEnvelope,
var createDateTime: String = "",
var formatCreateDateTime: String = "",
var values: Map<String, String> = mutableMapOf(),
var type: MqServiceType = MqServiceType.ENVELOPE
) {
fun getFormattedCreateDateTime(): String {
val date = LocalDateTime.now().atZone(ZoneOffset.UTC)
val formatter = DateTimeFormatter.ofPattern(formatCreateDateTime)
return date.format(formatter)
}
}
Пакет exceptions
ProcessException
— класс, который нигде не используется. Просто удалить.
class ServiceCommunicationException (serviceName: String, msg: String):
RuntimeException("Ошибка получения данных с сервиса '%s': %s".format(serviceName, msg))
Пакет mq
Опустим классы-контейнеры без инкапсулированной логики работы с данными. И остановлюсь на проблемах в других классах.
GetWalletBalanceRequest
Смарт-конструктор emerge может выкинуть исключение на этапе десериализации из файла.
Нужно это предусмотреть и обработать. Например. обернуть Result.runCatching{ }
. Тогда emerge будет возвращать Result, то есть однозначно говорить о том, что функция (метод) может зафэйлиться при исполнении и вернуть Two Track Type — или результат выполнения в поле success
или failure
.
Если вы отдельно обрабатываете исключения, можно обернуть их в удобный тип.
data class GetWalletBalanceRequest(
val requestType: String,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
val dateFrom: OffsetDateTime,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
val dateTo: OffsetDateTime?,
val additionalParameters: AdditionalParameters
) {
companion object {
fun emerge(
data: String
): GetWalletBalanceRequest {
val request = JsonUtil.fromString(data, PsGetBalanceRequest::class.java)
return createRequest(request)
}
private fun createRequest(
request: PsGetBalanceRequest
): GetWalletBalanceRequest =
GetWalletBalanceRequest(
request.requestType,
request.dateFrom,
request.dateTo,
AdditionalParameters()
)
}
}
Валидацию формата полей класса dateFrom
, dateTo
лучше вынести в функцию emerge
и если на вход влетели битые данные — вернуть понятную ошибку.
fun emerge(
data: String
): GetWalletBalanceRequest {
val request = JsonUtil.fromString(data, PsGetBalanceRequest::class.java)
return createRequest(request)
}
HttpRequestEnvelope
Класс — DTO, принадлежит слою представления, перегружен логикой, которой в DTO вообще быть не должно по определению. Её нужно вынести в соответствующие классы уровня бизнес-логики.
Поле класса body
(31 строка) лучше сделать пустой строкой по-умолчанию — так приближаемся к Null Safety universe.
Функция emerge
Поля просто перекладываются из входных параметров функции в поля класса. Умный конструктор говорит «тут возможна ошибка создания валидного объекта, поэтому я верну Result
», который нам не нужен. Поэтому лучше заменить его на простой of метод.
fun emerge(
correlationId: String,
httpMethod: String,
service: String,
operation: String,
httpHeaders: Map<String, String>,
httpBody: String
): Result<HttpRequestEnvelope> =
Result.success(
HttpRequestEnvelope(
correlationId = correlationId,
method = httpMethod,
service = service,
operation = operation,
headers = httpHeaders,
body = httpBody
)
)
Функция putPsRequestInBody
В первую очередь — что за заклинание вместо названия? Переименовать.
Нэйминг — это важно. Но мы на данном этапе не знаем, какое имя выбрать, нужно будет погрузиться в аналитику.
Во-вторых, в 71 строке мы генерируем json-строку из класса, который собирается из тела body-запроса. И если GetWalletBalanceRequest
зафэйлится и не сможет собраться из строки запроса, которая должна содержать JSON, мы получим необработанный Exception
. Это плохо, так в коде преднамеренно заложена ошибка и не заложена обработка этой ошибки.
Лучше собрать класс в одном месте с HttpRequestEnvelope
и GetWalletBalanceRequest
.
В-третьих, в 63 строке вместо fold
стоит использовать Result.map
— он для этого и создан. Или вовсе — передавать на вход в данный метод не Result
, а HttpRequestEnvelope
.
fun putPsRequestInBody(data: Result<HttpRequestEnvelope>): Result<HttpRequestEnvelope> =
data.fold(
onSuccess = { request ->
emerge(
request.correlationId,
request.method,
request.service,
request.operation,
request.headers,
JsonUtil.toJson(GetWalletBalanceRequest.emerge(request.body!!))
)
},
onFailure = { error -> Result.failure(error) }
)
Проектируйте систему так, чтобы она работала с валидными объектами — это возможно и помогает смоделировать процесс более надёжно. Так, на слое, где можно вызвать «Пут Пс Реквест Ин Боди» — что бы это ни значило — стоит обработать возможную ошибку, а не прокидывать её в наш замечательный метод.
Например так:
Кроме того, validatedRequest
содержит в себе validatePsRequest.
fun validatedRequest(data: Result<HttpRequestEnvelope>, timeout: Long): Result<HttpRequestEnvelope> =
data.fold(
onSuccess = { success -> validatePsRequest(success, timeout) },
onFailure = { error -> Result.failure(error) }
)
Но валидации для PsGetBalanceRequest
тут не место. Она должна находиться в умном конструкторе PsGetBalanceRequest
. Да и саму функцию validatePsRequest
(в 24 строки) можно описать элегантней.
private fun validatePsRequest(data: HttpRequestEnvelope, timeout: Long): Result<HttpRequestEnvelope> =
PsGetBalanceRequest.fromJsonString(data.body!!).fold(
onSuccess = { success ->
if (OffsetDateTime.now().plus(Duration.ofMillis(timeout)).isAfter(success.operTime)) {
return Result.failure(
ServiceCommunicationException("validation", "Истек скорок действия запроса")
)
}
if (OperationType.OPERATION_HISTORY == getOperationType(success.requestType)
&& success.dateFrom.isBefore(success.dateTo ?: OffsetDateTime.now())
&& (success.dateTo == null || success.dateTo.isBefore(OffsetDateTime.now()))
) {
return Result.success(data)
} else {
return Result.failure(
ServiceCommunicationException("validation", "Ошибка при валидации входных данных")
)
}
},
onFailure = {
Result.failure(
ServiceCommunicationException("validation", "Ошибка при валидации входных данных")
)
}
)
HttpResponseEnvelope
Вопрос к nullable-полям. Почему в запросе перекладчика могут отсутствовать:
- method;
- service;
- operation;
- body;
- headers.
Нужно чётко описать контракт в README и вместе с аналитиком прояснить, в каких случаях эти параметры могут быть null
. Часто можно прийти к выводу, что поля класса сделаны nullable «на всякий случай» потому что лень писать качественно.
class HttpResponseEnvelope(
/** Из хедера CorrelationId */
val correlationId: String,
/** HttpCode, 500 если ошибка перекладчика */
val status: Int,
/** Если была ошибка */
val message: String?,
/** Из HttpRequestEnvelope, null если вызова не было*/
val method: HttpMethod?,
/** Из HttpRequestEnvelope */
val service: String?,
/** Операция из HttpRequestEnvelope*/
val operation: String?,
/** Ответ сервиса в String */
val body: String?,
/** Хедеры, которые вернул сервис */
val headers: HttpHeaders?,
)
toJsonString
Функция не используется — удалить.
47 строка, emerge
Это просто мэпер, поэтому смарт-конструктор тут не нужен. Например можно использовать of
для такой задачи.
fun emerge(response: RestServiceResponse): Result<HttpResponseEnvelope> {
return Result.success(
HttpResponseEnvelope(
correlationId = response.requestEnvelope.correlationId,
status = response.httpCode.value(),
message = response.httpCode.reasonPhrase,
method = response.requestEnvelope.method,
service = response.requestEnvelope.service,
operation = response.requestEnvelope.operation,
headers = response.headers,
body = response.body ?: ""
)
)
}
И выглядеть это будет так:
fun of(response: RestServiceResponse): HttpResponseEnvelope {
return Result.success(
HttpResponseEnvelope(
correlationId = response.requestEnvelope.correlationId,
status = response.httpCode.value(),
message = response.httpCode.reasonPhrase,
method = response.requestEnvelope.method,
service = response.requestEnvelope.service,
operation = response.requestEnvelope.operation,
headers = response.headers,
body = response.body ?: ""
)
)
}
62 строка, emerge
В этой функции:
- не нужно создавать клон функции на 47-ой строке и передавать на вход Result — стоит удалить метод.
- нужен map вместо fold
fun emerge(
data: Result<RestServiceResponse>,
): Result<HttpResponseEnvelope> =
data.fold(
onSuccess = { restResponse ->
Result.success(
HttpResponseEnvelope(
correlationId = restResponse.requestEnvelope.correlationId,
status = restResponse.httpCode.value(),
message = restResponse.httpCode.reasonPhrase,
method = restResponse.requestEnvelope.method,
service = restResponse.requestEnvelope.service,
operation = restResponse.requestEnvelope.operation,
headers = restResponse.headers,
body = restResponse.body ?: ""
)
)
},
onFailure = { error ->
Result.failure(error)
})
}
А ещё стоит читать документацию к классу Result на сайте Kotlin и использовать методы по назначению и на нужном уровне. Но с этим мы разберемся подробнее при рефакторинге.
86 строка emerge(correlationId: String, ex: Throwable):
Дружелюбный Alt + F7 показал, что HttpResponseEnvelope
, MqResponseMessage
, RestServiceRequest
и PsGetBalanceRequest
не используются — удаляем.
Мы работаем не с библиотекой, так что IDE подсказывает, что нужно убрать, через warning. Можно еще подключить плагины: SonarLint, SonarAnalyzer и так далее — они хорошо помогают.
fun emerge(correlationId: String, ex: Throwable): HttpResponseEnvelope {
return HttpResponseEnvelope(
correlationId = correlationId,
status = HttpStatus.INTERNAL_SERVER_ERROR.value(),
message = HttpStatus.INTERNAL_SERVER_ERROR.reasonPhrase,
method = null,
headers = HttpHeaders.EMPTY,
service = "",
operation = "",
body = "Ошибка при процессинге сообщения: ${ex.message}"
)
}
Кстати, есть хорошая рекомендация от Марка Симана (кинга «Код, который умещается в голове»): делайте предупреждения ошибками и не допускайте пул реквесты с warning в коде.
Пакет service
MqHttpService
Метод registerJmsListener
создает слушатель на очередь, когда мы поднимаем конфигурацию в классе MqServiceConfiguration
. Описать слушатель на конкретной задаче лучше явно. Это упростит восприятие и сократит конфигурационную обертку.
При рефакторинге мы явно создадим слушатель в так называемом Buble Context — и покажем, как просто протестировать этот код.
fun registerJmsListener() {
log.debug("Registering JMS listener for queue ${mqConfig.REQ}")
val endpoint = SimpleJmsListenerEndpoint()
endpoint.id = "myJmsEndpoint"
endpoint.destination = mqConfig.REQ
endpoint.messageListener = MessageListener { message ->
log.info("Incoming transmission: ")
log.info("Raw message: ${message}")
//log.info("payload: ${message.payload}")
log.info("Selected pipeline: ${mqConfig.messagePipeline}")
pipelineServiceSelector.choosePipelineService(mqConfig.messagePipeline).receiveMessage(
messageConverter.fromMessage(message) as Message<Any>,
mqPublisher,
mqConfig,
restConfiguration,
restClient
)
}
container = jmsFactory.createListenerContainer(endpoint)
container!!.errorHandler = ErrorHandler {
it.printStackTrace()
}
container!!.initialize()
container!!.start()
}
MqPublisher
Содержит два небезопасных метода, которые могут выкинуть исключения:
- Первый — асинхронный метод, который оборачивает отправку сообщения в очередь на строке 22 в mono. Вызов может выкинуть исключение, как минимум, в двух очевидных случаях: если очереди не существует или отвалилось подключение к MQ.
fun publishToMq(jmsRequest: BasicJmsRequest<String>, originalMsgId: String?): Mono<MqPublishedResponce<String>> =
Mono.fromCallable {
jmsTemplate.convertAndSend(
jmsRequest.queue,
jmsRequest.payload
) { message -> fillMessageWithHeaders(message, jmsRequest.headers, originalMsgId) }
}
.map {
MqPublishedResponce(
correlationId = jmsRequest.correlationId,
status = "Success",
jmsRequest = jmsRequest
)
}
Mono.fromCallable {
jmsTemplate.convertAndSend(
jmsRequest.queue,
jmsRequest.payload
) { message -> fillMessageWithHeaders(message, jmsRequest.headers, originalMsgId) }
}
- Второй — небезопасный синхронный метод отправки сообщения в очередь.
fun sendToMq(jmsRequest: BasicJmsRequest<String>, originalMsgId: String?) {
jmsTemplate.convertAndSend(
jmsRequest.queue,
jmsRequest.payload
) { message -> fillMessageWithHeaders(message, jmsRequest.headers, originalMsgId) }
}
Зачем нам вообще и асинхронный, и синхронный методы, когда можно завернуть IO-операцию в асинхронный стек реактивщины и оставить только publishToMq, переименовав метод в publish.
Функция fillMessageWithHeaders
Содержит бизнес-логику обогащения запроса заголовками. Такое сложно тестировать. Так что функцию следует вытащить в доменный объект и тестировать явно юнит-тестом.
private fun fillMessageWithHeaders(
message: Message,
headers: Map<String, String>,
originalMsgId: String?
): Message {
message.jmsCorrelationID = originalMsgId
message.setIntProperty(JmsConstants.JMS_IBM_MSGTYPE, CMQC.MQMT_DATAGRAM)
headers.forEach { (key: String, value: String) ->
if (StringUtils.hasLength(key) && StringUtils.hasLength(value)) {
message.setStringProperty(key, value)
}
}
return message;
}
Интерфейс PipelineService
Функция receiveMessage
описывает громоздкий контракт, которому должны соответствовать сервисы обработки разных бизнес-потоков для разных слушателей. Если нужно передать много параметров — лучше оберните параметры в класс, получится проще и «чище».
Кроме того, большинство параметров передать инъекцией. Попробуйте написать Rest Controller
, который описывает API URL-ресурса, и в сигнатуру метода пердеайте конфигурацию rest-сервиса, rest client
, который понадобится под капотом на уровне application service
, и mq publisher
на всякий случай.
interface PipelineService {
fun receiveMessage(
message: Message<Any>,
mqPublisher: MqPublisher,
mqConfig: MqServiceCfg,
restConfiguration: RestConfiguration,
restClient: RESTClient
)
}
PipelineServicePS-сервис приложения
Application — сервис, помеченный аннотацией фреймворка как компонент. Если имплементировать интерфейс, начинаются очень странные дела в теле функции (честно, мы ее не придумали).
Входные параметры присваиваются полям класса, потому что разработчик так передал компоненты, конфигурацию и REST-клиент. Он логирует заголовки и тело запроса и пробрасывает входной параметр message
в метод process.
override fun receiveMessage(
message: Message<Any>,
mqPublisher: MqPublisher,
mqConfig: MqServiceCfg,
restConfiguration: RestConfiguration,
restClient: RESTClient,
) {
this.receivedMessage = message
this.mqPublisher = mqPublisher
this.mqConfig = mqConfig
this.restConfiguration = restConfiguration
this.restClient = restClient
log.info("Incoming transmission")
log.info("headers: ${message.headers}")
log.info("payload: ${message.payload}")
processMessage(message)
}
Пройдёмся по ошибкам в классе:
processMessage
. Метод на 30 строк, который состоит из секции настройки переменных (больше похожей на ловушки). Разберёмся с аналитиком по контракту и упростим.
fun processMessage(message: Message<Any>) {
val correlationId = message.headers[mqConfig.correlationID] as String? ?: UUID.randomUUID().toString()
val url = message.headers["X_Method"] as String? ?: "/somemethod"
log.debug("msg class is: ${message.javaClass}")
// TODO - Нужно найти способ получать msgId через стандартные методы
val msgId = message.headers["jms_messageId"] as String?
var httpReq: String = message.payload.toString()
-
сorrelationId
. 64-ая строка УстанавливаетсяElvis operator
рандомно и при этом отсутствует в заголовке. На делеcorrelationId
— должен передаваться в заголовке всегда, иначе система выдаст ошибку и сообщит об этом. Подробнее можно почитать тут. -
url
. Глушим отсутствие в заголовке значения метода. Так делать нельзя. Нужно проверить контракт, и еслиurl
не задан — вернуть сообщение об ошибке. -
Вместо того чтобы писать комментарий ниже, лучше сразу найти способ или создать тикет и добавить его в комментарий — задача будет реализована в рамках 20-30% техдолга.
// TODO - Нужно найти способ получать msgId через стандартные методы
val msgId = message.headers["jms_messageId"] as String?
var httpReq: String = message.payload.toString()
httpReq
должна быть неизменной —val
.
Функция acceptedMqReceipt
Во-первых, нужно уменьшить количество входных параметров.
fun acceptedMqReceipt(
correlationId: String,
url: String,
data: Result<HttpRequestEnvelope>,
originalMsgId: String?,
mqPublisher: MqPublisher,
mqConfig: MqServiceCfg,
httpRequestEnvelope: String
): Result<HttpRequestEnvelope> =
Во-вторых, в строке 106 — вместо fold
можно применить map.
В третьих, прочитаем тело функции:
data.fold(
onSuccess = { success ->
createMessageForReceipt(httpRequestEnvelope, ReceiptStatus.ACCEPTED, null, null)
.map { it ->
val receiptResponse = MqResponseMessage.emerge(correlationId, url, it)
val jmsResponse = convertToReceiptJmsResponse(receiptResponse, mqConfig)
mqPublisher.sendToMq(jmsResponse, originalMsgId)
return Result.success(success)
}
},
onFailure = { error ->
Result.failure(error)
}
)
- Строка 107. Если на вход пришел успешный запрос, вызываем функцию
createMessageForReceipt
, входных параметров в которую слишком много. - Строка 109. Вызываем map в котором лежит бизнес-логика преобразования сообщения, созданного в функции
createMessageForReceipt
. - Строка 112. Отправляем ответ на что-то неизвестное, судя по имени
jmsResponse
.
- Строка 100. Если приходит успешный
HttpRequestEnvelope
, который был собран из строки 75 и провалидирован, берем некоеPs
, помещаем вHttpRequestEnvelope
и проваливаемся в странную функциюacceptedMqReceipt
, где создаем сообщение для квитанции, превращаем его вjmsResponse
и отправляем в строку 112
Mono.just(
HttpRequestEnvelope.fromJsonString(message.payload.toString())
).map {
HttpRequestEnvelope.validatedRequest(it, mqConfig.timeout)
}.map {
HttpRequestEnvelope.putPsRequestInBody(it)
}.map {
acceptedMqReceipt(correlationId, url, it, msgId, mqPublisher, mqConfig, httpReq)
То есть функция должна отправить сообщение с квитанцией в очередь mq
. Но из имени не понятно, что делается в теле. И происходящее описано очень сложно. Решение — переименовать функцию, отправить квитанцию на вход, подать один параметр, в теле отправить квитанцию в mq
.
Функция fun createMessageForReceipt
Эта функция буквально говорит то же, что и предыдущая: «Я содержу много бизнес-логики преобразований. Меня неудобно читать. Во мне много входных параметров. Во мне неверно используют Result
».
Кроме того, не стоит кидать fold
в fold
через map. А map{it}
мэпит в самого себя строка 151.
createMessageForReceipt
после упрощения будет возвращать объект, который легко протестировать.
fun createMessageForReceipt(
httpRequestEnvelope: String,
flag: ReceiptStatus,
errorCode: Int?,
description: String?
): Result<String> =
fromJsonString(httpRequestEnvelope).fold(
onSuccess = { request ->
PsGetBalanceRequest.fromJsonString(request.body!!).fold(
onSuccess = { request ->
Result.success(
JsonUtil.toJson(
ReceiptPs(
request.requestType,
request.dateFrom,
request.dateTo,
request.operTime,
flag,
errorCode,
description
)
)
)
},
onFailure = { error -> Result.failure(error) }
)
},
onFailure = { error -> Result.failure(error) }
).map { it }
Функция convertToReceiptJmsResponse
Во-первых, mqConfig
передавать не нужно, это — параметр класса, его класс получает в виде инъекции. Во-вторых, бизнес-логику конвертации стоит выносить из сервиса в класс, так как в данном приватном методе её не протестировать наглядно и просто.
private fun convertToReceiptJmsResponse(
mqRequest: MqResponseMessage,
mqConfig: MqServiceCfg
) = BasicJmsRequest(
correlationId = mqRequest.correlationId,
queue = mqConfig.RECEIPT,
payload = mqRequest.payload,
headers = prepareSipHeaders(mqRequest.correlationId, mqRequest.operation, mqConfig)
)
Функция convertToBasicJmsResponse
Те же замечания что и к предыдущей функции.
private fun convertToBasicJmsResponse(
mqRequest: MqResponseMessage,
mqConfig: MqServiceCfg
) = BasicJmsRequest(
correlationId = mqRequest.correlationId,
queue = mqConfig.RES,
payload = mqRequest.payload,
headers = prepareSipHeaders(mqRequest.correlationId, mqRequest.operation, mqConfig)
)
Функция prepareSipHeaders
Те же замечания что и к предыдущей функции.
private fun prepareSipHeaders(correlationId: String, url: String, mqConfig: MqServiceCfg): Map<String, String> {
val headers = mutableMapOf(
mqConfig.correlationID to correlationId,
"X_Method" to url,
"RequestID" to UUID.randomUUID().toString(),
mqConfig.createDateTime to mqConfig.getFormattedCreateDateTime(),
)
headers.putAll(mqConfig.values)
return headers
}
Функция processMqResponse
В HttpResponseEnvelope.emerge
нет умного конструктора, поэтому функцию назвать нужно было map
и использовать просто в of
.
fun processMqResponse(
data: Result<RestServiceResponse>,
): Result<HttpResponseEnvelope> =
data.fold(
onSuccess = { success -> HttpResponseEnvelope.emerge(success) },
onFailure = { error ->
log.debug(error.message)
Result.failure(error)
})
Кроме того, нужно правильно применить Result
в классе HttpResponseEnvelope
. Сейчас, этот метод (функция) возвращает Result
просто потому что.
fun emerge(response: RestServiceResponse): Result<HttpResponseEnvelope> {
return Result.success(
HttpResponseEnvelope(
correlationId = response.requestEnvelope.correlationId,
status = response.httpCode.value(),
message = response.httpCode.reasonPhrase,
method = response.requestEnvelope.method,
service = response.requestEnvelope.service,
operation = response.requestEnvelope.operation,
headers = response.headers,
body = response.body ?: ""
)
)
}
Функция submitMqReceipt
Судя по развилке, функция делает несколько дел сразу.
fun submitMqReceipt(
correlationId: String,
url: String,
mqResponse: Result<MqResponseMessage>,
originalMessageId: String?,
mqPublisher: MqPublisher,
mqConfig: MqServiceCfg,
httpRequestEnvelope: String
): Result<MqResponseMessage> =
mqResponse.fold(
onSuccess = { success ->
createMessageForReceipt(httpRequestEnvelope, ReceiptStatus.SUCCESS, null, null).map {
val receiptResponse = MqResponseMessage.emerge(correlationId, url, it)
val jmsResponse = convertToReceiptJmsResponse(receiptResponse, mqConfig)
mqPublisher.sendToMq(jmsResponse, originalMessageId)
return Result.success(success)
}
},
onFailure = { error ->
createMessageForReceipt(httpRequestEnvelope, ReceiptStatus.ERROR, null, error.message).map {
val receiptResponse = MqResponseMessage.emerge(correlationId, url, it)
val jmsResponse = convertToReceiptJmsResponse(receiptResponse, mqConfig)
mqPublisher.sendToMq(jmsResponse, originalMessageId)
return Result.failure(error)
}
}
)
- Если на вход залетает
mqResponse
—Result<MqResponseMessage>
в видеResult.success
— то функция некрасиво создает квитанциюcreateMessageForReceipt
. И через две операции мэпит в JMS ответ и отправляет в MQ.
- Если на вход (строка 204) залетает
mqResponse
—Result<MqResponseMessage>
в видеResult.failure
— то создаётсяcreateMessageForReceipt
квитанции об ошибке.
К тому же функция submitMqReceipt
принимает на вход слишком много лишних параметров.
Функция submitMqResponse чуть проще, но ошибки те же: выбор, конвертация, отправка.
fun submitMqResponse(
correlationId: String,
url: String,
data: Result<MqResponseMessage>,
originalMsgId: String?,
mqPublisher: MqPublisher,
mqConfig: MqServiceCfg
): Mono<MqPublishedResponce<String>> =
data.fold(
onSuccess = { success -> mqPublisher.publishToMq(convertToBasicJmsResponse(success, mqConfig), originalMsgId) },
onFailure = { error -> mqPublisher.publishToMq(convertToBasicJmsResponse(MqResponseMessage.emerge(correlationId,url,error), mqConfig), originalMsgId)}
)
После форматирования чуть проще воспринять.
fun submitMqResponse(
correlationId: String,
url: String,
data: Result<MqResponseMessage>,
originalMsgId: String?,
mqPublisher: MqPublisher,
mqConfig: MqServiceCfg
): Mono<MqPublishedResponce<String>> =
data.fold(
onSuccess = { success ->
mqPublisher.publishToMq(
convertToBasicJmsResponse(success, mqConfig),
originalMsgId
)
},
onFailure = { error ->
mqPublisher.publishToMq(
convertToBasicJmsResponse(
MqResponseMessage.emerge(
correlationId,
url,
error
), mqConfig
), originalMsgId
)
}
)
В следующей части начнём рефакторить код и разбирать всё подробнее.