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

  1. Qué es el encapsulamiento y por qué importa
  2. Modificadores de acceso: public, private, protected, internal
  3. El problema concreto: Disponible modificable libremente
  4. private set: exponer lectura, restringir escritura
  5. Propiedades con lógica de validación en el set
  6. Propiedades de solo lectura
  7. Aplicando encapsulamiento al resto del modelo: Socio y Prestamo
  8. El principio de mínima exposición

  1. 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.

  1. Modificadores de acceso: public, private, protected, internal

C# 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 jerarquia

protected 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.

  1. El problema concreto: Disponible modificable libremente

Así 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).

  1. private set: exponer lectura, restringir escritura

La 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 fuera

Con 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.

  1. Propiedades con lógica de validación en el set

private 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
Cuándo usarla El dato no necesita ninguna restricción especial Hay una regla de negocio que debe cumplirse siempre

  1. 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.

  1. Aplicando encapsulamiento al resto del modelo: Socio y Prestamo

Con 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();

  1. 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 private con propiedad completa: si defines _titulo sin ningún valor por defecto ni asignación en el constructor, la propiedad Titulo devolvería null hasta la primera asignación válida; asegúrate de que el constructor pase siempre por el set (asignando Titulo = titulo;, no _titulo = titulo; directamente, para que la validación también se aplique en la construcción).
  • Confundir private set con private a secas: private set sigue permitiendo la lectura pública (get sigue siendo public); lo que se restringe es solo la escritura. Un campo completamente private no sería accesible ni para leer desde fuera de la clase.
  • Usar protected como solución por defecto: protected expone 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 private y ve abriendo únicamente lo que el resto del programa necesite de verdad, en lugar de empezar con todo public y 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

  1. Modifica la propiedad Disponible de MaterialBibliotecario para que tenga un get público y un set privado (private set). Comprueba que, tras crear un objeto Libro, la línea libro1.Disponible = true; escrita fuera de la clase provoca un error de compilación, mientras que libro1.Prestar() sigue funcionando con normalidad.

  2. Convierte la propiedad Titulo de MaterialBibliotecario en una propiedad completa con un campo privado _titulo, cuyo set lance una ArgumentException con el mensaje "El titulo no puede estar vacio." si el valor recibido es una cadena vacía o solo espacios (usa string.IsNullOrWhiteSpace). Prueba a asignar un título vacío y comprueba que se lanza la excepción.

  3. En la clase Prestamo, declara FechaDevolucion como DateTime? con get público y set privado, y añade un método void RegistrarDevolucion() que le asigne DateTime.Now. Crea un Prestamo, comprueba que FechaDevolucion empieza en null, y después de llamar a RegistrarDevolucion(), 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#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados