¿Sufro de uso excesivo de encapsulación?

11

He notado algo en mi código en varios proyectos que me parece un olor a código y algo malo que hacer, pero no puedo lidiar con eso.

Al intentar escribir "código limpio", tiendo a usar en exceso los métodos privados para que mi código sea más fácil de leer. El problema es que el código es realmente más limpio, pero también es más difícil de probar (sí, sé que puedo probar métodos privados ...) y, en general, me parece un mal hábito.

Aquí hay un ejemplo de una clase que lee algunos datos de un archivo .csv y devuelve un grupo de clientes (otro objeto con varios campos y atributos).

public class GroupOfCustomersImporter {
    //... Call fields ....
    public GroupOfCustomersImporter(String filePath) {
        this.filePath = filePath;
        customers = new HashSet<Customer>();
        createCSVReader();
        read();
        constructTTRP_Instance();
    }

    private void createCSVReader() {
        //....
    }

    private void read() {
        //.... Reades the file and initializes the class attributes
    }

    private void readFirstLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readSecondLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readCustomerLine(String[] inputLine) { 
        //.... Method used by the read() method
    }

    private void constructGroupOfCustomers() {
        //this.groupOfCustomers = new GroupOfCustomers(**attributes of the class**);
    }

    public GroupOfCustomers getConstructedGroupOfCustomers() {
        return this.GroupOfCustomers;
    }
}

Como puede ver, la clase solo tiene un constructor que llama a algunos métodos privados para hacer el trabajo, sé que no es una buena práctica en general, pero prefiero encapsular toda la funcionalidad en la clase en lugar de hacer públicos los métodos, en cuyo caso un cliente debe trabajar de esta manera:

GroupOfCustomersImporter importer = new GroupOfCustomersImporter(filepath)
importer.createCSVReader();
read();
GroupOfCustomer group = constructGoupOfCustomerInstance();

Prefiero esto porque no quiero poner líneas de código inútiles en el código del lado del cliente molestando a la clase del cliente con detalles de implementación.

Entonces, ¿es realmente un mal hábito? En caso afirmativo, ¿cómo puedo evitarlo? Tenga en cuenta que lo anterior es solo un ejemplo simple. Imagina que ocurre la misma situación en algo un poco más complejo.

Florent Tselai
fuente

Respuestas:

17

Creo que en realidad estás en el camino correcto en cuanto a querer ocultar los detalles de implementación del cliente. Desea diseñar su clase para que la interfaz que ve el cliente sea la API más simple y concisa que pueda encontrar. No solo el cliente no se "molestará" con los detalles de implementación, sino que también podrá refactorizar el código subyacente sin preocuparse de que las personas que llaman de ese código deban modificarse. Reducir el acoplamiento entre diferentes módulos tiene algunos beneficios reales y definitivamente debe esforzarse por lograrlo.

Entonces, si sigue el consejo que acabo de proporcionar, terminará con algo que ya notó en su código, una gran cantidad de lógica que permanece oculta detrás de una interfaz pública y no es fácilmente accesible.

Idealmente, debería ser capaz de probar su clase solo contra su interfaz pública y, si tiene dependencias externas, es posible que deba introducir objetos falsos / simulados / trozos para aislar el código que se está probando.

Sin embargo, si hace esto y todavía siente que no puede evaluar fácilmente cada parte de la clase, entonces es muy probable que su única clase esté haciendo demasiado. En el espíritu del principio SRP , puede seguir lo que Michael Feathers llama "Patrón de clase de Sprout" y extraer una parte de su clase original en una nueva clase.

En su ejemplo, tiene una clase de importador que también es responsable de leer un archivo de texto. Una de sus opciones es extraer un fragmento de código de lectura de archivos en una clase InputFileParser separada. Ahora todas esas funciones privadas se vuelven públicas y, por lo tanto, fácilmente comprobables. Al mismo tiempo, la clase del analizador no es algo que sea visible para clientes externos (márquelo como "interno", no publique archivos de encabezado o simplemente no lo anuncie como parte de su API), ya que seguirán utilizando el original importador cuya interfaz seguirá siendo corta y dulce.

DXM
fuente
1

Como alternativa a poner muchas llamadas a métodos en el constructor de su clase, lo cual estoy de acuerdo en evitar en general, puede crear un método de fábrica para manejar todas las cosas de inicialización adicionales que no desea molestar al cliente lado con. Esto significaría que expondrías un método para que se parezca a tu constructGroupOfCustomers()método como público, y lo uses como un método estático de tu clase:

GroupOfCustomersImporter importer = 
    new GroupOfCustomersImporter.CreateInstance();

o como método de una clase de fábrica separada quizás:

ImporterFactory factory = new ImporterFactory();
GroupOfCustomersImporter importer = factory.CreateGroupOfCustomersImporter();

Esas son las primeras dos opciones pensadas en mi cabeza.

También vale la pena considerar que tus instintos están tratando de decirte algo. Cuando tu instinto te dice que el código está empezando a oler, probablemente huele, ¡y es mejor hacer algo al respecto antes de que apesta! Claro que en la superficie puede no haber nada intrínsecamente incorrecto con su código per se, sin embargo, una revisión rápida y un refactor le ayudarán a resolver sus pensamientos sobre el problema para que pueda pasar a otras cosas con confianza sin preocuparse si está creando un potencial castillo de naipes. En este caso particular, delegar algunas de las responsabilidades del constructor a, o ser llamado por, una fábrica asegura que pueda ejercer un mayor grado de control de la instanciación de su clase y de sus posibles futuros descendientes.

S.Robins
fuente
1

Añadiendo mi propia respuesta, ya que terminó demasiado tiempo para un comentario.

Tiene toda la razón en que la encapsulación y las pruebas son bastante contrarias: si oculta casi todo, es difícil de probar.

Sin embargo, puede hacer que el ejemplo sea más verificable al proporcionar una secuencia en lugar de un nombre de archivo; de esa manera, simplemente proporciona un csv conocido de la memoria o un archivo si lo desea. También hará que su clase sea más sólida cuando necesite agregar soporte http;)

Además, considere usar lazy-init:

public class Csv
{
  private CustomerList list;

  public CustomerList get()
  {
    if(list == null)
        load();
     return list;
  }
}
Max
fuente
1

El constructor no debe hacer demasiado, excepto inicializar algunas variables o el estado del objeto. Hacer demasiado en el constructor puede generar excepciones de tiempo de ejecución. Trataría de evitar que el cliente haga algo como esto:

MyClass a;
try {
   a = new MyClass();
} catch (MyException e) {
   //do something
}

En lugar de hacer:

MyClass a = new MyClass(); // Or a factory method
try {
   a.doSomething();
} catch (MyException e) {
   //do something
}
trotuar
fuente