Cargando Reglas desde CSV y una Tabla de Base de Datos
Las mismas columnas alimentan una regla ya sea que se analicen desde un archivo incluido al iniciar o se lean en vivo desde Postgres — el truco es una única función factory que ninguna fuente puede saltarse.
Las clases `MaxAgeGuardrail` y `LowMileageBoost` de la lección anterior demuestran que la interfaz `Rule` funciona, pero son inútiles como solución real: sus umbrales están compilados dentro de la propia clase, así que cambiar 2012 por 2013 sigue significando un cambio de código y un deploy — exactamente el problema que este módulo existe para resolver. Lo que en realidad necesita cambiar es la implementación de la regla: en vez de una clase a medida por cada guardrail, escribes un número pequeño de clases de regla genéricas y parametrizadas cuyos umbrales, operadores y campos objetivo llegan desde afuera — un archivo CSV, o una fila de base de datos — al momento de construirlas.
Un archivo CSV de reglas le da a cada fila seis columnas: `rule_type`, `field`, `operator`, `threshold`, `amount` y `priority`. `rule_type` selecciona qué `RuleOutcome` produce la fila (`GUARDRAIL`, `BOOST`, etc.); `field` nombra una propiedad de `ValuationContext` como `mileage` o `modelYear`; `operator` y `threshold` describen la comparación; `amount` solo importa para las filas `BOOST`, donde lleva el ajuste en dólares; y `priority` controla el orden de evaluación exactamente como lo hacía en la lección anterior. Analizar esta forma no necesita una librería de CSV completa — un `split(",")` ingenuo está bien mientras controles el archivo y ninguno de tus valores contenga legítimamente una coma, lo cual es una suposición razonable para umbrales numéricos y nombres de campo cortos.
La decisión de diseño interesante es la función factory que convierte una fila analizada en una instancia viva de `Rule`. `ruleFromRow()` busca el campo nombrado en un pequeño mapa de selectores (`"mileage" -> { ctx -> ctx.mileage.toDouble() }`), y luego despacha según `rule_type` para decidir si construir un `ComparisonGuardrail` o un `ComparisonBoost`. Este es el único lugar de todo el sistema que sabe que la cadena `"GUARDRAIL"` significa "construye esta clase en particular" — todo el resto del código simplemente trabaja con la interfaz `Rule`. Si más adelante agregas un quinto tipo de resultado, este `when` es el único lugar que tocas.
Empaquetar las reglas en un archivo CSV versionado en el control de código te da algo valioso: cada cambio de regla pasa por la misma revisión de PR, el mismo CI y el mismo historial de git que cualquier otro cambio de código, lo cual importa para un conjunto de reglas que el equipo de cumplimiento eventualmente va a querer auditar. El costo es exactamente el costo que este módulo se propuso eliminar — cambiar un umbral sigue requiriendo un deploy, solo que uno mucho más pequeño y seguro de lo que antes era editar una función de 200 líneas. Las reglas en CSV tienen sentido para políticas que cambian rara vez pero que deberían revisarse con cuidado cada vez que lo hacen.
Una tabla `pricing_rule` en la misma base de datos Postgres del Módulo 3 elimina por completo ese requisito de deploy que quedaba. La forma de la fila es idéntica — la migración que crea `pricing_rule` refleja columna por columna las seis del CSV — y el cargador es un pequeño repositorio de Spring que ejecuta un `SELECT` y mapea cada fila del `ResultSet` a través de exactamente la misma función `ruleFromRow()` que llama el cargador de CSV. Esa reutilización es todo el punto: analizar "de dónde vino esta fila" y "qué significa esta fila" son dos preocupaciones separadas, y solo la primera difiere entre los dos cargadores.
Leer `pricing_rule` en cada solicitud de valuación sería un viaje innecesario a Postgres por datos que cambian quizás unas pocas veces al día, así que en la práctica cargas el conjunto de reglas activas una vez — al iniciar, y de nuevo en una actualización programada — y mantienes la lista ordenada en memoria para que el motor la use, la misma lista en memoria que `RulesEngine` ya espera del cargador de CSV. Si esa actualización es un simple sondeo con `@Scheduled` o algo que invalida una caché compartida en Redis es una decisión de infraestructura que las reglas mismas no necesitan conocer; de cualquier forma, el motor que construyes en la siguiente lección nunca habla directamente con la base de datos.
// Two generic, data-driven Rule implementations, plus the comparison// helper they share -- the parameterized cousins of the hardcoded// MaxAgeGuardrail and LowMileageBoost from the last lesson.private fun compare(actual: Double, operator: String, threshold: Double): Boolean = when (operator) {">" -> actual > threshold"<" -> actual < threshold">=" -> actual >= threshold"<=" -> actual <= threshold"==" -> actual == thresholdelse -> error("Unsupported operator: $operator")}class ComparisonGuardrail(override val name: String,override val priority: Int,private val field: (ValuationContext) -> Double,private val operator: String,private val threshold: Double,private val reason: String,) : Rule {override fun evaluate(context: ValuationContext): RuleOutcome? =if (compare(field(context), operator, threshold)) RuleOutcome.Guardrail(reason) else null}class ComparisonBoost(override val name: String,override val priority: Int,private val field: (ValuationContext) -> Double,private val operator: String,private val threshold: Double,private val amount: Double,private val reason: String,) : Rule {override fun evaluate(context: ValuationContext): RuleOutcome? =if (compare(field(context), operator, threshold)) RuleOutcome.Boost(amount, reason) else null}
Two generic, data-driven Rule implementations plus the comparison helper they share — the parameterized cousins of the hardcoded MaxAgeGuardrail and LowMileageBoost from the last lesson.
import java.io.Fileprivate val fieldSelectors: Map<String, (ValuationContext) -> Double> = mapOf("mileage" to { ctx: ValuationContext -> ctx.mileage.toDouble() },"modelYear" to { ctx: ValuationContext -> ctx.modelYear.toDouble() },"accidentCount" to { ctx: ValuationContext -> ctx.accidentCount.toDouble() },)// The one place that knows how a rule_type string turns into a live Rule instance.fun ruleFromRow(ruleType: String,field: String,operator: String,threshold: Double,amount: Double,priority: Int,): Rule {val selector = fieldSelectors[field] ?: error("Unknown field: $field")val name = "$ruleType-$field-$operator-$threshold"val reason = "$field $operator $threshold"return when (ruleType) {"GUARDRAIL" -> ComparisonGuardrail(name, priority, selector, operator, threshold, reason)"BOOST" -> ComparisonBoost(name, priority, selector, operator, threshold, amount, reason)else -> error("Unknown rule_type: $ruleType")}}// rule_type,field,operator,threshold,amount,priority -- amount is blank/ignored for non-BOOST rows.// Real CSV needs a proper parser for quoting/escaping; this naive split is fine// for our controlled, comma-free values.fun loadRulesFromCsv(path: String): List<Rule> =File(path).readLines().drop(1) // header.filter { it.isNotBlank() }.map { line -> line.split(",").map { it.trim() } }.map { cols ->ruleFromRow(ruleType = cols[0],field = cols[1],operator = cols[2],threshold = cols[3].toDouble(),amount = cols[4].toDoubleOrNull() ?: 0.0,priority = cols[5].toInt(),)}
The factory that turns one parsed row into a live Rule, and the naive CSV loader that calls it. Requires java.io.File, so it does not run in-browser.
import org.springframework.jdbc.core.JdbcTemplateimport org.springframework.stereotype.Repository// Same rows, this time from a `pricing_rule` Postgres table instead of a// bundled file -- see Module 3 for the DataSource/JdbcTemplate setup.@Repositoryclass PricingRuleRepository(private val jdbcTemplate: JdbcTemplate) {fun loadActiveRules(): List<Rule> =jdbcTemplate.query("SELECT rule_type, field, operator, threshold, amount, priority FROM pricing_rule WHERE active = true") { rs, _ ->// The exact same factory the CSV loader calls -- only the row source differs.ruleFromRow(ruleType = rs.getString("rule_type"),field = rs.getString("field"),operator = rs.getString("operator"),threshold = rs.getDouble("threshold"),amount = rs.getDouble("amount"),priority = rs.getInt("priority"),)}}
The same factory reused from a Postgres-backed repository via Spring's JdbcTemplate — only the row source changes, never the parsing logic. Requires a real DataSource/JdbcTemplate bean, so it does not run in-browser.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. Both the CSV loader and the Postgres-backed `PricingRuleRepository` call the same `ruleFromRow(...)` factory to turn a row into a `Rule`. What is the main benefit of sharing that one function between both sources, instead of writing separate row-to-Rule logic for CSV and for the database?