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.