Clase 087 — Visibilidad, encapsulación y contratos (public/private)

Parte 5 — Funciones y modularidad · ⏱️ Duración estimada: 90 min · Nivel: IntermedioClase construida — 10 implementaciones del núcleo verificadas contra casos.json.


🎯 Objetivo

Entender la encapsulación no como una regla de etiqueta («no toques los campos ajenos») sino como el mecanismo que hace confiable a un objeto. Un tipo encapsulado guarda su estado interno tras una pared —lo declara privado— y ofrece hacia fuera solo un puñado de operaciones con significado —el contrato público—. La consecuencia es poderosa: si el único modo de tocar el saldo es a través de depositar y retirar, entonces esos dos métodos son los únicos lugares donde el saldo puede volverse inválido, y basta escribirlos bien una vez para que el resto del programa no pueda romper nunca la regla saldo >= 0. La pared no está para ocultar por ocultar: está para reducir a un puñado de puntos los sitios donde algo puede salir mal.

La idea nace de David Parnas y su ocultamiento de información, y McConnell la desarrolla en Code Complete (cap. 6, «Working Classes»): una clase bien diseñada expone una interfaz que es una abstracción coherente y esconde tras ella todo lo que podría cambiar. Joshua Bloch la eleva a consejo de cabecera en Effective Java con el ítem «Minimize the accessibility of classes and members»: haz cada declaración tan inaccesible como el programa permita. La razón es de acoplamiento —cuanto menos expones, menos código ajeno depende de tus detalles internos, y más libre eres de cambiarlos—.

Esta clase practica el gesto fundacional: una Cuenta con un saldo privado y un método depositar que lo modifica de forma controlada. Verás que los diez lenguajes tienen opiniones muy distintas sobre cómo se dibuja esa pared: algunos la hacen cumplir con el compilador (Java, Rust, C#), otros la dejan a la palabra dada (Python, con su convención del guion bajo), y uno —Go— decide la visibilidad por algo tan simple como la mayúscula inicial del nombre.

🧩 Situación

Piensa en una cuenta bancaria representada con un campo saldo público, accesible desde cualquier parte del programa. Todo funciona hasta que, en algún módulo remoto escrito meses después, alguien hace cuenta.saldo -= comision sin comprobar nada, y una cuenta con dos euros queda en menos ocho. La regla del negocio —«el saldo nunca es negativo»— existía en la cabeza del programador original, pero no en el código: nada la protegía. Encapsular convierte esa regla en algo que la máquina custodia. Si saldo es privado y solo retirar puede disminuirlo, entonces retirar es el único guardián que debe comprobar if (monto <= saldo), y ningún módulo lejano puede saltárselo porque, sencillamente, no alcanza el campo. La invariante deja de ser una promesa verbal y pasa a ser una propiedad estructural del programa. Aquí ensayamos la base de esa idea: dos depósitos de n sobre un saldo que empieza en cero y solo se toca por métodos.

🧮 Modelo

Especificación y verificación en casos.json:

stdin esperado
50 saldo=100
0 saldo=0
30 saldo=60

📖 Definiciones y características

📐 Algoritmo (pseudocódigo neutral)

LEER n
cuenta <- nueva Cuenta()
cuenta.depositar(n) ; cuenta.depositar(n)
ESCRIBIR "saldo=" cuenta.saldo()

🌐 Implementaciones idiomáticas — el código a la vista

Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json. Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.

Python · python/main.py · python main.py

import sys


class Cuenta:
    def __init__(self):
        self._saldo = 0  # privado por convención

    def depositar(self, monto):
        self._saldo += monto

    def saldo(self):
        return self._saldo


n = int(sys.stdin.readline())
c = Cuenta()
c.depositar(n)
c.depositar(n)
print(f"saldo={c.saldo()}")

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

JavaScript · javascript/main.mjs · node main.mjs

import { readFileSync } from "node:fs";

class Cuenta {
  #saldo = 0; // campo privado real
  depositar(monto) {
    this.#saldo += monto;
  }
  saldo() {
    return this.#saldo;
  }
}

const n = parseInt(readFileSync(0, "utf8").trim(), 10);
const c = new Cuenta();
c.depositar(n);
c.depositar(n);
console.log(`saldo=${c.saldo()}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

TypeScript · typescript/main.ts · pnpm exec tsx main.ts

import { readFileSync } from "node:fs";

class Cuenta {
  private saldoInterno = 0;
  depositar(monto: number): void {
    this.saldoInterno += monto;
  }
  saldo(): number {
    return this.saldoInterno;
  }
}

const n: number = parseInt(readFileSync(0, "utf8").trim(), 10);
const c = new Cuenta();
c.depositar(n);
c.depositar(n);
console.log(`saldo=${c.saldo()}`);

🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript

Java · java/Main.java · java Main.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static class Cuenta {
        private long saldo = 0;

        void depositar(long monto) {
            saldo += monto;
        }

        long saldo() {
            return saldo;
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        long n = Long.parseLong(br.readLine().trim());
        Cuenta c = new Cuenta();
        c.depositar(n);
        c.depositar(n);
        System.out.println("saldo=" + c.saldo());
    }
}

🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure

C# · csharp/Program.cs · dotnet run

using System;

// Las sentencias top-level van antes de la declaración del tipo.
long n = long.Parse(Console.In.ReadToEnd().Trim());
var c = new Cuenta();
c.Depositar(n);
c.Depositar(n);
Console.WriteLine($"saldo={c.Saldo()}");

class Cuenta {
    private long saldo = 0;
    public void Depositar(long monto) => saldo += monto;
    public long Saldo() => saldo;
}

🧬 El mismo programa en la familia .NET: F# · VB.NET

Go · go/main.go · go run main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

// saldo en minúscula: privado del paquete.
type cuenta struct {
    saldo int64
}

func (c *cuenta) depositar(monto int64) {
    c.saldo += monto
}

func (c *cuenta) obtenerSaldo() int64 {
    return c.saldo
}

func main() {
    line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
    n, _ := strconv.ParseInt(strings.TrimSpace(line), 10, 64)
    c := &cuenta{}
    c.depositar(n)
    c.depositar(n)
    fmt.Printf("saldo=%d\n", c.obtenerSaldo())
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

Rust · rust/main.rs · rustc main.rs -o main && ./main

use std::io::Read;

struct Cuenta {
    saldo: i64, // privado fuera del módulo
}

impl Cuenta {
    fn nueva() -> Self {
        Cuenta { saldo: 0 }
    }
    fn depositar(&mut self, monto: i64) {
        self.saldo += monto;
    }
    fn saldo(&self) -> i64 {
        self.saldo
    }
}

fn main() {
    let mut s = String::new();
    std::io::stdin().read_to_string(&mut s).unwrap();
    let n: i64 = s.trim().parse().unwrap();
    let mut c = Cuenta::nueva();
    c.depositar(n);
    c.depositar(n);
    println!("saldo={}", c.saldo());
}

🧬 El mismo programa en la familia Sistemas: Zig · Nim · D

C · c/main.c · cc main.c -o main && ./main

#include <stdio.h>

/* C no tiene 'private'; se usa una struct y funciones por convención. */
struct Cuenta {
    long saldo;
};

void depositar(struct Cuenta *c, long monto) {
    c->saldo += monto;
}

int main(void) {
    long n;
    if (scanf("%ld", &n) != 1) return 1;
    struct Cuenta c = {0};
    depositar(&c, n);
    depositar(&c, n);
    printf("saldo=%ld\n", c.saldo);
    return 0;
}

🧬 El mismo programa en la familia C / llaves: C++ · Objective-C

SQL · sql/main.sql · sqlite3 :memory: < main.sql

-- SQL encapsula con vistas/permisos; aquí el cálculo va en la consulta.
WITH montos(n) AS (VALUES (50), (0), (30))
SELECT printf('saldo=%d', n * 2) AS resultado FROM montos;

🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog

PHP · php/main.php · php main.php

<?php
class Cuenta {
    private $saldo = 0;

    public function depositar($monto) {
        $this->saldo += $monto;
    }

    public function saldo() {
        return $this->saldo;
    }
}

$n = (int) trim(fgets(STDIN));
$c = new Cuenta();
$c->depositar($n);
$c->depositar($n);
echo "saldo=" . $c->saldo() . "\n";

🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R

SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.

🔬 Ejemplo trabajado — del stdin a la salida

Sigamos el primer caso de casos.json (stdin = "50", esperado = "saldo=100") por tres lenguajes que dibujan la pared de la privacidad de tres formas distintas.

Python. La clase Cuenta guarda su estado en self._saldo, y el guion bajo inicial es la convención de Python para «esto es privado». Pero es solo eso, una convención: nada en el lenguaje impide que un módulo externo escriba c._saldo = -999. La comunidad respeta el guion bajo del mismo modo que se respeta una señal de «no pasar». El recorrido del caso: n = int(sys.stdin.readline()) lee "50\n" y produce n=50; c.depositar(n) ejecuta self._saldo += 50 dejando el saldo en 50; la segunda llamada lo lleva a 100; c.saldo() devuelve ese 100 y print emite saldo=100. Ramalho, en Fluent Python, matiza que el doble guion bajo (__saldo) activa además el name mangling, que dificulta —no prohíbe— el acceso externo.

Java. Aquí la pared es de hormigón: private long saldo significa que el compilador rechazará cualquier acceso a saldo desde fuera de Cuenta. Con n=50, la primera c.depositar(n) deja saldo en 50, la segunda en 100, y c.saldo() lo devuelve para imprimir saldo=100. Si alguien escribiera c.saldo = -999 en otra clase, el programa no compilaría. Esta es la diferencia clave con Python: en Java la invariante no depende de la buena voluntad de los demás programadores, sino de una garantía que la máquina hace cumplir antes de ejecutar. Es exactamente lo que Bloch recomienda al minimizar la accesibilidad.

Go. Go no tiene las palabras public ni private: la visibilidad se decide por la mayúscula inicial del identificador. El campo saldo empieza en minúscula, así que es privado del paquete; si se llamara Saldo, sería exportado. Con n=50, c.depositar(n) suma 50 dos veces sobre el campo del struct cuenta, y c.obtenerSaldo() devuelve 100 para que fmt.Printf("saldo=%d\n", ...) imprima saldo=100. Nótese que la frontera de privacidad en Go es el paquete, no el tipo: dentro del mismo paquete, cualquier función alcanza saldo; la pared solo se levanta ante código de otros paquetes. Los tres llegan a saldo=100, pero custodian la invariante con tres rigores distintos: la palabra dada (Python), el compilador por tipo (Java) y el compilador por paquete y mayúscula (Go).

🔬 Comparación

Lenguaje Cómo marca lo privado ¿Lo hace cumplir?
Python Convención _saldo; __saldo activa name mangling No, es acuerdo social
JavaScript Campos con #saldo (privados reales de clase) Sí, error en tiempo de ejecución
TypeScript private saldoInterno (chequeo del compilador) En compilación; en runtime es accesible
Java private / protected / public / package-private Sí, el compilador lo verifica
C# private / protected / internal / public Sí, el compilador lo verifica
Go Mayúscula inicial = exportado; minúscula = privado del paquete Sí, por paquete
Rust Privado por defecto; pub expone; frontera es el módulo Sí, el compilador lo verifica
C No existe private; struct con campos accesibles, convención No, disciplina del programador
SQL Vistas y permisos (GRANT) restringen el acceso a datos Sí, el motor lo aplica
PHP private / protected / public en la clase Sí, error en tiempo de ejecución

La síntesis la ofrece Bloch en Effective Java: minimizar la accesibilidad es minimizar el acoplamiento, y con ello maximizar la libertad de cambiar el interior. Los lenguajes se ordenan en un espectro de rigor. En un extremo, Python y C confían en la convención —eficaces mientras el equipo la respete—. En el otro, Java, C#, Rust y Go la hacen cumplir con el compilador, cada uno con su frontera: Java y C# por tipo, Rust por módulo, Go por paquete. En medio, TypeScript ofrece una privacidad que existe para el compilador pero se evapora en el JavaScript emitido, mientras que los #campos de JavaScript sí son inviolables en ejecución. Elegir bien lo privado, dice McConnell, es tan importante como elegir bien lo público: la pared no protege a la máquina, protege al humano de sí mismo dentro de seis meses.

🧬 El concepto en la familia

En Ruby los atributos de instancia son privados por naturaleza —solo el objeto los ve— y se exponen deliberadamente con attr_reader, attr_writer o métodos escritos a mano; la privacidad es el punto de partida, no una anotación. En Go, como vimos, no hay palabras clave: la visibilidad es una propiedad ortográfica del identificador y su alcance es el paquete entero, un diseño deliberadamente minimalista. En Swift hay una escala fina de niveles —private, fileprivate, internal, public, open— que gradúa cuánto sale la visibilidad más allá del propio tipo, el archivo o el módulo. Reconocer dónde pone cada familia la frontera —tipo, archivo, módulo, paquete— permite predecir qué se puede tocar desde dónde sin leer el manual.

✅ Prueba común

Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:

python scripts/verificar_equivalencia.py 087

🧪 Reto de transferencia

Detalle en reto.md.

⚠️ Errores comunes

❓ Preguntas frecuentes

🔗 Referencias

Libros de la parte:

Libros de los lenguajes del núcleo:


⏮️ Clase 086 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 088 ⏭️