Introducción al Diseño de Sistemas de Facturación
Este artículo explora el proceso de diseño e implementación de un sistema de facturación de telecomunicaciones utilizando principios de programación orientada a objetos (POO) en Java. A través de la iteración de requisitos, desde la facturación de líneas fijas hasta la integración de servicios móviles y SMS, se destacará la importancia de un diseño de clases robusto y extensible. El objetivo es construir un sistema que no solo cumpla con las especificaciones actuales, sino que también facilite futuras expansiones sin grandes reestructuraciones.
Análisis y Diseño del Sistema
Consideraremos un escenario simplificado para una empresa de telecomunicaciones que opera en una ciudad (por ejemplo, Nanchang, con código de área 0791) y su provincia (Jiangxi, con códigos de área 0790-0799, 0701). El sistema debe gestionar diferentes planes de tarifas para usuarios de líneas fijas y móviles, incluyendo llamadas locales, provinciales, nacionales y roaming, así como el envío de mensajes SMS.
Requisitos de Facturación
Líneas Fijas (Plan Básico)
- Cuota mensual: 20 CNY.
- Llamadas entrantes: Gratuitas.
- Llamadas salientes:
- Locales (dentro de 0791): 0.1 CNY/minuto.
- Provinciales (dentro de Jiangxi, excluyendo local): 0.3 CNY/minuto.
- Nacionales (fuera de Jiangxi): 0.6 CNY/minuto.
- Duración de llamadas: Se redondea al minuto superior.
Teléfonos Móviles (Plan en Tiempo Real)
- Cuota mensual: 15 CNY.
- Llamadas entrantes: Gratuitas (local y provincial).
- Llamadas salientes (desde la ciudad de origen):
- Locales: 0.1 CNY/minuto.
- Provinciales: 0.2 CNY/minuto.
- Nacionales: 0.3 CNY/minuto.
- Roaming Provincial (llamadas realizadas): 0.3 CNY/minuto.
- Roaming Nacional (llamadas entrantes): 0.3 CNY/minuto.
- Roaming Nacional (llamadas realizadas): 0.6 CNY/minuto.
Estructura de Datos y Clases
Para abordar la complejidad de los planes de facturación y los tipos de comunicación, se propone una estructura de clases modular y jerárquica:
Usuario: Representa al cliente, con atributos como número de teléfono, saldo y un plan de facturación asociado. Contiene una colección deRegistrosUsuario.RegistrosUsuario: Almacena todas las interacciones de un usuario, como listas de llamadas salientse (locales, provinciales, nacionales), llamadas entrantes y registros de mensajes.RegistroComunicacion(clase abstracta): Clase base para todos los tipos de comunicación, con atributos como número de origen y número de destino.RegistroLlamada(extiendeRegistroComunicacion): Registra detalles de una llamada, incluyendo hora de inicio, hora de fin, y códigos de área de origen y destino.RegistroMensaje(extiendeRegistroComunicacion): Registra detalles de un mensaje SMS, inclyuendo el contenido.PlanFacturacion(interfaz/clase abstracta): Define la interfaz para calcular el costo total y obtener la cuota mensual. Contiene una colección deReglaFacturacion.PlanFijo(implementaPlanFacturacion): Implementación específica para líneas fijas.PlanMovil(implementaPlanFacturacion): Implementación específica para teléfonos móviles.ReglaFacturacion(interfaz/clase abstracta): Define el método para calcular el costo de un tipo específico de interacción.- Clases de reglas concretas (extienden
ReglaFacturacion): Por ejemplo,ReglaLlamadaLocal,ReglaLlamadaProvincial,ReglaRoamingEntrante, etc., cada una encapsulando la lógica de cálculo para su respectivo tipo de cargo.
Esta estructura permite agregar nuevos planes o reglas de facturación con un mínimo impacto en el código existente, adhiriéndose al principio de abierto/cerrado de SOLID.
Ejemplo de Implementación en Java
A continuación, se presenta un esqueleto de la implementación en Java, mostrando cómo las clases interactúan para procesar los datos de facturación.
Clase principal para la ejecución:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
public class SistemaFacturacion {
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss");
private static final double SALDO_INICIAL = 100.0;
private static final String AREA_CODE_NANCHANG = "0791";
private static final Set<String> AREA_CODES_JIANGXI = new HashSet<>(Arrays.asList(
"0790", "0791", "0792", "0793", "0794", "0795", "0796", "0797", "0798", "0799", "0701"
));
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
List<Usuario> usuarios = new ArrayList<>();
// 1. Procesar registros de alta de usuarios
String linea;
while (!(linea = scanner.nextLine()).startsWith("t-")) {
if (linea.matches("u-(0791[0-9]{7,8}) 0")) { // Usuario de línea fija
String numero = linea.substring(2, linea.indexOf(" ", 2));
// Evitar duplicados
if (usuarios.stream().noneMatch(u -> u.getNumero().equals(numero))) {
usuarios.add(new Usuario(numero, SALDO_INICIAL, new PlanFijo()));
}
} else if (linea.matches("u-(1[3-9]\\d{9}) (1|2)")) { // Usuario de móvil
String numero = linea.substring(2, linea.indexOf(" ", 2));
// Suponemos que 1 y 2 son tipos de planes móviles para simplificar
if (usuarios.stream().noneMatch(u -> u.getNumero().equals(numero))) {
usuarios.add(new Usuario(numero, SALDO_INICIAL, new PlanMovil()));
}
}
// Ignorar líneas con formato incorrecto
}
// 2. Procesar registros de comunicación
do {
if (linea.startsWith("t-")) { // Registro de llamada
RegistroLlamada llamada = parseCallRecord(linea);
if (llamada != null) {
for (Usuario user : usuarios) {
if (user.getNumero().equals(llamada.getNumeroOrigen())) {
// Asignar el tipo de llamada al usuario
user.getRegistros().addLlamada(llamada, getCallType(llamada.getNumeroOrigen(), llamada.getNumeroDestino(), llamada.getCodigoAreaOrigen(), llamada.getCodigoAreaDestino()));
break;
}
}
// Considerar también la llamada para el receptor si es móvil y hay roaming
for (Usuario user : usuarios) {
if (user.getNumero().equals(llamada.getNumeroDestino()) && user.getPlan() instanceof PlanMovil) {
user.getRegistros().addLlamada(llamada, TipoLlamada.ENTRANTE_MOVIL); // Para facturar roaming entrante si aplica
break;
}
}
}
}
// Aquí se podría añadir la lógica para registros de SMS (m-) si se extiende el sistema
} while (!(linea = scanner.nextLine()).equals("end"));
// 3. Calcular costos y mostrar resultados
// Ordenar usuarios por número de teléfono
usuarios.sort(Comparator.comparing(Usuario::getNumero));
for (Usuario user : usuarios) {
double costoTotal = user.getPlan().calcularCostoTotal(user.getRegistros(), user.getNumero());
double saldoFinal = user.getSaldoInicial() - costoTotal;
System.out.printf("%s %.2f %.2f%n", user.getNumero(), costoTotal, saldoFinal);
}
scanner.close();
}
private static RegistroLlamada parseCallRecord(String line) {
String[] parts = line.split(" ");
try {
if (parts.length == 7 && parts[0].startsWith("t-")) { // Formato: t-numOrigen numDestino YYYY.MM.DD HH:MM:SS YYYY.MM.DD HH:MM:SS
String numOrigen = parts[0].substring(2);
String numDestino = parts[1];
Date startTime = DATE_FORMAT.parse(parts[2] + " " + parts[3]);
Date endTime = DATE_FORMAT.parse(parts[4] + " " + parts[5]);
// Para líneas fijas, el código de área de origen y destino se infiere del número
String areaOrigen = numOrigen.length() == 11 || numOrigen.length() == 12 ? numOrigen.substring(0, 4) : "";
String areaDestino = numDestino.length() == 11 || numDestino.length() == 12 ? numDestino.substring(0, 4) : "";
return new RegistroLlamada(numOrigen, numDestino, startTime, endTime, areaOrigen, areaDestino);
} else if (parts.length == 8 && parts[0].startsWith("t-")) { // Formato: t-numOrigen numDestino areaDestino YYYY.MM.DD HH:MM:SS YYYY.MM.DD HH:MM:SS (fijo a móvil)
String numOrigen = parts[0].substring(2);
String numDestino = parts[1];
String areaDestino = parts[2]; // Proporcionado explícitamente
Date startTime = DATE_FORMAT.parse(parts[3] + " " + parts[4]);
Date endTime = DATE_FORMAT.parse(parts[5] + " " + parts[6]);
String areaOrigen = numOrigen.length() == 11 || numOrigen.length() == 12 ? numOrigen.substring(0, 4) : "";
return new RegistroLlamada(numOrigen, numDestino, startTime, endTime, areaOrigen, areaDestino);
} else if (parts.length == 9 && parts[0].startsWith("t-")) { // Formato: t-numOrigen areaOrigen numDestino areaDestino YYYY.MM.DD HH:MM:SS YYYY.MM.DD HH:MM:SS (móvil a móvil)
String numOrigen = parts[0].substring(2);
String areaOrigen = parts[1];
String numDestino = parts[2];
String areaDestino = parts[3];
Date startTime = DATE_FORMAT.parse(parts[4] + " " + parts[5]);
Date endTime = DATE_FORMAT.parse(parts[6] + " " + parts[7]);
return new RegistroLlamada(numOrigen, numDestino, startTime, endTime, areaOrigen, areaDestino);
}
} catch (ParseException e) {
// Ignorar registros de llamada con formato de fecha/hora inválido
return null;
}
return null; // Formato de línea no reconocido
}
private static TipoLlamada getCallType(String callerNum, String receiverNum, String callerArea, String receiverArea) {
boolean isCallerLandline = callerNum.length() <= 9; // Suponemos que números de línea fija son más cortos
boolean isReceiverLandline = receiverNum.length() <= 9;
if (isCallerLandline) {
if (callerArea.equals(AREA_CODE_NANCHANG)) { // Desde Nanchang
if (receiverArea.equals(AREA_CODE_NANCHANG)) {
return TipoLlamada.SALIENTE_FIJO_LOCAL;
} else if (AREA_CODES_JIANGXI.contains(receiverArea)) {
return TipoLlamada.SALIENTE_FIJO_PROVINCIAL;
} else {
return TipoLlamada.SALIENTE_FIJO_NACIONAL;
}
}
// Si el código de área del que llama no es Nanchang, el problema original dice que se trata como normal
// Aquí podríamos añadir lógica para otras ciudades con línea fija si fuera necesario.
} else { // Es un móvil
if (callerArea.equals(AREA_CODE_NANCHANG)) { // Desde Nanchang
if (receiverArea.equals(AREA_CODE_NANCHANG)) {
return TipoLlamada.SALIENTE_MOVIL_LOCAL;
} else if (AREA_CODES_JIANGXI.contains(receiverArea)) {
return TipoLlamada.SALIENTE_MOVIL_PROVINCIAL;
} else {
return TipoLlamada.SALIENTE_MOVIL_NACIONAL;
}
} else if (AREA_CODES_JIANGXI.contains(callerArea)) { // Roaming provincial
return TipoLlamada.SALIENTE_MOVIL_ROAMING_PROVINCIAL;
} else { // Roaming nacional
return TipoLlamada.SALIENTE_MOVIL_ROAMING_NACIONAL;
}
}
return TipoLlamada.DESCONOCIDO; // Tipo por defecto para no-match
}
public static long calculateCallMinutes(Date startTime, Date endTime) {
if (startTime == null || endTime == null || endTime.before(startTime)) {
return 0;
}
long diffInMillis = endTime.getTime() - startTime.getTime();
// Redondear a un minuto superior
return (long) Math.ceil((double) diffInMillis / (1000 * 60));
}
}
Clases de modelo y lógica de negocio:
// Enum para categorizar los tipos de llamadas
enum TipoLlamada {
SALIENTE_FIJO_LOCAL, SALIENTE_FIJO_PROVINCIAL, SALIENTE_FIJO_NACIONAL,
SALIENTE_MOVIL_LOCAL, SALIENTE_MOVIL_PROVINCIAL, SALIENTE_MOVIL_NACIONAL,
SALIENTE_MOVIL_ROAMING_PROVINCIAL, SALIENTE_MOVIL_ROAMING_NACIONAL,
ENTRANTE_MOVIL, // Para llamadas entrantes en roaming móvil
DESCONOCIDO
}
// Clase Usuario
class Usuario {
private String numero;
private double saldoInicial;
private PlanFacturacion plan;
private RegistrosUsuario registros;
public Usuario(String numero, double saldoInicial, PlanFacturacion plan) {
this.numero = numero;
this.saldoInicial = saldoInicial;
this.plan = plan;
this.registros = new RegistrosUsuario();
}
public String getNumero() { return numero; }
public double getSaldoInicial() { return saldoInicial; }
public PlanFacturacion getPlan() { return plan; }
public RegistrosUsuario getRegistros() { return registros; }
}
// Clase RegistrosUsuario
class RegistrosUsuario {
private List<RegistroLlamada> llamadasSalientesLocal;
private List<RegistroLlamada> llamadasSalientesProvincial;
private List<RegistroLlamada> llamadasSalientesNacional;
private List<RegistroLlamada> llamadasSalientesRoamingProvincial;
private List<RegistroLlamada> llamadasSalientesRoamingNacional;
private List<RegistroLlamada> llamadasEntrantesMovil; // Para roaming entrante móvil
private List<RegistroMensaje> mensajesEnviados;
private List<RegistroMensaje> mensajesRecibidos;
public RegistrosUsuario() {
llamadasSalientesLocal = new ArrayList<>();
llamadasSalientesProvincial = new ArrayList<>();
llamadasSalientesNacional = new ArrayList<>();
llamadasSalientesRoamingProvincial = new ArrayList<>();
llamadasSalientesRoamingNacional = new ArrayList<>();
llamadasEntrantesMovil = new ArrayList<>();
mensajesEnviados = new ArrayList<>();
mensajesRecibidos = new ArrayList<>();
}
public void addLlamada(RegistroLlamada record, TipoLlamada tipo) {
switch (tipo) {
case SALIENTE_FIJO_LOCAL:
case SALIENTE_MOVIL_LOCAL:
llamadasSalientesLocal.add(record);
break;
case SALIENTE_FIJO_PROVINCIAL:
case SALIENTE_MOVIL_PROVINCIAL:
llamadasSalientesProvincial.add(record);
break;
case SALIENTE_FIJO_NACIONAL:
case SALIENTE_MOVIL_NACIONAL:
llamadasSalientesNacional.add(record);
break;
case SALIENTE_MOVIL_ROAMING_PROVINCIAL:
llamadasSalientesRoamingProvincial.add(record);
break;
case SALIENTE_MOVIL_ROAMING_NACIONAL:
llamadasSalientesRoamingNacional.add(record);
break;
case ENTRANTE_MOVIL:
llamadasEntrantesMovil.add(record);
break;
default:
// Manejo de tipo desconocido o ignorado
break;
}
}
// Getters para las listas de registros
public List<RegistroLlamada> getLlamadasSalientesLocal() { return llamadasSalientesLocal; }
public List<RegistroLlamada> getLlamadasSalientesProvincial() { return llamadasSalientesProvincial; }
public List<RegistroLlamada> getLlamadasSalientesNacional() { return llamadasSalientesNacional; }
public List<RegistroLlamada> getLlamadasSalientesRoamingProvincial() { return llamadasSalientesRoamingProvincial; }
public List<RegistroLlamada> getLlamadasSalientesRoamingNacional() { return llamadasSalientesRoamingNacional; }
public List<RegistroLlamada> getLlamadasEntrantesMovil() { return llamadasEntrantesMovil; }
// ... otros getters para mensajes, etc.
}
// Clase abstracta RegistroComunicacion
abstract class RegistroComunicacion {
protected String numeroOrigen;
protected String numeroDestino;
public RegistroComunicacion(String numeroOrigen, String numeroDestino) {
this.numeroOrigen = numeroOrigen;
this.numeroDestino = numeroDestino;
}
public String getNumeroOrigen() { return numeroOrigen; }
public String getNumeroDestino() { return numeroDestino; }
}
// Clase RegistroLlamada
class RegistroLlamada extends RegistroComunicacion {
private Date horaInicio;
private Date horaFin;
private String codigoAreaOrigen;
private String codigoAreaDestino;
public RegistroLlamada(String numeroOrigen, String numeroDestino, Date horaInicio, Date horaFin,
String codigoAreaOrigen, String codigoAreaDestino) {
super(numeroOrigen, numeroDestino);
this.horaInicio = horaInicio;
this.horaFin = horaFin;
this.codigoAreaOrigen = codigoAreaOrigen;
this.codigoAreaDestino = codigoAreaDestino;
}
public Date getHoraInicio() { return horaInicio; }
public Date getHoraFin() { return horaFin; }
public String getCodigoAreaOrigen() { return codigoAreaOrigen; }
public String getCodigoAreaDestino() { return codigoAreaDestino; }
}
// Clase RegistroMensaje (implementación simplificada)
class RegistroMensaje extends RegistroComunicacion {
private String contenido;
public RegistroMensaje(String numeroOrigen, String numeroDestino, String contenido) {
super(numeroOrigen, numeroDestino);
this.contenido = contenido;
}
public String getContenido() { return contenido; }
}
// Interfaz PlanFacturacion
interface PlanFacturacion {
double calcularCostoTotal(RegistrosUsuario registros, String numeroUsuario);
double getCuotaMensual();
}
// Clase abstracta base para planes de facturación
abstract class AbstractPlanFacturacion implements PlanFacturacion {
protected List<ReglaFacturacion> reglas;
public AbstractPlanFacturacion() {
this.reglas = new ArrayList<>();
inicializarReglas();
}
protected abstract void inicializarReglas();
@Override
public double calcularCostoTotal(RegistrosUsuario registros, String numeroUsuario) {
double totalCost = getCuotaMensual();
for (ReglaFacturacion regla : reglas) {
totalCost += regla.calcularCosto(registros, numeroUsuario);
}
return totalCost;
}
}
// Implementación PlanFijo
class PlanFijo extends AbstractPlanFacturacion {
@Override
protected void inicializarReglas() {
reglas.add(new ReglaLlamadaFijaLocal());
reglas.add(new ReglaLlamadaFijaProvincial());
reglas.add(new ReglaLlamadaFijaNacional());
}
@Override
public double getCuotaMensual() {
return 20.0;
}
}
// Implementación PlanMovil
class PlanMovil extends AbstractPlanFacturacion {
@Override
protected void inicializarReglas() {
reglas.add(new ReglaLlamadaMovilLocal());
reglas.add(new ReglaLlamadaMovilProvincial());
reglas.add(new ReglaLlamadaMovilNacional());
reglas.add(new ReglaLlamadaMovilRoamingProvincial());
reglas.add(new ReglaLlamadaMovilRoamingNacional());
reglas.add(new ReglaLlamadaMovilEntranteRoaming());
}
@Override
public double getCuotaMensual() {
return 15.0;
}
}
// Interfaz ReglaFacturacion
interface ReglaFacturacion {
double calcularCosto(RegistrosUsuario registros, String numeroUsuario);
}
// Clases de reglas de facturación (ejemplos)
class ReglaLlamadaFijaLocal implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesLocal()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) { // Asegurarse de que es la llamada de este usuario
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.1;
}
}
return costo;
}
}
class ReglaLlamadaFijaProvincial implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesProvincial()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.3;
}
}
return costo;
}
}
class ReglaLlamadaFijaNacional implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesNacional()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.6;
}
}
return costo;
}
}
class ReglaLlamadaMovilLocal implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesLocal()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.1;
}
}
return costo;
}
}
class ReglaLlamadaMovilProvincial implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesProvincial()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.2;
}
}
return costo;
}
}
class ReglaLlamadaMovilNacional implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesNacional()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.3;
}
}
return costo;
}
}
class ReglaLlamadaMovilRoamingProvincial implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesRoamingProvincial()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.3;
}
}
return costo;
}
}
class ReglaLlamadaMovilRoamingNacional implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasSalientesRoamingNacional()) {
if (llamada.getNumeroOrigen().equals(numeroUsuario)) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.6;
}
}
return costo;
}
}
class ReglaLlamadaMovilEntranteRoaming implements ReglaFacturacion {
@Override
public double calcularCosto(RegistrosUsuario registros, String numeroUsuario) {
double costo = 0;
for (RegistroLlamada llamada : registros.getLlamadasEntrantesMovil()) {
// Solo se cobra si el usuario es el receptor y está en roaming nacional
// Asumimos que `codigoAreaDestino` de la llamada refleja la ubicación del receptor.
if (llamada.getNumeroDestino().equals(numeroUsuario) && !SistemaFacturacion.AREA_CODES_JIANGXI.contains(llamada.getCodigoAreaDestino())) {
costo += SistemaFacturacion.calculateCallMinutes(llamada.getHoraInicio(), llamada.getHoraFin()) * 0.3;
}
}
return costo;
}
}
// ... otras reglas para SMS si se implementan
Lecciones Aprendidas y Recomendaciones
La experiencia en el desarrollo de este sistema subraya la importancia de un diseño de clases bien pensado desde el principio. Entender y aplicar los principios de la programación orientada a objetos, como la herencia y el polimorfismo, es crucial para crear código reutilizable y fácil de mantener.
- Diseño basado en diagramas de clases: Iniciar con un diagrama de clases claro ayuda a visualizar la estructura del sistema y las relaciones entre los objetos, evitando errores de diseño y facilitando la implementación.
- Herencia y polimorfismo: Utilizar jerarquías de clases (como
PlanFacturacionyReglaFacturacion) reduce la duplicación de código y permite extender el sistema con nuevos comportamientos sin modificar las clases existentes. Por ejemplo, agregar un nuevo tipo de plan de facturación simplemente implica implementar la interfazPlanFacturaciony sus reglas asociadas. - Manejo robusto de entradas: La validación y el parseo de entradas son puntos críticos. En lugar de depender de expresiones regulares complejas para todo, es a menudo más efectivo combinar
String.split()con una validación de formato más estructurada y manejo de excepciones, especialmente para fechas y horas. - Modularidad: Descomponer el problema en módulos (clases y métodos) más pequeños y enfocados hace que el código sea más legible, testeable y manejable. Por ejemplo, una clase
SistemaFacturacionse encarga de la lógica principal y delega las responsabilidades de cálculo de costos a los objetosPlanFacturacionyReglaFacturacion.