Ir al contenido principal

Encapsulación y niveles de acceso

La encapsulación es uno de los pilares fundamentales de la programación orientada a objetos, junto con la herencia y el polimorfismo. Este principio establece que los datos internos de un objeto deben estar protegidos del acceso directo desde el exterior, permitiendo que solo métodos específicos puedan modificarlos de manera controlada.

Los niveles de acceso en C# proporcionan las herramientas necesarias para implementar la encapsulación de manera efectiva, definiendo qué partes de tu código pueden acceder a los miembros de una clase. Dominar estos conceptos te permitirá crear código más seguro, mantenible y menos propenso a errores.

A lo largo de este artículo, exploraremos los diferentes modificadores de acceso disponibles en C#, cómo aplicar correctamente la encapsulación, y las mejores prácticas para proteger la integridad de tus objetos.

Concepto de encapsulación

La encapsulación es el principio de programación que consiste en ocultar los detalles internos de implementación de una clase y exponer solo una interfaz controlada para interactuar con los objetos. Este concepto se basa en la idea de crear una "caja negra" donde el usuario del objeto no necesita conocer cómo funciona internamente, solo cómo utilizarlo.

Beneficios de la encapsulación

Beneficio Descripción
Protección de datos Evita modificaciones no controladas de los datos internos
Mantenibilidad Permite cambiar la implementación interna sin afectar el código externo
Validación Posibilita verificar que los datos cumplan ciertas reglas antes de almacenarlos
Abstracción Simplifica el uso de la clase al ocultar complejidad innecesaria
Integridad Garantiza que el objeto mantenga un estado válido en todo momento

Ejemplo sin encapsulación (problemático)

// Clase SIN encapsulación - EVITAR este enfoque
public class CuentaBancariaProblematica
{
    public string titular;          // Campo público - problemático
    public decimal saldo;           // Campo público - problemático
    public string numeroCuenta;     // Campo público - problemático
}

class Program
{
    static void Main()
    {
        CuentaBancariaProblematica cuenta = new CuentaBancariaProblematica();
        
        // Problemas: acceso directo a los campos
        cuenta.titular = "";           // Titular vacío - no válido
        cuenta.saldo = -1000;          // Saldo negativo - problemático
        cuenta.numeroCuenta = "123";   // Número muy corto - inválido
        
        Console.WriteLine($"Titular: '{cuenta.titular}'");
        Console.WriteLine($"Saldo: {cuenta.saldo}");
        Console.WriteLine($"Número: {cuenta.numeroCuenta}");
        
        // El objeto está en un estado inválido y no hay forma de evitarlo
    }
}

Ejemplo con encapsulación (correcto)

// Clase CON encapsulación - Enfoque correcto
public class CuentaBancaria
{
    // Campos privados - datos protegidos
    private string titular;
    private decimal saldo;
    private string numeroCuenta;
    
    // Constructor para inicialización controlada
    public CuentaBancaria(string nombreTitular, string numero)
    {
        EstablecerTitular(nombreTitular);
        EstablecerNumeroCuenta(numero);
        saldo = 0; // Saldo inicial siempre 0
    }
    
    // Métodos públicos para acceso controlado
    public string ObtenerTitular()
    {
        return titular;
    }
    
    public void EstablecerTitular(string nombreTitular)
    {
        if (string.IsNullOrWhiteSpace(nombreTitular))
        {
            throw new ArgumentException("El nombre del titular no puede estar vacío");
        }
        titular = nombreTitular;
    }
    
    public decimal ObtenerSaldo()
    {
        return saldo;
    }
    
    public string ObtenerNumeroCuenta()
    {
        return numeroCuenta;
    }
    
    private void EstablecerNumeroCuenta(string numero)
    {
        if (string.IsNullOrWhiteSpace(numero) || numero.Length < 8)
        {
            throw new ArgumentException("El número de cuenta debe tener al menos 8 caracteres");
        }
        numeroCuenta = numero;
    }
    
    public bool Depositar(decimal cantidad)
    {
        if (cantidad <= 0)
        {
            Console.WriteLine("La cantidad a depositar debe ser positiva");
            return false;
        }
        
        saldo += cantidad;
        Console.WriteLine($"Depósito exitoso. Nuevo saldo: ${saldo:F2}");
        return true;
    }
    
    public bool Retirar(decimal cantidad)
    {
        if (cantidad <= 0)
        {
            Console.WriteLine("La cantidad a retirar debe ser positiva");
            return false;
        }
        
        if (cantidad > saldo)
        {
            Console.WriteLine("Fondos insuficientes");
            return false;
        }
        
        saldo -= cantidad;
        Console.WriteLine($"Retiro exitoso. Nuevo saldo: ${saldo:F2}");
        return true;
    }
}

class Program
{
    static void Main()
    {
        try
        {
            // Creación controlada del objeto
            CuentaBancaria cuenta = new CuentaBancaria("Ana García", "CTA-12345678");
            
            // Acceso controlado a los datos
            Console.WriteLine($"Titular: {cuenta.ObtenerTitular()}");
            Console.WriteLine($"Saldo inicial: ${cuenta.ObtenerSaldo():F2}");
            Console.WriteLine($"Número: {cuenta.ObtenerNumeroCuenta()}");
            
            // Operaciones controladas
            cuenta.Depositar(1000);
            cuenta.Retirar(250);
            cuenta.Retirar(2000); // Esto fallará por fondos insuficientes
            
            Console.WriteLine($"Saldo final: ${cuenta.ObtenerSaldo():F2}");
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

Modificadores de acceso en C#

C# proporciona varios modificadores de acceso que definen el nivel de visibilidad de los miembros de una clase:

Tabla de modificadores de acceso

Modificador Nivel de acceso Descripción
public Público Accesible desde cualquier parte del código
private Privado Accesible solo dentro de la misma clase
protected Protegido Accesible dentro de la clase y sus clases derivadas
internal Interno Accesible dentro del mismo ensamblado (assembly)
protected internal Protegido interno Combinación de protected e internal
private protected Privado protegido Accesible dentro de la clase y clases derivadas del mismo ensamblado

Modificador public

Los miembros public son accesibles desde cualquier parte del código:

public class CalculadoraBasica
{
    // Método público - accesible desde cualquier lugar
    public double Sumar(double a, double b)
    {
        return a + b;
    }
    
    // Propiedad pública - accesible desde cualquier lugar
    public string Version { get; set; } = "1.0";
}

class Program
{
    static void Main()
    {
        CalculadoraBasica calc = new CalculadoraBasica();
        
        // Acceso público desde otra clase
        double resultado = calc.Sumar(5, 3);
        Console.WriteLine($"Resultado: {resultado}");
        
        Console.WriteLine($"Versión: {calc.Version}");
        calc.Version = "1.1"; // Modificación permitida
    }
}

Modificador private

Los miembros private solo son accesibles dentro de la misma clase:

public class TemperaturaSensor
{
    // Campo privado - solo accesible dentro de esta clase
    private double temperaturaInterna;
    private bool sensorActivo;
    
    public TemperaturaSensor()
    {
        InicializarSensor(); // Método privado llamado internamente
    }
    
    // Método privado - lógica interna oculta
    private void InicializarSensor()
    {
        temperaturaInterna = 20.0; // Temperatura ambiente por defecto
        sensorActivo = true;
        Console.WriteLine("Sensor inicializado");
    }
    
    // Método público que usa datos privados
    public double LeerTemperatura()
    {
        if (!sensorActivo)
        {
            throw new InvalidOperationException("El sensor no está activo");
        }
        
        // Simular lectura de temperatura
        Random random = new Random();
        temperaturaInterna += (random.NextDouble() - 0.5) * 2; // Variación aleatoria
        
        return Math.Round(temperaturaInterna, 1);
    }
    
    public void ApagarSensor()
    {
        sensorActivo = false;
        Console.WriteLine("Sensor apagado");
    }
}

class Program
{
    static void Main()
    {
        TemperaturaSensor sensor = new TemperaturaSensor();
        
        // Acceso permitido a métodos públicos
        for (int i = 0; i < 3; i++)
        {
            double temp = sensor.LeerTemperatura();
            Console.WriteLine($"Temperatura: {temp}°C");
        }
        
        sensor.ApagarSensor();
        
        // Los siguientes accesos NO están permitidos (provocarían error de compilación):
        // sensor.temperaturaInterna = 100;  // Error: campo privado
        // sensor.InicializarSensor();       // Error: método privado
        // sensor.sensorActivo = true;       // Error: campo privado
    }
}

Modificador protected

Los miembros protected son accesibles en la clase actual y en sus clases derivadas:

// Clase base
public class Vehiculo
{
    protected string marca;        // Accesible en clases derivadas
    protected int añoFabricacion;  // Accesible en clases derivadas
    private string numeroChasis;   // Solo accesible en esta clase
    
    public Vehiculo(string marcaVehiculo, int año)
    {
        marca = marcaVehiculo;
        añoFabricacion = año;
        numeroChasis = GenerarNumeroChasis();
    }
    
    // Método protegido - puede ser usado por clases derivadas
    protected virtual void MostrarInformacionBasica()
    {
        Console.WriteLine($"Marca: {marca}");
        Console.WriteLine($"Año: {añoFabricacion}");
    }
    
    private string GenerarNumeroChasis()
    {
        return $"CH-{DateTime.Now.Ticks}";
    }
    
    public void MostrarNumeroChasis()
    {
        Console.WriteLine($"Número de chasis: {numeroChasis}");
    }
}

// Clase derivada
public class Automovil : Vehiculo
{
    private int numeroPuertas;
    
    public Automovil(string marca, int año, int puertas) : base(marca, año)
    {
        numeroPuertas = puertas;
    }
    
    public void MostrarInformacionCompleta()
    {
        // Acceso a miembros protegidos de la clase base
        Console.WriteLine("=== Información del Automóvil ===");
        MostrarInformacionBasica(); // Método protegido heredado
        
        // Acceso directo a campos protegidos
        Console.WriteLine($"Marca (acceso directo): {marca}");
        Console.WriteLine($"Año (acceso directo): {añoFabricacion}");
        Console.WriteLine($"Puertas: {numeroPuertas}");
        
        // NO es posible acceder a miembros privados de la clase base:
        // Console.WriteLine(numeroChasis); // Error: es privado
    }
}

class Program
{
    static void Main()
    {
        Automovil auto = new Automovil("Toyota", 2023, 4);
        
        // Acceso a método público
        auto.MostrarInformacionCompleta();
        auto.MostrarNumeroChasis();
        
        // Los siguientes accesos NO están permitidos desde fuera de la jerarquía:
        // auto.marca = "Honda";                    // Error: protegido
        // auto.MostrarInformacionBasica();         // Error: protegido
    }
}

Modificador internal

Los miembros internal son accesibles dentro del mismo ensamblado (assembly):

// Clase con miembros internos
public class ConfiguracionSistema
{
    // Propiedad interna - accesible solo dentro del mismo ensamblado
    internal string RutaConfiguracion { get; set; }
    
    // Campo interno
    internal bool ModoDesarrollo;
    
    private string claveSecreta;
    
    public ConfiguracionSistema()
    {
        RutaConfiguracion = @"C:\Config\app.config";
        ModoDesarrollo = true;
        claveSecreta = "clave-super-secreta";
    }
    
    // Método interno
    internal void CargarConfiguracionDesarrollo()
    {
        if (ModoDesarrollo)
        {
            Console.WriteLine("Cargando configuración de desarrollo...");
            RutaConfiguracion = @"C:\Config\dev.config";
        }
    }
    
    public void MostrarConfiguracion()
    {
        Console.WriteLine($"Ruta de configuración: {RutaConfiguracion}");
        Console.WriteLine($"Modo desarrollo: {ModoDesarrollo}");
    }
}

// Otra clase en el mismo ensamblado
public class GestorConfiguracion
{
    public void ConfigurarSistema()
    {
        ConfiguracionSistema config = new ConfiguracionSistema();
        
        // Acceso permitido a miembros internos (mismo ensamblado)
        config.RutaConfiguracion = @"C:\Config\custom.config";
        config.ModoDesarrollo = false;
        config.CargarConfiguracionDesarrollo(); // Método interno accesible
        
        config.MostrarConfiguracion();
        
        // Este acceso NO está permitido:
        // config.claveSecreta = "nueva-clave"; // Error: es privado
    }
}

class Program
{
    static void Main()
    {
        GestorConfiguracion gestor = new GestorConfiguracion();
        gestor.ConfigurarSistema();
    }
}

Propiedades y encapsulación

Las propiedades en C# proporcionan una forma elegante de implementar encapsulación, combinando la simplicidad de acceso de los campos con el control de los métodos:

Propiedades automáticas

public class Producto
{
    // Propiedades automáticas con diferentes niveles de acceso
    public string Nombre { get; set; }              // Lectura y escritura pública
    public decimal Precio { get; private set; }     // Lectura pública, escritura privada
    public DateTime FechaCreacion { get; }          // Solo lectura (readonly)
    
    public Producto(string nombre, decimal precio)
    {
        Nombre = nombre;
        Precio = precio;
        FechaCreacion = DateTime.Now; // Solo se puede establecer en el constructor
    }
    
    // Método para modificar el precio de forma controlada
    public void ActualizarPrecio(decimal nuevoPrecio)
    {
        if (nuevoPrecio < 0)
        {
            throw new ArgumentException("El precio no puede ser negativo");
        }
        
        Precio = nuevoPrecio; // Acceso privado desde dentro de la clase
    }
}

class Program
{
    static void Main()
    {
        Producto producto = new Producto("Laptop", 999.99m);
        
        // Accesos permitidos
        Console.WriteLine($"Nombre: {producto.Nombre}");
        Console.WriteLine($"Precio: ${producto.Precio}");
        Console.WriteLine($"Creado: {producto.FechaCreacion}");
        
        producto.Nombre = "Laptop Gaming";        // Permitido: set público
        producto.ActualizarPrecio(1199.99m);      // Permitido: método controlado
        
        // Accesos NO permitidos:
        // producto.Precio = 500;           // Error: set es privado
        // producto.FechaCreacion = ...;    // Error: solo lectura
        
        Console.WriteLine($"Precio actualizado: ${producto.Precio}");
    }
}

Propiedades con lógica personalizada

public class CuentaAhorros
{
    private decimal saldo;
    private decimal tasaInteres;
    private int transaccionesMensuales;
    
    // Propiedad con validación personalizada
    public decimal Saldo
    {
        get 
        { 
            return saldo; 
        }
        private set 
        { 
            if (value < 0)
                throw new ArgumentException("El saldo no puede ser negativo");
            saldo = value; 
        }
    }
    
    // Propiedad con lógica de validación
    public decimal TasaInteres
    {
        get { return tasaInteres; }
        set
        {
            if (value < 0 || value > 0.1m) // Entre 0% y 10%
            {
                throw new ArgumentException("La tasa de interés debe estar entre 0% y 10%");
            }
            tasaInteres = value;
        }
    }
    
    // Propiedad calculada (solo lectura)
    public decimal InteresAnualEstimado
    {
        get
        {
            return saldo * tasaInteres;
        }
    }
    
    // Propiedad con acceso controlado
    public int TransaccionesMensuales
    {
        get { return transaccionesMensuales; }
        private set { transaccionesMensuales = value; }
    }
    
    public CuentaAhorros(decimal saldoInicial, decimal tasa)
    {
        Saldo = saldoInicial;
        TasaInteres = tasa;
        TransaccionesMensuales = 0;
    }
    
    public bool Depositar(decimal cantidad)
    {
        if (cantidad <= 0)
        {
            Console.WriteLine("La cantidad debe ser positiva");
            return false;
        }
        
        Saldo += cantidad;
        TransaccionesMensuales++;
        Console.WriteLine($"Depósito exitoso. Nuevo saldo: ${Saldo:F2}");
        return true;
    }
    
    public bool Retirar(decimal cantidad)
    {
        if (cantidad <= 0)
        {
            Console.WriteLine("La cantidad debe ser positiva");
            return false;
        }
        
        if (cantidad > Saldo)
        {
            Console.WriteLine("Fondos insuficientes");
            return false;
        }
        
        Saldo -= cantidad;
        TransaccionesMensuales++;
        Console.WriteLine($"Retiro exitoso. Nuevo saldo: ${Saldo:F2}");
        return true;
    }
    
    public void MostrarInformacion()
    {
        Console.WriteLine($"Saldo actual: ${Saldo:F2}");
        Console.WriteLine($"Tasa de interés: {TasaInteres:P2}");
        Console.WriteLine($"Interés anual estimado: ${InteresAnualEstimado:F2}");
        Console.WriteLine($"Transacciones este mes: {TransaccionesMensuales}");
    }
}

class Program
{
    static void Main()
    {
        try
        {
            CuentaAhorros cuenta = new CuentaAhorros(1000m, 0.03m); // 3% de interés
            
            cuenta.MostrarInformacion();
            Console.WriteLine();
            
            // Operaciones válidas
            cuenta.Depositar(500);
            cuenta.Retirar(200);
            
            // Intentar cambiar la tasa de interés
            cuenta.TasaInteres = 0.05m; // 5% - válido
            
            Console.WriteLine("\nDespués de las operaciones:");
            cuenta.MostrarInformacion();
            
            // Esto provocará una excepción:
            // cuenta.TasaInteres = 0.15m; // Error: tasa demasiado alta
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

Resumen

La encapsulación es un principio fundamental que protege la integridad de los objetos mediante el control del acceso a sus datos internos. Los modificadores de acceso en C# (public, private, protected, internal) proporcionan las herramientas necesarias para implementar diferentes niveles de protección según las necesidades específicas de cada situación.

Las propiedades ofrecen una forma elegante de combinar la facilidad de uso de los campos públicos con el control y la validación de los métodos, permitiendo crear interfaces limpias y seguras para interactuar con los objetos. Dominar estos conceptos te permitirá crear código más robusto, mantenible y menos propenso a errores, estableciendo una base sólida para el desarrollo de aplicaciones complejas.