Clases, Interfaces y Genéricos

Interfaces

Es una colección de métodos abstractos y propiedades constantes. En las interfaces se especifica qué se debe hacer pero no su implementación. Serán las clases que implementen estas interfaces las que describen la lógica del comportamiento de los métodos. Las clases que hereden de la interfaz solo podrán hacerlo de ella.

Un momento muy útil en el que declarar una interfaz, puede ser cuando vemos que dos clases tienen el mismo contrato, por ejemplo, tenemos una clase coche y una clase moto, que implementan los mismos métodos de formas distintas.

Su utilidad se encuentra cuando buscamos muchas implementaciones, por lo que si la idea es solo generar una implementación, deberíamos basarnos en una sola clase. El hecho de estar obligando a pasar primero por la interfaz a la hora de entender el código, se hace tedioso cuando el objetivo es sólo entender el comportamiento de una clase.

Otro aspecto a cuidar, es el nombre que reciba la interfaz. Incluir en los nombres de interfaces el prefijo “I” o en el de las implementaciones el sufijo “Impl”, solo muestra un mal nombre que no describe como funciona el sistema. Se debe buscar nombres que cuenten una historia, por lo que si una interfaz se llama “Encryptor”, su implementación debería usar ese nombre de base, pero añadiendo que implementa, como puede ser “SimpleEncryptor”.

interface Encryptor{
        String Encrypt(String text);
    }
    public class SimpleEncryptor implements Encryptor{
        @Override
        public String Encrypt(String text) {
            return text.toUpperCase();
        }
    }
    public class ComplexEncryptor implements Encryptor{
        @Override
        public String Encrypt(String text) {
            return text.toUpperCase().concat("_SECRET");
        }
    }

Clases abstractas

Otra opción que tenemos, sería usar una clase abstracta. A diferencia de la interfaz, ésta si define una base sobre la que trabajar las implementaciones, por lo que la idea de usarla es mejorar dicha clase padre, ya sea añadiendo comportamiento o especificando uno nuevo.

Tanto como con las interfaces, como con las clases abstractas los métodos suelen definirse como “protected” para que solo las implementaciones puedan usarlo.

public abstract class Encryptor{
        String Encrypt(String text){
            return text.toUpperCase();
        }
    }
    public class EncryptorWithOriginalFunctionality extends Encryptor {
        @Override
        public String Encrypt(String text){
           String original = super.Encrypt(text);
            return original.concat("_SECRET");
        }
    }
    public class EncryptorWithoutOriginalFunctionality extends Encryptor {
        @Override
        public String Encrypt(String text){
            return text.concat("_SECRET");
        }
    }

Genéricos

Cuando queremos aprovechar un método para que opere con distintos tipos de datos, como puede ser una cadena de texto o un entero, surge la idea de utilizar algo más general como puede la clase Object, ya que dicha ambos tipos de datos son de dicha clase. El problema es que si tenemos una colección que contiene diferentes tipos de datos, no obligamos a usar casteo, algo que a la hora de error no nos detalla mucho sobre el porque ocurre el fallo más allá de no poder realizar un casteo.

public class Cache {
        List<Object> objects = new ArrayList<>();

        void addObjects() {
            objects.add("a");
            objects.add(1);
        }

        String getObject() {
            return (String) objects.get(0);
        }
    }

    @Test
    void simple_test() {
        Cache cache = new Cache();
        cache.addObjects();
        assertThat(cache.getObject()).isEqualTo("a");
    }
/*************************************************************************/
public class Cache {
        List<Object> objects = new ArrayList<>();

        void addObjects() {
            objects.add("a");
            objects.add(1);
        }

        String getObject() {
            return (String) objects.get(1);
        }
    }

    @Test
    void simple_test() {
        Cache cache = new Cache();
        cache.addObjects();
        assertThat(cache.getObject()).isEqualTo("a");
//no puede realizar el casteo
    }

Otra solución sería decirle a la clase que acepte cualquier tipo de dato, por lo que el programa si ejecutaría en dicho caso, y comprobaríamos el fallo en tiempo de compilación.

public class Cache<T> {
        List<T> objects = new ArrayList<>();

        void addObjects(T object) {
            objects.add(object);
        }

        T getObject(int i) {
            return objects.get(i);
        }
    }

    @Test
    void simple_test() {
        Cache cache = new Cache();
        cache.addObjects("a");
        assertThat(cache.getObject(0)).isEqualTo("a");
    }
/********************************************************************/
public class Cache<T> {
        List<T> objects = new ArrayList<>();

        void addObjects(T object) {
            objects.add(object);
        }

        T getObject(int i) {
            return objects.get(i);
        }
//no declaramos ningún casteo
    }

    @Test
    void simple_test() {
        Cache cache = new Cache();
        cache.addObjects(1);
        assertThat(cache.getObject(0)).isEqualTo("a");
//como acepta el tipo, el error ocurre por no ser lo mismo que esperamos
    }
Creado con Hugo
Tema Stack diseñado por Jimmy