Diseño y Desarrollo de un Sistema de Facturación de Telecomunicaciones en Java

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 de RegistrosUsuario.
  • 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 (extiende RegistroComunicacion): Registra detalles de una llamada, incluyendo hora de inicio, hora de fin, y códigos de área de origen y destino.
  • RegistroMensaje (extiende RegistroComunicacion): 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 de ReglaFacturacion.
  • PlanFijo (implementa PlanFacturacion): Implementación específica para líneas fijas.
  • PlanMovil (implementa PlanFacturacion): 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 PlanFacturacion y ReglaFacturacion) 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 interfaz PlanFacturacion y 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 SistemaFacturacion se encarga de la lógica principal y delega las responsabilidades de cálculo de costos a los objetos PlanFacturacion y ReglaFacturacion.

Etiquetas: java Programación Orientada a Objetos POO diseño de software Sistemas de Facturación

Publicado el 7-22 09:36