Hay un problema latente en el modelo de BiblioTech tal y como ha quedado hasta ahora: cualquier
parte del programa puede escribir directamente libro1.Disponible = true;, sin pasar por
Prestar() ni Devolver(), dejando el catálogo en un estado incoherente (por ejemplo, un
libro marcado disponible que en realidad sigue en manos de un socio). El encapsulamiento es
el principio de la programación orientada a objetos que resuelve justo esto: ocultar el estado
interno de un objeto y exponer únicamente las operaciones controladas a través de las cuales
ese estado puede cambiar. En esta lección aprenderás los modificadores de acceso de C# y
cerrarás esa puerta trasera de Disponible, para que solo pueda modificarse mediante los
métodos pensados para ello.
Contenido
- Qué es el encapsulamiento y por qué importa
- Modificadores de acceso:
public,private,protected,internal - El problema concreto:
Disponiblemodificable libremente private set: exponer lectura, restringir escritura- Propiedades con lógica de validación en el
set - Propiedades de solo lectura
- Aplicando encapsulamiento al resto del modelo:
SocioyPrestamo - El principio de mínima exposición
- Qué es el encapsulamiento y por qué importa
El encapsulamiento consiste en ocultar los detalles internos de cómo un objeto almacena y
gestiona su estado, exponiendo hacia fuera únicamente una interfaz controlada de operaciones
válidas. La idea central es: el código externo a la clase no debería poder dejar un objeto en
un estado incoherente. Hasta ahora, cualquier código con acceso a un objeto Libro puede
escribir libro1.Disponible = false; sin haber llamado realmente a Prestar(), saltándose
por completo la lógica que decide si el préstamo es válido (por ejemplo, comprobar que el
libro estuviera realmente disponible antes de prestarlo). El encapsulamiento cierra esa vía de
escape.
- Modificadores de acceso:
public, private, protected, internal
public, private, protected, internalC# ofrece varios modificadores de acceso que controlan desde dónde se puede usar un miembro (propiedad, método, campo) de una clase:
| Modificador | Accesible desde... |
|---|---|
public |
Cualquier parte del programa, sin restricciones |
private |
Únicamente desde dentro de la propia clase |
protected |
Desde la propia clase y desde cualquier clase que herede de ella |
internal |
Desde cualquier parte del mismo ensamblado (proyecto compilado), pero no desde otros proyectos externos |
Un ejemplo breve y genérico, fuera del modelo de BiblioTech, ayuda a fijar la diferencia entre
private y protected:
class Base
{
private int valorPrivado = 1;
protected int valorProtegido = 2;
public int valorPublico = 3;
public void MostrarDesdeBase()
{
Console.WriteLine(valorPrivado); // Ok: mismo clase
Console.WriteLine(valorProtegido); // Ok: mismo clase
}
}
class Derivada : Base
{
public void MostrarDesdeDerivada()
{
// Console.WriteLine(valorPrivado); // Error: no accesible, es private en Base
Console.WriteLine(valorProtegido); // Ok: protected SI es accesible desde una clase heredera
Console.WriteLine(valorPublico); // Ok: publico, accesible desde cualquier sitio
}
}
Derivada objeto = new Derivada();
Console.WriteLine(objeto.valorPublico); // Ok: publico
// Console.WriteLine(objeto.valorProtegido); // Error: protected no es accesible desde fuera de la jerarquiaprotected ocupa una posición intermedia entre private (solo la propia clase) y public
(todo el programa): es visible para las clases herederas, pero sigue oculto para el resto del
código externo a la jerarquía. Este curso usará sobre todo public y private; protected
aparecerá de forma puntual cuando una clase base necesite compartir algo con sus herederas sin
exponerlo al resto del programa, e internal no será necesario en los ejemplos de BiblioTech,
ya que todo el proyecto se compila como una única aplicación.
- El problema concreto:
Disponible modificable libremente
Disponible modificable librementeAsí estaba definida, hasta ahora, la propiedad Disponible:
class MaterialBibliotecario
{
public string Titulo { get; set; }
public string Autor { get; set; }
public bool Disponible { get; set; } = true;
// ...
}Con { get; set; } públicos, nada impide este código, en cualquier parte del programa:
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
libro1.Disponible = true; // Se salta por completo la logica de Prestar()/Devolver()Este Disponible = true directo no pasa por ninguna comprobación: no verifica si había un
préstamo en curso, no registra ningún mensaje, no aplica ninguna regla de negocio. El resultado
es un objeto cuyo estado (Disponible) puede quedar desincronizado del resto del sistema (por
ejemplo, de los objetos Prestamo que referencian ese libro).
private set: exponer lectura, restringir escritura
private set: exponer lectura, restringir escrituraLa solución más directa es permitir que Disponible se lea desde cualquier parte
(public get), pero que solo se escriba desde dentro de la propia clase
(private set):
class MaterialBibliotecario
{
public string Titulo { get; set; }
public string Autor { get; set; }
public bool Disponible { get; private set; } = true;
public MaterialBibliotecario(string titulo, string autor)
{
Titulo = titulo;
Autor = autor;
}
public void Prestar()
{
if (Disponible)
{
Disponible = false; // Ok: estamos dentro de la propia clase
Console.WriteLine($"'{Titulo}' ha sido prestado.");
}
else
{
Console.WriteLine($"'{Titulo}' no esta disponible para prestamo.");
}
}
public void Devolver()
{
Disponible = true; // Ok: estamos dentro de la propia clase
Console.WriteLine($"'{Titulo}' ha sido devuelto.");
}
}Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
Console.WriteLine(libro1.Disponible); // Ok: lectura publica, True
libro1.Prestar(); // Ok: cambia Disponible internamente, a traves del metodo
// libro1.Disponible = true; // Error de compilacion: el set es private, no accesible desde fueraCon private set, la última línea ya no compila: el compilador impide, ahora de forma
garantizada (no solo por convención o buena voluntad), que código externo a la clase
asigne un valor a Disponible sin pasar por Prestar() o Devolver(). Esta es la forma más
simple y habitual de encapsular una propiedad en C#: exponer la lectura, encerrar la escritura
dentro de operaciones con significado (Prestar, Devolver), en lugar de un set genérico.
- Propiedades con lógica de validación en el
set
setprivate set resuelve el caso de Disponible, pero a veces se necesita algo más flexible:
permitir la escritura desde fuera, pero validando el valor antes de aceptarlo. Para eso
hace falta una propiedad completa, con un campo (backing field) explícito detrás:
class MaterialBibliotecario
{
private string _titulo;
public string Titulo
{
get { return _titulo; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("El titulo no puede estar vacio.");
}
_titulo = value;
}
}
// Autor, Disponible, constructor, Prestar(), Devolver() como en el apartado anterior
}Dentro del bloque set, la palabra clave value representa automáticamente el valor que se
está intentando asignar. Aquí, si alguien intenta poner un título vacío o compuesto solo por
espacios (string.IsNullOrWhiteSpace), la propiedad lanza una ArgumentException —recuerda el
manejo de excepciones del Módulo 2— en lugar de aceptar un dato inválido. _titulo, con guion
bajo inicial en minúscula, es la convención habitual en C# para nombrar el campo privado que
respalda a una propiedad con lógica propia.
Libro libro1 = new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4");
libro1.Titulo = "";
// Lanza ArgumentException: "El titulo no puede estar vacio."Propiedad autoimplementada ({ get; set; }) |
Propiedad completa con validación | |
|---|---|---|
| Sintaxis | Una sola línea, sin campo visible | get/set explícitos, con campo _titulo propio |
| Permite validar el valor antes de aceptarlo | No | Sí |
| Cuándo usarla | El dato no necesita ninguna restricción especial | Hay una regla de negocio que debe cumplirse siempre |
- Propiedades de solo lectura
Un tercer nivel de restricción, más estricto que private set, es no declarar ningún set en
absoluto: la propiedad solo puede asignarse dentro del constructor (o mediante un
inicializador), y nunca más después:
class Socio
{
public int Id { get; } // solo lectura: nunca cambia tras crear el objeto
public string Nombre { get; set; }
public Socio(int id, string nombre)
{
Id = id;
Nombre = nombre;
}
}public int Id { get; } (sin set de ningún tipo) expresa la intención con total claridad:
el identificador de un socio se fija al crearlo y no debe cambiar nunca durante toda la vida
del objeto. Intentar escribir socio1.Id = 5; fuera del constructor sería un error de
compilación, igual que ocurriría con private set, pero aquí queda claro que ni siquiera la
propia clase necesita reasignarlo más adelante.
- Aplicando encapsulamiento al resto del modelo:
Socio y Prestamo
Socio y PrestamoCon estas tres técnicas (private set, propiedades con validación, y propiedades de solo
lectura), se puede revisar el resto del modelo de BiblioTech para que cada dato se exponga
únicamente con el nivel de control que le corresponde:
class Prestamo
{
public Libro Libro { get; }
public Socio Socio { get; }
public DateTime FechaPrestamo { get; }
public DateTime? FechaDevolucion { get; private set; }
public Prestamo(Libro libro, Socio socio)
{
Libro = libro;
Socio = socio;
FechaPrestamo = DateTime.Now;
FechaDevolucion = null;
}
public void RegistrarDevolucion()
{
FechaDevolucion = DateTime.Now;
}
}Ahora Libro, Socio y FechaPrestamo de un Prestamo son de solo lectura (no tiene sentido
que un préstamo "cambie" de libro o de socio una vez creado), y FechaDevolucion solo puede
fijarse desde dentro de la propia clase Prestamo, a través del método RegistrarDevolucion()
—nunca asignando la fecha directamente desde fuera:
Libro libro1 = new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1");
Socio socio1 = new Socio(1, "Ana Martinez");
libro1.Prestar();
Prestamo prestamo1 = new Prestamo(libro1, socio1);
// prestamo1.FechaDevolucion = DateTime.Now; // Error de compilacion: el set es private
prestamo1.RegistrarDevolucion(); // forma correcta y unica de registrar la devolucion
libro1.Devolver();
- El principio de mínima exposición
Todo lo visto en esta lección se resume en un único principio práctico: expón de tu clase
únicamente lo mínimo que el resto del programa necesita usar, y nada más. Cada propiedad,
método o campo público es una promesa hacia el resto del código: cuanto más expongas, más
formas tendrá el resto del programa de acoplarse a los detalles internos de tu clase, y más
difícil será cambiar esos detalles el día de mañana sin romper algo. Antes de marcar algo como
public, pregúntate si de verdad necesita serlo, o si bastaría con private (o, en el caso de
propiedades, con restringir solo el set).
| Regla práctica | Aplicada en BiblioTech |
|---|---|
| Si un dato no debe cambiar tras crearse, hazlo de solo lectura | Socio.Id, Prestamo.Libro, Prestamo.Socio, Prestamo.FechaPrestamo |
Si un dato solo debe cambiar mediante una operación concreta, restringe el set |
MaterialBibliotecario.Disponible (solo vía Prestar()/Devolver()), Prestamo.FechaDevolucion (solo vía RegistrarDevolucion()) |
| Si un dato necesita cumplir una regla al asignarse, usa una propiedad completa con validación | MaterialBibliotecario.Titulo |
Si un dato puede cambiar libremente sin ninguna restricción, un { get; set; } público es suficiente |
Socio.Nombre |
Errores Comunes y Consejos
- Dejar todas las propiedades como
{ get; set; }público "por si acaso": es la opción más cómoda a corto plazo, pero renuncia a toda la protección que ofrece el encapsulamiento; revisa, propiedad a propiedad, si de verdad necesita poder escribirse libremente desde fuera. - Olvidar inicializar un campo
privatecon propiedad completa: si defines_titulosin ningún valor por defecto ni asignación en el constructor, la propiedadTitulodevolveríanullhasta la primera asignación válida; asegúrate de que el constructor pase siempre por elset(asignandoTitulo = titulo;, no_titulo = titulo;directamente, para que la validación también se aplique en la construcción). - Confundir
private setconprivatea secas:private setsigue permitiendo la lectura pública (getsigue siendopublic); lo que se restringe es solo la escritura. Un campo completamenteprivateno sería accesible ni para leer desde fuera de la clase. - Usar
protectedcomo solución por defecto:protectedexpone el miembro a todas las clases herederas presentes y futuras, lo cual también es una forma de acoplamiento; resérvalo para cuando de verdad una clase base necesite compartir algo con sus herederas. - Consejo: al diseñar una clase nueva, empieza marcando todo como
privatey ve abriendo únicamente lo que el resto del programa necesite de verdad, en lugar de empezar con todopublicy restringir después; es mucho más fácil abrir acceso más adelante que cerrarlo sin romper código que ya dependía de él.
Ejercicios
-
Modifica la propiedad
DisponibledeMaterialBibliotecariopara que tenga ungetpúblico y unsetprivado (private set). Comprueba que, tras crear un objetoLibro, la línealibro1.Disponible = true;escrita fuera de la clase provoca un error de compilación, mientras quelibro1.Prestar()sigue funcionando con normalidad. -
Convierte la propiedad
TitulodeMaterialBibliotecarioen una propiedad completa con un campo privado_titulo, cuyosetlance unaArgumentExceptioncon el mensaje"El titulo no puede estar vacio."si el valor recibido es una cadena vacía o solo espacios (usastring.IsNullOrWhiteSpace). Prueba a asignar un título vacío y comprueba que se lanza la excepción. -
En la clase
Prestamo, declaraFechaDevolucioncomoDateTime?congetpúblico ysetprivado, y añade un métodovoid RegistrarDevolucion()que le asigneDateTime.Now. Crea unPrestamo, comprueba queFechaDevolucionempieza ennull, y después de llamar aRegistrarDevolucion(), comprueba que ya tiene un valor.
Soluciones
class MaterialBibliotecario
{
public string Titulo { get; set; }
public string Autor { get; set; }
public bool Disponible { get; private set; } = true;
public MaterialBibliotecario(string titulo, string autor)
{
Titulo = titulo;
Autor = autor;
}
public void Prestar()
{
if (Disponible)
{
Disponible = false;
}
}
}
// libro1.Disponible = true; // Error de compilacion: el set es private
private string _titulo;
public string Titulo
{
get { return _titulo; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("El titulo no puede estar vacio.");
}
_titulo = value;
}
}
Al ejecutar libro1.Titulo = "";, se lanza ArgumentException con el mensaje "El titulo no puede estar vacio.", ya que la cadena vacía cumple la condición de
string.IsNullOrWhiteSpace.
class Prestamo
{
public Libro Libro { get; }
public Socio Socio { get; }
public DateTime FechaPrestamo { get; }
public DateTime? FechaDevolucion { get; private set; }
public Prestamo(Libro libro, Socio socio)
{
Libro = libro;
Socio = socio;
FechaPrestamo = DateTime.Now;
FechaDevolucion = null;
}
public void RegistrarDevolucion()
{
FechaDevolucion = DateTime.Now;
}
}
Prestamo prestamo1 = new Prestamo(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"), new Socio(1, "Ana Martinez"));
Console.WriteLine(prestamo1.FechaDevolucion); // (vacio / null)
prestamo1.RegistrarDevolucion();
Console.WriteLine(prestamo1.FechaDevolucion); // fecha y hora actuales
Conclusión
En esta lección has protegido el estado interno del modelo de BiblioTech: los modificadores de
acceso (public, private, protected, internal), la restricción de escritura con
private set, las propiedades completas con validación en el set, y las propiedades de solo
lectura, aplicadas a MaterialBibliotecario.Disponible, MaterialBibliotecario.Titulo y
Prestamo.FechaDevolucion. Ya no es posible dejar un libro o un préstamo en un estado
incoherente saltándose la lógica de negocio: toda modificación pasa, obligatoriamente, por los
métodos pensados para ello.
Queda todavía un cabo suelto conceptual: MaterialBibliotecario se puede instanciar
directamente con new MaterialBibliotecario("...", "..."), aunque en la práctica no representa
ningún material real de BiblioTech (solo tiene sentido como base de Libro o Revista). En la
próxima lección, Abstracción, aprenderás a impedir esa instanciación directa convirtiendo
MaterialBibliotecario en una clase abstract, dejando claro en el propio código que su único
propósito es servir de base a otras clases.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
