El libro "Enfoque orientado a objetos. 5to int. ed. "

imagenLa programación orientada a objetos (OOP) está en el corazón de los lenguajes C ++, Java, C #, Visual Basic .NET, Ruby, Objective-C e incluso Swift. No pueden prescindir de los objetos de tecnología web, porque usan JavaScript, Python y PHP.

Es por eso que Matt Weissfeld aconseja desarrollar una forma de pensar orientada a objetos y solo entonces proceder con el desarrollo orientado a objetos en un lenguaje de programación específico.

Este libro está escrito por el desarrollador para desarrolladores y le permite elegir los mejores enfoques para resolver problemas específicos. Aprenderá a aplicar correctamente la herencia y la composición, comprender la diferencia entre agregación y asociación, y dejar de confundir la interfaz y la implementación.

Las tecnologías de programación cambian y se desarrollan constantemente, pero los conceptos orientados a objetos son independientes de la plataforma y permanecen consistentemente efectivos. Esta publicación se centra en los fundamentos fundamentales de OOP: patrones de diseño, dependencias y principios SÓLIDOS que harán que su código sea comprensible, flexible y esté bien mantenido.

Principios de diseño orientado a objetos sólidos


1. SRP: principio de responsabilidad única


El principio de responsabilidad exclusiva establece que solo se requiere una razón para realizar cambios en una clase. Cada clase y módulo de programa debe tener una tarea en prioridad. Por lo tanto, no debe introducir métodos que puedan causar cambios en la clase por más de una razón. Si la descripción de la clase contiene la palabra "y", entonces se puede violar el principio SRP. En otras palabras, cada módulo o clase debe ser responsable de una parte de la funcionalidad del software, y dicha responsabilidad debe estar completamente encapsulada en la clase.

Crear una jerarquía de figuras es uno de los ejemplos clásicos que ilustran la herencia. Este ejemplo a menudo se encuentra en la enseñanza, y lo uso a lo largo de este capítulo (así como en todo el libro). En este ejemplo, la clase Circle hereda los atributos de la clase Shape. La clase Shape proporciona el método abstracto calcArea () como un contrato para una subclase. Cada clase que hereda de Shape debe tener su propia implementación del método calcArea ():

abstract class Shape{
     protected String name;
     protected double area;
     public abstract double calcArea();
}

En este ejemplo, la clase Circle, que hereda de la clase Shape, proporciona su implementación del método calcArea (), si es necesario:

class Circle extends Shape{
     private double radius;

     public Circle(double r) {
           radius = r;
     }
     public double calcArea() {
           area = 3.14*(radius*radius) ;
           return (area);
     };
}

La tercera clase, CalculateAreas, calcula el área de las diversas formas contenidas en la matriz de formas. La matriz de formas tiene un tamaño ilimitado y puede contener varias formas, como cuadrados y triángulos.

class CalculateAreas {
     Shape[] shapes;
     double sumTotal=0;
     public CalculateAreas(Shape[] sh) {
           this.shapes = sh;
     }
     public double sumAreas() {
           sumTotal=0;
           for (inti=0; i<shapes.length; i++) {
           sumTotal = sumTotal + shapes[i].calcArea() ;
           }
           return sumTotal ;
     }
     public void output() {
           System.out.printIn("Total of all areas = " + sumTotal);
     }
}

Tenga en cuenta que la clase CalculateAreas también maneja la salida de la aplicación, lo que puede causar problemas. El comportamiento de conteo de área y el comportamiento de salida están relacionados porque están contenidos en la misma clase.

Podemos verificar la funcionalidad de este código utilizando la aplicación de prueba apropiada TestShape:

public class TestShape {
      public static void main(String args[]) {

            System.out.printin("Hello World!");

            Circle circle = new Circle(1);

            Shape[] shapeArray = new Shape[1];
            shapeArray[0] = circle;

            CalculateAreas ca = new CalculateAreas(shapeArray) ;

            ca.sumAreas() ;
            ca.output();
      }
}

Ahora que tenemos una aplicación de prueba a nuestra disposición, podemos centrarnos en el problema del principio de responsabilidad exclusiva. Nuevamente, el problema es con la clase CalculateAreas y el hecho de que esta clase contiene comportamientos para sumar las áreas de varias formas, así como para generar datos.

La pregunta fundamental (y, de hecho, el problema) es que si necesita cambiar la funcionalidad del método output (), deberá realizar cambios en la clase CalculateAreas independientemente de si cambia el método para calcular el área de formas. Por ejemplo, si de repente deseamos enviar datos a la consola HTML, y no a texto plano, tendremos que volver a compilar y volver a incrustar el código que suma el área de las formas. Todo porque la responsabilidad está relacionada.

De acuerdo con el principio de responsabilidad exclusiva, la tarea es cambiar un método, no afecta a los otros métodos y no tiene que volver a compilar. "La clase debe tener una sola razón para el cambio: la única responsabilidad que debe cambiarse".

Para resolver este problema, puede colocar dos métodos en clases separadas, uno para la salida original de la consola y el otro para la salida HTML:

class CaiculateAreas {;
     Shape[] shapes;
     double sumTotal=0;

     public CalculateAreas(Shape[] sh) {
           this.shapes = sh;
     }

     public double sumAreas() {
           sumTotal=0;

           for (inti=0; i<shapes.length; i++) {

                sumTotal = sumTotal + shapes[i].calcArea();

           }

                return sumTotal;
           }
}
class OutputAreas {
     double areas=0;
     public OutputAreas (double a) {
           this.areas = a;
     }

           public void console() {
           System.out.printin("Total of all areas = " + areas);
     }
     public void HTML() {
           System.out.printIn("<HTML>") ;
           System.out.printin("Total of all areas = " + areas);
           System.out.printin("</HTML>") ;
     }
}

La conclusión aquí es que ahora puede enviar una conclusión en diferentes direcciones según la necesidad. Si desea agregar la posibilidad de otro método de salida, por ejemplo, JSON, puede agregarlo a la clase OutputAreas sin tener que hacer cambios en la clase CalculateAreas. Como resultado, puede redistribuir la clase CalculateAreas sin afectar a otras clases de ninguna manera.

2. OCP: principio abierto / cerrado


El principio de apertura / cercanía establece que puede extender el comportamiento de una clase sin hacer cambios.

Volvamos a prestar atención al ejemplo con figuras. En el siguiente código, hay una clase ShapeCalculator que toma un objeto Rectangle, calcula el área de este objeto y devuelve valores. Esta es una aplicación simple, pero solo funciona con rectángulos.

class Rectangle{
     protected double length;
     protected double width;

     public Rectangle(double 1, double w) {
           length = 1;
           width = w;
     };
}
class CalculateAreas {
     private double area;

     public double calcArea(Rectangle r) {

           area = r.length * r.width;

           return area;
     }
}
public class OpenClosed {
      public static void main(String args[]) {

            System.out.printin("Hello World");

            Rectangle r = new Rectangle(1,2);

            CalculateAreas ca = new CalculateAreas ();

            System.out.printin("Area = "+ ca.calcArea(r));
      }
}

El hecho de que esta aplicación funcione solo en el caso de los rectángulos conduce a una limitación que explica claramente el principio de apertura / cierre: si queremos agregar la clase Circle a la clase CalculateArea (cambiar lo que hace), necesitamos hacer cambios en el módulo mismo. Obviamente, esto entra en conflicto con el principio de apertura / cercanía, que establece que no debemos realizar cambios en el módulo para cambiar lo que hace.

Para cumplir con el principio de apertura / cercanía, podemos volver al ejemplo ya probado con figuras, donde se crea una clase abstracta de Shape y directamente las figuras heredan de la clase Shape, que tiene un método abstracto getArea ().

Por el momento, puede agregar tantas clases diferentes como sea necesario, sin la necesidad de realizar cambios directamente en la clase Shape (por ejemplo, la clase Circle). Ahora podemos decir que la clase Shape está cerrada.

El siguiente código proporciona una implementación de la solución para rectángulos y círculos y le permite crear un número ilimitado de formas:

abstract class Shape {
      public abstract double getArea() ;
}
class Rectangle extends Shape
{

      protected double length;
      protected double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      };
      public double getArea() {
            return length*width;
      }

}
class Circle extends Shape
{
      protected double radius;

      public Circle(double r) {
            radius = r;
      };
      public double getArea() {
            return radius*radius*3.14;
      }
}
class CalculateAreas {
      private double area;

      public double calcArea(Shape s) {
            area = s.getArea();
            return area;
      }
}

public class OpenClosed {
      public static void main(String args[]) {

            System.out.printiIn("Hello World") ;

            CalculateAreas ca = new CalculateAreas() ;

            Rectangle r = new Rectangle(1,2);

            System.out.printIn("Area = " + ca.calcArea(r));

            Circle c = new Circle(3);

            System.out.printIn("Area = " + ca.calcArea(c));
}
}

Vale la pena señalar que con esta implementación, el método CalculateAreas () no debe modificarse al crear una nueva instancia de la clase Shape.

Puede escalar el código sin preocuparse por la existencia del código anterior. El principio de apertura / cercanía es que debe extender el código utilizando subclases para que la clase original no requiera modificaciones. Sin embargo, el concepto de "extensión" en sí mismo es controvertido en algunas discusiones sobre los principios de SOLID. Para decirlo sin rodeos, si preferimos la composición en lugar de la herencia, ¿cómo afecta esto el principio de apertura / cercanía?

Al cumplir con uno de los principios SOLID, el código puede satisfacer los criterios de otros principios SOLID. Por ejemplo, cuando se diseña de acuerdo con el principio de apertura / cercanía, el código puede ajustarse a los requisitos del principio de responsabilidad exclusiva.

3. LSP: principio de sustitución de Lisk


De acuerdo con el principio de sustitución de Liskov, el diseño debe permitir la posibilidad de reemplazar cualquier instancia de la clase principal con una instancia de una de las clases secundarias. Si la clase principal puede realizar cualquier tarea, la clase secundaria también debe poder hacerlo.

Considere algún código que sea correcto a primera vista, pero que viole el principio de sustitución de Lisk. El siguiente código contiene una clase abstracta de forma genérica. La clase Rectangle, a su vez, hereda los atributos de la clase Shape y anula su método abstracto calcArea (). La clase Square, a su vez, hereda de Rectangle.

abstract class Shape{
      protected double area;

      public abstract double calcArea();
}
class Rectangle extends Shape{
      private double length;
      private double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      }
      public double calcArea() {
            area = length*width;
            return (area) ;
      };
}
class Square extends Rectangle{
      public Square(double s) {
            super(s, Ss);
      }
}

public class LiskovSubstitution {
      public static void main(String args[]) {

            System.out.printIn("Hello World") ;

            Rectangle r = new Rectangle(1,2);

            System.out.printin("Area = " + r.calcArea());

            Square s = new Square(2) ;

            System.out.printin("Area = " + s.calcArea());
      }
}

Hasta ahora todo bien: el rectángulo es una instancia de la figura, por lo que no hay nada de qué preocuparse, ya que el cuadrado es una instancia del rectángulo, y de nuevo, todo es correcto, ¿verdad?

Ahora hagamos una pregunta filosófica: ¿un cuadrado sigue siendo un rectángulo? Muchos responderán afirmativamente. Aunque se puede suponer que un cuadrado es un caso especial de un rectángulo, sus propiedades serán diferentes. Un rectángulo es un paralelogramo (los lados opuestos son iguales), como un cuadrado. Al mismo tiempo, el cuadrado también es un rombo (todos los lados son iguales), mientras que el rectángulo no lo es. Por lo tanto, hay diferencias.

Cuando se trata de diseño orientado a objetos, el problema no es la geometría. El problema es cómo exactamente creamos rectángulos y cuadrados. Aquí está el constructor para la clase Rectangle:

public Rectangle(double 1, double w) {
      length = 1;
      width = w;
}

Obviamente, el constructor requiere dos parámetros. Sin embargo, el constructor para la clase Square requiere solo uno, aunque la clase padre, Rectangle, requiere dos.

class Square extends Rectangle{
      public Square(double s) {
      super(s, Ss);
}

De hecho, la función para calcular el área es ligeramente diferente en el caso de cada una de estas dos clases. Es decir, la clase Square, por así decirlo, imita un Rectángulo, pasando el mismo parámetro dos veces al constructor. Puede parecer que tal solución es bastante adecuada, pero de hecho puede engañar a los desarrolladores que acompañan el código, que está lleno de dificultades cuando se acompaña en el futuro. Al menos esto es un problema y, probablemente, una dudosa decisión de diseño. Cuando un constructor llama a otro, es una buena idea tomarse un descanso y reconsiderar la construcción, tal vez la clase secundaria no se construye correctamente.

¿Cómo encontrar una salida a esta situación? En pocas palabras, no puede sustituir una clase cuadrada por un rectángulo. Por lo tanto, Square no debería ser hijo de la clase Rectangle. Deben ser clases separadas.

abstract class Shape {
      protected double area;

      public abstract double calcArea();
}

class Rectangle extends Shape {

      private double length;
      private double width;

      public Rectangle(double 1, double w) {
            length = 1;
            width = w;
      }

      public double calcArea() {
            area = length*width;
            return (area);
      };
}

class Square extends Shape {
      private double side;

      public Square(double s) {
            side = s;
      }
      public double calcArea() {
            area = side*side;
            return (area);
      };
}

public class LiskovSubstitution {
      public static void main(String args[]) {

             System.out.printIn("Hello World") ;

             Rectangle r = new Rectangle(1,2);

             System.out.printIn("Area = " + r.calcArea());

             Square s = new Square(2) ;

             System.out.printIn("Area = " + s.calcArea());
      }
}

4. ISP: principio de intercambio de interfaz


El principio de separación de interfaces establece que es mejor crear muchas interfaces pequeñas que varias grandes.

En este ejemplo, creamos una interfaz única que incluye varios comportamientos para la clase Mammal, a saber, eat () y makeNoise ():

interface IMammal {
     public void eat();
     public void makeNoise() ;
}
class Dog implements IMammal {
     public void eat() {
           System.out.printIn("Dog is eating");
     }
     public void makeNoise() {
           System.out.printIn("Dog is making noise");
     }
}
public class MyClass {
      public static void main(String args[]) {

            System.out.printIn("Hello World");

            Dog fido = new Dog();
            fido.eat();
            fido.makeNoise()
      }
}

En lugar de crear una única interfaz para la clase Mammal, debe crear
interfaces separadas para todos los comportamientos:

interface IEat {
     public void eat();
}
interface IMakeNoise {
     public void makeNoise() ;
}
class Dog implements IEat, IMakeNoise {
     public void eat() {
           System.out.printIn("Dog is eating");
     }
     public void makeNoise() {
           System.out.printIn("Dog is making noise");
     }
}
public class MyClass {
      public static void main(String args[]) {

            System.out.printIn("Hello World") ;

            Dog fido = new Dog();
            fido.eat();
            fido.makeNoise();
      }
}

Separamos los comportamientos de la clase Mamífero. Resulta que en lugar de crear una sola clase Mammal a través de la herencia (más precisamente, interfaces), pasamos al diseño basado en la composición, similar a la estrategia utilizada en el capítulo anterior.

En pocas palabras, con este enfoque, podemos crear instancias de la clase Mammal usando la composición, en lugar de ser obligados a usar comportamientos que están integrados en una sola clase Mammal. Por ejemplo, supongamos que se descubre un mamífero que no come, sino que absorbe nutrientes a través de la piel. Si heredamos de la clase Mammal que contiene el comportamiento eat (), este comportamiento será redundante para el nuevo mamífero. Además, si todos los comportamientos se presentan en interfaces individuales separadas, resultará construir la clase de cada mamífero exactamente como se pretende.

»Se puede encontrar más información sobre el libro en el sitio web del editor
» Tabla de contenido
» Extracto de

Habrozhitelami 25% de descuento en el cupón -OOP

Tras el pago de la versión en papel del libro, se envía un libro electrónico por correo electrónico.

Source: https://habr.com/ru/post/undefined/


All Articles