JavaRush /Blog Java /Random-ES /Diseño de clases e interfaces (Traducción del artículo)
fatesha
Nivel 22

Diseño de clases e interfaces (Traducción del artículo)

Publicado en el grupo Random-ES
Diseño de Clases e Interfaces (Traducción del artículo) - 1

Contenido

  1. Introducción
  2. Interfaces
  3. Marcadores de interfaz
  4. Interfaces funcionales, métodos estáticos y métodos predeterminados.
  5. clases abstractas
  6. Clases inmutables (permanentes)
  7. clases anónimas
  8. Visibilidad
  9. Herencia
  10. herencia múltiple
  11. Herencia y composición
  12. Encapsulación
  13. Clases y métodos finales.
  14. Que sigue
  15. Descargar código fuente

1. INTRODUCCIÓN

No importa qué lenguaje de programación utilice (y Java no es una excepción), seguir buenos principios de diseño es la clave para escribir código limpio, comprensible y verificable; y también crearlo para que sea duradero y respalde fácilmente la resolución de problemas. En esta parte del tutorial, analizaremos los componentes fundamentales que proporciona el lenguaje Java e introduciremos un par de principios de diseño en un esfuerzo por ayudarlo a tomar mejores decisiones de diseño. Más específicamente, discutiremos interfaces e interfaces que usan métodos predeterminados (una nueva característica en Java 8), clases abstractas y finales, clases inmutables, herencia, composición y revisaremos las reglas de visibilidad (o accesibilidad) que mencionamos brevemente en Lección parte 1 "Cómo crear y destruir objetos" .

2. INTERFACES

En la programación orientada a objetos , el concepto de interfaces constituye la base para el desarrollo de contratos . En pocas palabras, las interfaces definen un conjunto de métodos (contratos) y cada clase que requiere soporte para esa interfaz específica debe proporcionar una implementación de esos métodos: una idea bastante simple pero poderosa. Muchos lenguajes de programación tienen interfaces de una forma u otra, pero Java en particular proporciona soporte de lenguaje para esto. Echemos un vistazo a una definición de interfaz simple en Java.
package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
void performAction();
}
En el fragmento anterior, la interfaz que llamamos SimpleInterfacedeclara solo un método llamado performAction. La principal diferencia entre interfaces y clases es que las interfaces describen cuál debería ser el contacto (declaran un método), pero no proporcionan su implementación. Sin embargo, las interfaces en Java pueden ser más complejas: pueden incluir interfaces anidadas, clases, recuentos, anotaciones y constantes. Por ejemplo:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
}
En este ejemplo más complejo, hay varias restricciones que las interfaces imponen incondicionalmente sobre las construcciones anidadas y las declaraciones de métodos, y el compilador de Java las aplica. En primer lugar, incluso si no se declara explícitamente, cada declaración de método en una interfaz es pública (y sólo puede ser pública). Por tanto, las siguientes declaraciones de métodos son equivalentes:
public void performAction();
void performAction();
Vale la pena mencionar que cada método en una interfaz se declara implícitamente abstract , e incluso estas declaraciones de métodos son equivalentes:
public abstract void performAction();
public void performAction();
void performAction();
En cuanto a los campos constantes declarados, además de ser públicos , también son implícitamente estáticos y están marcados como finales . Por tanto, las siguientes declaraciones también son equivalentes:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
Finalmente, las clases, interfaces o recuentos anidados, además de ser públicos , también se declaran implícitamente estáticos . Por ejemplo, estas declaraciones también equivalen a:
class InnerClass {
}

static class InnerClass {
}
El estilo que elija es una preferencia personal, pero conocer estas propiedades simples de las interfaces puede evitar tener que escribir innecesariamente.

3. Marcador de interfaz

Una interfaz de marcador es un tipo especial de interfaz que no tiene métodos ni otras construcciones anidadas. Cómo lo define la biblioteca Java:
public interface Cloneable {
}
Los marcadores de interfaz no son contratos per se, pero son una técnica algo útil para "adjuntar" o "asociar" algún rasgo específico con una clase. Por ejemplo, con respecto a Cloneable , la clase está marcada como clonable, pero la forma en que esto puede o debe implementarse no forma parte de la interfaz. Otro ejemplo muy famoso y ampliamente utilizado de marcador de interfaz es Serializable:
public interface Serializable {
}
Esta interfaz marca la clase como adecuada para la serialización y deserialización y, nuevamente, no especifica cómo se puede o debe implementar esto. Los marcadores de interfaz tienen su lugar en la programación orientada a objetos, aunque no satisfacen el propósito principal de que una interfaz sea un contrato. 

4. INTERFACES FUNCIONALES, MÉTODOS POR DEFECTO Y MÉTODOS ESTÁTICOS

Desde los lanzamientos de Java 8, las interfaces han adquirido algunas características nuevas muy interesantes: métodos estáticos, métodos predeterminados y conversión automática desde lambdas (interfaces funcionales). En la sección de interfaces, enfatizamos el hecho de que las interfaces en Java solo pueden declarar métodos, pero no proporcionan su implementación. Con un método predeterminado, las cosas son diferentes: una interfaz puede marcar un método con la palabra clave predeterminada y proporcionarle una implementación. Por ejemplo:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}
Al estar en el nivel de instancia, los métodos predeterminados podrían ser anulados por cada implementación de interfaz, pero las interfaces ahora también pueden incluir métodos estáticos , por ejemplo: paquete com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}
Se podría decir que proporcionar la implementación en la interfaz anula todo el propósito de la programación del contrato. Pero hay muchas razones por las que estas características se introdujeron en el lenguaje Java y no importa cuán útiles o confusas sean, están ahí para usted y su uso. Las interfaces funcionales son una historia diferente y han demostrado ser adiciones muy útiles al lenguaje. Básicamente, una interfaz funcional es una interfaz con un solo método abstracto declarado en ella. RunnableLa interfaz de biblioteca estándar es un muy buen ejemplo de este concepto.
@FunctionalInterface
public interface Runnable {
    void run();
}
El compilador de Java trata las interfaces funcionales de manera diferente y puede convertir una función lambda en una implementación de interfaz funcional cuando tenga sentido. Consideremos la siguiente descripción de función: 
public void runMe( final Runnable r ) {
    r.run();
}
Para llamar a esta función en Java 7 y versiones anteriores, se debe proporcionar una implementación de la interfaz Runnable(por ejemplo, usando clases anónimas), pero en Java 8 es suficiente proporcionar una implementación del método run() usando la sintaxis lambda:
runMe( () -> System.out.println( "Run!" ) );
Además, la anotación @FunctionalInterface (las anotaciones se cubrirán en detalle en la Parte 5 del tutorial) sugiere que el compilador puede verificar si una interfaz contiene solo un método abstracto, por lo que cualquier cambio realizado en la interfaz en el futuro no violará esta suposición. .

5. CLASES ABSTRACTAS

Otro concepto interesante soportado por el lenguaje Java es el concepto de clases abstractas. Las clases abstractas son algo similares a las interfaces en Java 7 y están muy cerca de la interfaz del método predeterminado en Java 8. A diferencia de las clases regulares, no se puede crear una instancia de una clase abstracta, pero se puede crear una subclase (consulte la sección Herencia para obtener más detalles). Más importante aún, las clases abstractas pueden contener métodos abstractos: un tipo especial de método sin implementación, como una interfaz. Por ejemplo:
package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
}
En este ejemplo, la clase SimpleAbstractClassse declara abstracta y contiene un método abstracto declarado. Las clases abstractas son muy útiles; la mayoría o incluso algunas partes de los detalles de implementación se pueden compartir entre muchas subclases. Sea como fuere, aún dejan la puerta entreabierta y permiten personalizar el comportamiento inherente a cada una de las subclases mediante métodos abstractos. Vale la pena mencionar que, a diferencia de las interfaces, que solo pueden contener declaraciones públicas , las clases abstractas pueden utilizar todo el poder de las reglas de accesibilidad para controlar la visibilidad de un método abstracto.

6. CLASES INMEDIATAS

La inmutabilidad es cada vez más importante en el desarrollo de software hoy en día. El auge de los sistemas multinúcleo ha planteado muchos problemas relacionados con el intercambio de datos y el paralelismo. Pero definitivamente ha surgido un problema: tener poco (o incluso ningún) estado mutable conduce a una mejor extensibilidad (escalabilidad) y un razonamiento más fácil sobre el sistema. Desafortunadamente, el lenguaje Java no proporciona un soporte decente para la inmutabilidad de clases. Sin embargo, utilizando una combinación de técnicas, es posible diseñar clases que sean inmutables. En primer lugar, todos los campos de la clase deben ser finales (marcados como finales ). Éste es un buen comienzo, pero no es garantía. 
package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection<String> collectionOfString;
}
En segundo lugar, asegúrese de una inicialización adecuada: si un campo es una referencia a una colección o matriz, no asigne esos campos directamente desde los argumentos del constructor; en su lugar, haga copias. Esto asegurará que el estado de la colección o matriz no se modifique fuera de ella.
public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection<String> collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}
Y finalmente, garantizar el acceso adecuado (captadores). Para las colecciones, la inmutabilidad debe proporcionarse como un contenedor  Collections.unmodifiableXxx: con las matrices, la única forma de proporcionar una verdadera inmutabilidad es proporcionar una copia en lugar de devolver una referencia a la matriz. Puede que esto no sea aceptable desde un punto de vista práctico, ya que depende mucho del tamaño de la matriz y puede ejercer una presión enorme sobre el recolector de basura.
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
Incluso este pequeño ejemplo da una buena idea de que la inmutabilidad aún no es un ciudadano de primera clase en Java. Las cosas pueden complicarse si una clase inmutable tiene un campo que hace referencia a un objeto de otra clase. Esas clases también deberían ser inmutables, pero no hay forma de garantizarlo. Existen varios analizadores de código fuente Java decentes, como FindBugs y PMD, que pueden ser de gran ayuda al verificar su código y señalar fallas comunes de programación Java. Estas herramientas son grandes amigas de cualquier desarrollador de Java.

7. CLASES ANÓNIMAS

En la era anterior a Java 8, las clases anónimas eran la única forma de garantizar que las clases se definieran sobre la marcha y se crearan instancias de inmediato. El propósito de las clases anónimas era reducir el texto repetitivo y proporcionar una forma breve y sencilla de representar las clases como un registro. Echemos un vistazo a la forma antigua y típica de generar un nuevo hilo en Java:
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}
En este ejemplo, la implementación de Runnablela interfaz se proporciona inmediatamente como una clase anónima. Aunque existen algunas limitaciones asociadas con las clases anónimas, las principales desventajas de usarlas son la sintaxis de construcción muy detallada a la que Java como lenguaje obliga. Incluso una clase anónima que no hace nada requiere al menos 5 líneas de código cada vez que se escribe.
new Runnable() {
   @Override
   public void run() {
   }
}
Afortunadamente, con Java 8, lambda y las interfaces funcionales, todos estos estereotipos pronto desaparecerán y finalmente escribir código Java parecerá realmente conciso.
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

8. VISIBILIDAD

Ya hablamos un poco sobre las reglas de visibilidad y accesibilidad en Java en la Parte 1 del tutorial. En esta parte, volveremos a abordar este tema, pero en el contexto de la subclasificación. Diseño de Clases e Interfaces (Traducción del artículo) - 2La visibilidad en diferentes niveles permite o impide que las clases vean otras clases o interfaces (por ejemplo, si están en paquetes diferentes o anidadas entre sí) o que las subclases vean y accedan a los métodos, constructores y campos de sus padres. En la siguiente sección, herencia, veremos esto en acción.

9. HERENCIA

La herencia es uno de los conceptos clave de la programación orientada a objetos y sirve como base para construir una clase de relaciones. Combinada con reglas de visibilidad y accesibilidad, la herencia permite diseñar clases en una jerarquía que se puede ampliar y mantener. A nivel conceptual, la herencia en Java se implementa mediante subclases y la palabra clave extends , junto con la clase principal. Una subclase hereda todos los elementos públicos y protegidos de la clase principal. Además, una subclase hereda los elementos privados del paquete de su clase principal si ambas (subclase y clase) están en el mismo paquete. Dicho esto, es muy importante, sin importar lo que intentes diseñar, ceñirte al conjunto mínimo de métodos que una clase expone públicamente o a sus subclases. Por ejemplo, veamos la clase Parenty su subclase Childpara demostrar la diferencia en los niveles de visibilidad y sus efectos.
package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}
La herencia es un tema muy amplio en sí mismo, con muchos detalles específicos de Java. Sin embargo, existen algunas reglas que son fáciles de seguir y pueden ser de gran ayuda para mantener la brevedad de la jerarquía de clases. En Java, cada subclase puede anular cualquier método heredado de su padre a menos que haya sido declarado final. Sin embargo, no existe una sintaxis o palabra clave especial para marcar un método como anulado, lo que puede generar confusión. Es por eso que se introdujo la anotación @Override : siempre que su objetivo sea anular un método heredado, utilice la anotación @Override para indicarlo de manera sucinta. Otro dilema al que se enfrentan constantemente los desarrolladores de Java en el diseño es la construcción de jerarquías de clases (con clases concretas o abstractas) frente a la implementación de interfaces. Recomendamos encarecidamente favorecer las interfaces sobre las clases o clases abstractas siempre que sea posible. Las interfaces son más ligeras, más fáciles de probar y mantener y también minimizan los efectos secundarios de los cambios de implementación. Muchas técnicas de programación avanzadas, como la creación de clases proxy en la biblioteca estándar de Java, dependen en gran medida de las interfaces.

10. HERENCIA MÚLTIPLE

A diferencia de C++ y algunos otros lenguajes, Java no admite herencia múltiple: en Java, cada clase puede tener sólo un padre directo (con la clase Objecten la parte superior de la jerarquía). Sin embargo, una clase puede implementar múltiples interfaces y, por lo tanto, el apilamiento de interfaces es la única forma de lograr (o simular) herencia múltiple en Java.
package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
La implementación de múltiples interfaces es realmente bastante poderosa, pero a menudo la necesidad de usar la implementación una y otra vez conduce a una jerarquía de clases profunda como una forma de superar la falta de soporte de Java para la herencia múltiple. 
public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
Y así sucesivamente... El reciente lanzamiento de Java 8 soluciona de alguna manera el problema con la inyección del método predeterminado. Debido a los métodos predeterminados, las interfaces en realidad proporcionan no sólo un contrato, sino también una implementación. Por lo tanto, las clases que implementan estas interfaces también heredarán automáticamente estos métodos implementados. Por ejemplo:
package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
Tenga en cuenta que la herencia múltiple es una herramienta muy poderosa, pero al mismo tiempo peligrosa. El conocido problema del Diamante de la Muerte se cita a menudo como un defecto importante en la implementación de la herencia múltiple, lo que obliga a los desarrolladores a diseñar jerarquías de clases con mucho cuidado. Desafortunadamente, las interfaces de Java 8 con métodos predeterminados también son víctimas de estos defectos.
interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
}
Por ejemplo, el siguiente fragmento de código no se podrá compilar:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
En este punto, es justo decir que Java como lenguaje siempre ha tratado de evitar los casos extremos de la programación orientada a objetos, pero a medida que el lenguaje evoluciona, algunos de esos casos han comenzado a aparecer repentinamente. 

11. HERENCIA Y COMPOSICIÓN

Afortunadamente, la herencia no es la única forma de diseñar tu clase. Otra alternativa que muchos desarrolladores creen que es mucho mejor que la herencia es la composición. La idea es muy simple: en lugar de crear una jerarquía de clases, estas deben estar compuestas por otras clases. Veamos este ejemplo:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
La clase Vehicleconsta de un motor y ruedas (además de muchas otras piezas que se dejan de lado por simplicidad). Sin embargo, se puede decir que una clase Vehicletambién es un motor, por lo que se puede diseñar mediante herencia. 
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
¿Qué solución de diseño sería la correcta? Los lineamientos básicos generales se conocen como principios IS-A (es) y HAS-A (contiene). IS-A es una relación de herencia: una subclase también satisface la especificación de clase de la clase principal y una variación de la clase principal (subclase) extiende su padre. Si desea saber si una entidad extiende a otra, haga una prueba de coincidencia - IS -A (es).") Por lo tanto, HAS-A es una relación de composición: una clase posee (o contiene) un objeto que En la mayoría de los casos, el principio HAS-A funciona mejor que IS-A por varias razones: 
  • El diseño es más flexible;
  • El modelo es más estable porque el cambio no se propaga a través de la jerarquía de clases;
  • Una clase y su composición están débilmente acopladas en comparación con la composición, que acopla estrechamente un padre y su subclase.
  • La línea lógica de pensamiento en una clase es más sencilla, ya que todas sus dependencias están incluidas en ella, en un solo lugar. 
De todos modos, la herencia tiene su lugar y resuelve una serie de problemas de diseño existentes de diversas maneras, por lo que no debe descuidarse. Tenga en cuenta estas dos alternativas al diseñar su modelo orientado a objetos.

12. ENCAPSULACIÓN.

El concepto de encapsulación en la programación orientada a objetos es ocultar todos los detalles de implementación (como el modo operativo, los métodos internos, etc.) del mundo exterior. Los beneficios de la encapsulación son la facilidad de mantenimiento y la facilidad de cambio. La implementación interna de la clase está oculta, el trabajo con los datos de la clase se produce exclusivamente a través de los métodos públicos de la clase (un problema real si estás desarrollando una biblioteca o un framework utilizado por mucha gente). La encapsulación en Java se logra mediante reglas de visibilidad y accesibilidad. En Java, se considera una buena práctica no exponer nunca los campos directamente, solo a través de captadores y definidores (a menos que los campos estén marcados como finales). Por ejemplo:
package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
}
Este ejemplo recuerda a lo que se llama JavaBeans en el lenguaje Java: las clases estándar de Java se escriben de acuerdo con un conjunto de convenciones, una de las cuales permite acceder a los campos únicamente mediante métodos getter y setter. Como ya enfatizamos en la sección de herencia, siempre respete el contrato mínimo de publicidad en una clase, utilizando los principios de encapsulación. Todo lo que no debería ser público debería volverse privado (o protegido/paquete privado, dependiendo del problema que esté resolviendo). Esto dará sus frutos a largo plazo al brindarle libertad para diseñar sin (o al menos minimizar) cambios importantes. 

13. CLASES FINALES Y MÉTODOS

En Java, hay una manera de evitar que una clase se convierta en subclase de otra clase: la otra clase debe declararse final. 
package com.javacodegeeks.advanced.design;

public final class FinalClass {
}
La misma palabra clave final en la declaración de un método evita que las subclases anulen el método. 
package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}
No existen reglas generales para decidir si una clase o un método deben ser definitivos o no. Las clases y métodos finales limitan la extensibilidad y es muy difícil pensar en el futuro si una clase debe o no heredarse, o si un método debe o no anularse en el futuro. Esto es especialmente importante para los desarrolladores de bibliotecas, ya que decisiones de diseño como esta podrían limitar significativamente la aplicabilidad de la biblioteca. La biblioteca estándar de Java tiene varios ejemplos de clases finales, siendo la más famosa la clase String. En una etapa temprana, esta decisión se tomó para evitar cualquier intento de los desarrolladores de encontrar su propia y "mejor" solución para implementar cadenas. 

14. ¿QUÉ SIGUE?

En esta parte de la lección, cubrimos los conceptos de programación orientada a objetos en Java. También echamos un vistazo rápido a la programación por contrato, abordamos algunos conceptos funcionales y vemos cómo el lenguaje ha evolucionado con el tiempo. En la siguiente parte de la lección, conoceremos los genéricos y cómo cambian la forma en que abordamos la seguridad de tipos en la programación. 

15. DESCARGAR CÓDIGO FUENTE

Puede descargar la fuente aquí: advanced-java-part-3 Fuente: Cómo diseñar clases y
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION