《面向对象的方法》一书。5th int。ed。”

图片面向对象编程(OOP)是C ++,Java,C#,Visual Basic .NET,Ruby,Objective-C甚至Swift语言的核心。他们不能没有网络技术对象,因为它们使用JavaScript,Python和PHP。

这就是为什么Matt Weissfeld建议开发一种面向对象的思维方式,然后才使用特定的编程语言进行面向对象的开发的原因。

本书是由开发人员为开发人员编写的,可让您选择解决特定问题的最佳方法。您将学习如何正确应用继承和组合,了解聚合和关联之间的区别,以及避免混淆接口和实现。

编程技术在不断变化和发展,但是面向对象的概念与平台无关,并且始终有效。本出版物重点介绍OOP的基本原理:设计模式,依赖关系和SOLID原理,这些原理将使您的代码易于理解,灵活且易于维护。

SOLID面向对象的设计原则


1. SRP:唯一责任原则


唯一责任原则指出,仅需一个理由即可更改班级。每个类和程序模块必须具有一个优先级的任务。因此,不应出于多种原因引入可导致类更改的方法。如果类描述包含单词“ and”,则可能违反SRP原则。换句话说,每个模块或类都必须负责软件功能的一部分,并且此类责任必须完全封装在类中。

创建图的层次结构是说明继承的经典示例之一,该示例经常在教学中找到,我在本章(以及整本书中)都使用它。在此示例中,Circle类从Shape类继承属性。Shape类提供抽象的calcArea()方法作为子类的协定。从Shape继承的每个类都必须具有自己的calcArea()方法的实现:

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

在此示例中,从Shape类继承的Circle类在必要时提供其对calcArea()方法的实现:

class Circle extends Shape{
     private double radius;

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

第三类CalculateAreas计算Shape数组中包含的各种形状的面积。Shape数组的大小不受限制,可以包含各种形状,例如正方形和三角形。

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);
     }
}

请注意,CalculateAreas类还处理应用程序输出,这可能会导致问题。面积计数行为和输出行为是相关的,因为它们包含在同一类中。

我们可以使用适当的测试应用程序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();
      }
}

现在我们有一个测试应用程序可供使用,我们可以专注于唯一责任原则的问题。同样,问题出在CalculateAreas类上,该类包含用于将各种形状的区域相加以及输出数据的行为。

基本的问题(实际上是问题)是,如果您需要更改output()方法的功能,则无论计算形状面积的方法是否发生变化,都需要对CalculateAreas类进行更改。例如,如果我们突然想要将数据输出到HTML控制台而不是纯文本,则需要重新编译并重新实现代码,这会增加图形的面积。都是因为责任是相关的。

按照唯一责任原则,任务是更改一种方法不会影响另一种方法,并且不必重新编译。 “班级应该只有一个改变的理由,这是唯一需要改变的责任。”

要解决此问题,可以将两个方法放在单独的类中,一个用于原始控制台输出,另一个用于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>") ;
     }
}

最重要的是,现在您可以根据需要向不同的方向发送结论。如果要添加其他输出方法(例如JSON)的可能性,则可以将其添加到OutputAreas类,而无需对CalculateAreas类进行更改。因此,您可以重新分配CalculateAreas类,而不会以任何方式影响其他类。

2. OCP:开放/封闭原则


开放性/紧密性的原则指出,您可以扩展类的行为而无需进行更改。

让我们再次关注带有数字的示例。在下面的代码中,有一个ShapeCalculator类,它接收一个Rectangle对象,计算该对象的面积并返回值。这是一个简单的应用程序,但仅适用于矩形。

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));
      }
}

该应用程序仅在矩形的情况下起作用的事实导致了一个局限性,该局限性清楚地说明了开放性/闭合性的原理:如果我们想将Circle类添加到CalculateArea类(更改其功能),则需要对模块本身进行更改。显然,这与开放性/封闭性原则相冲突,开放性/封闭性原则指出我们不应更改模块以更改其功能。

为了遵守开放性/封闭性原则,我们可以返回已经测试过的图形实例,其中创建了一个抽象Shape类,并且直接从Shape类继承了图形,该Shape类具有一个抽象getArea()方法。

目前,您可以根据需要添加任意多个不同的类,而无需直接对Shape类(例如Circle类)进行更改。现在我们可以说Shape类已关闭。

以下代码为矩形和圆形提供了解决方案的实现,并允许您创建无限数量的形状:

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));
}
}

值得注意的是,使用此实现,在创建Shape类的新实例时,不应修改CalculateAreas()方法。

您可以扩展代码而不必担心先前代码的存在。开放性/封闭性的原则是您应该使用子类扩展代码,以使原始类不需要进行编辑。但是,在有关SOLID原理的一些讨论中,“扩展”概念本身存在争议。坦率地说,如果我们更喜欢组合而不是继承,这如何影响开放性/封闭性原则?

遵循SOLID原则之一时,代码可能满足其他SOLID原则的条件。例如,在按照开放性/封闭性原则进行设计时,代码可能符合唯一责任原则的要求。

3. LSP:Lisk替代原则


根据Liskov替换原则,设计应提供使用父类之一的实例替换父类的任何实例的可能性。如果父类可以执行任何任务,则子类也必须能够执行。

乍一看一些正确的代码,但违反了Lisk替换原则。下面的代码包含通用的Shape抽象类。反过来,Rectangle类从Shape类继承属性,并覆盖其抽象的calcArea()方法。而Square类则继承自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());
      }
}

到目前为止,一切都很好:矩形是图形的一个实例,因此不必担心,因为正方形是矩形的一个实例-一切都正确,对吗?

现在让我们问一个哲学问题:正方形还是矩形吗?许多人会回答是肯定的。尽管可以假定正方形是矩形的特例,但其属性会有所不同。矩形是一个平行四边形(相对的边是相同的),就像一个正方形。同时,正方形也是菱形(所有边都相同),而矩形不是。因此,存在差异。

对于面向对象的设计,问题不在于几何。问题是我们如何精确地创建矩形和正方形。这是Rectangle类的构造函数:

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

显然,构造函数需要两个参数。但是,即使父类Rectangle需要两个,Square类的构造函数也只需要一个。

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

实际上,在这两类中的每一种的情况下,用于计算面积的功能都略有不同。也就是说,Square类确实模仿了Rectangle,将相同的参数两次传递给构造函数。这样的解决方法似乎很合适,但是实际上,它可能会误导附带代码的开发人员,而将来在附带代码时,会充满陷阱。至少这是一个问题,并且可能是一个可疑的设计决策。当一个构造函数调用另一个构造函数时,最好休息一下并重新考虑该构造-子类可能未正确构建。

如何找到摆脱这种情况的出路?简而言之,您不能用Square类代替Rectangle。因此,Square不应是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 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:接口共享原则


接口分离的原理指出,创建多个小接口比创建多个大接口更好。

在此示例中,我们创建一个接口,其中包含Mammal类的几种行为,即eat()和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()
      }
}

您需要
为所有行为创建单独的接口,而不是为Mammal类创建单个接口:

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();
      }
}

我们将行为与哺乳动物班分开。事实证明,与其通过继承(更确切地说,是接口)创建单个Mammal类,我们不如上一章中所使用的策略,而是基于组合进行设计。

简而言之,通过这种方法,我们可以使用合成来创建哺乳动物类的实例,而不是被迫使用嵌入在单个哺乳动物类中的行为。例如,假设发现哺乳动物不吃东西,而是通过皮肤吸收营养。如果我们从包含eat()行为的Mammal类继承,则此行为对于新哺乳动物将是多余的。而且,如果将所有行为都布置在单独的单个界面中,那么结果将完全按照预期来构建每种哺乳动物的类别。

»这本书的更多信息可以在出版商的网站上找到
» 目录
» Habrozhitelami的节录可

在优惠券上享受25%的折扣-OOP

支付了纸质版本的书后,就会通过电子邮件发送电子书。

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


All Articles