The book “Object-Oriented Approach. 5th int. ed. “

imageObject Oriented Programming (OOP) is at the heart of the C ++, Java, C #, Visual Basic .NET, Ruby, Objective-C, and even Swift languages. They cannot do without the objects of web technology, because they use JavaScript, Python and PHP.

That is why Matt Weissfeld advises developing an object-oriented way of thinking and only then proceed with object-oriented development in a specific programming language.

This book is written by the developer for developers and allows you to choose the best approaches for solving specific problems. You will learn how to correctly apply inheritance and composition, understand the difference between aggregation and association, and stop confusing the interface and implementation.

Programming technologies are constantly changing and developing, but object-oriented concepts are platform independent and remain consistently effective. This publication focuses on the fundamental fundamentals of OOP: design patterns, dependencies, and SOLID principles that will make your code understandable, flexible, and well-maintained.

SOLID Object Oriented Design Principles

1. SRP: sole responsibility principle

The principle of sole responsibility states that only one reason is required to make changes to a class. Each class and program module must have one task in priority. Therefore, you should not introduce methods that can cause changes to the class for more than one reason. If the class description contains the word “and,” then the SRP principle may be violated. In other words, each module or class must be responsible for one part of the software functionality, and such responsibility must be fully encapsulated in the class.

Creating a hierarchy of figures is one of the classic examples illustrating inheritance. This example is often found in teaching, and I use it throughout this chapter (as well as throughout the book). In this example, the Circle class inherits attributes from the Shape class. The Shape class provides the abstract calcArea () method as a contract for a subclass. Each class that inherits from Shape must have its own implementation of the calcArea () method:

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

In this example, the Circle class, which inherits from the Shape class, provides its implementation of the calcArea () method, if necessary:

class Circle extends Shape{
     private double radius;

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

The third class, CalculateAreas, calculates the area of ​​the various shapes contained in the Shape array. The Shape array is unlimited in size and can contain various shapes, such as squares and triangles.

class CalculateAreas {
     Shape[] shapes;
     double sumTotal=0;
     public CalculateAreas(Shape[] sh) {
           this.shapes = sh;
     }
     public double sumAreas() {
           sumTotal=0;
           for (inti=0; ipublic 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();
      }
}

Now that we have a test application at our disposal, we can focus on the problem of the principle of sole responsibility. Again, the problem is with the CalculateAreas class and the fact that this class contains behaviors for adding up the areas of various shapes, as well as for outputting data.

The fundamental question (and, in fact, the problem) is that if you need to change the functionality of the output () method, you will need to make changes to the CalculateAreas class regardless of whether the method for calculating the area of ​​shapes changes. For example, if we suddenly want to output data to the HTML console, and not to plain text, we will need to re-compile and re-embed code that adds up the area of ​​the shapes. All because liability is related.

In accordance with the principle of sole responsibility, the task is to change one method does not affect the other methods and do not have to re-compile. "The class should have one, only one, reason for change - the only responsibility that needs to be changed."

To solve this issue, you can put two methods in separate classes, one for the original console output, the other for HTML output:

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

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

     public double sumAreas() {
           sumTotal=0;

           for (inti=0; i") ;
           System.out.printin("Total of all areas = " + areas);
           System.out.printin("") ;
     }
}

The bottom line here is that now you can send a conclusion in different directions depending on the need. If you want to add the possibility of another output method, for example, JSON, you can add it to the OutputAreas class without having to make changes to the CalculateAreas class. As a result, you can redistribute the CalculateAreas class without affecting other classes in any way.

2. OCP: open / closed principle

The principle of openness / closeness states that you can extend the behavior of a class without making changes.

Let us again pay attention to the example with figures. In the code below, there is a ShapeCalculator class that takes a Rectangle object, calculates the area of ​​this object, and returns values. This is a simple application, but it only works with rectangles.

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

The fact that this application works only in the case of rectangles leads to a limitation that clearly explains the principle of openness / closure: if we want to add the Circle class to the CalculateArea class (change what it does), we need to make changes to the module itself. Obviously, this conflicts with the principle of openness / closeness, which states that we should not make changes to the module to change what it does.

To comply with the principle of openness / closeness, we can return to the already tested example with figures, where an abstract class Shape is created and directly the figures inherit from the Shape class, which has an abstract method getArea ().

At the moment, you can add as many different classes as needed, without the need to make changes directly to the Shape class (for example, the Circle class). Now we can say that the Shape class is closed.

The code below provides an implementation of the solution for rectangles and circles and allows you to create an unlimited number of shapes:

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

It is worth noting that with this implementation, the CalculateAreas () method should not be modified when creating a new instance of the Shape class.

You can scale the code without worrying about the existence of the previous code. The principle of openness / closeness is that you should extend the code using subclasses so that the original class does not require edits. However, the concept of “extension” itself is controversial in some discussions regarding the principles of SOLID. Expansely speaking, if we prefer composition rather than inheritance, how does this affect the principle of openness / closeness?

When complying with one of the SOLID principles, the code may satisfy the criteria of other SOLID principles. For example, when designing in accordance with the principle of openness / closeness, the code may be suitable for the principle of sole responsibility.

3. LSP: Lisk substitution principle

According to the Liskov substitution principle, design should provide for the possibility of replacing any instance of the parent class with an instance of one of the child classes. If the parent class can perform any task, the child class must also be able to.

Consider some code that is correct at first glance, but violates the Lisk substitution principle. The code below contains a generic Shape abstract class. The Rectangle class, in turn, inherits attributes from the Shape class and overrides its abstract calcArea () method. The Square class, in turn, inherits from 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());
      }
}

So far, so good: the rectangle is an instance of the figure, so there is nothing to worry about, since the square is an instance of the rectangle - and again, everything is correct, right?

Now let's ask a philosophical question: is a square still a rectangle? Many will answer in the affirmative. Although it can be assumed that a square is a special case of a rectangle, its properties will differ. A rectangle is a parallelogram (the opposite sides are the same), like a square. At the same time, the square is also a rhombus (all sides are the same), while the rectangle is not. Therefore, there are differences.

When it comes to object-oriented design, the problem is not geometry. The problem is how exactly we create rectangles and squares. Here is the constructor for the Rectangle class:

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

Obviously, the constructor requires two parameters. However, the constructor for the Square class requires only one, even though the parent class, Rectangle, requires two.

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

In fact, the functional for calculating the area is slightly different in the case of each of these two classes. That is, the Square class, as it were, imitates a Rectangle, passing the same parameter twice to the constructor. It may seem that such a workaround is quite suitable, but in fact it can mislead the developers accompanying the code, which is quite fraught with pitfalls when accompanied in the future. At least this is a problem and, probably, a dubious design decision. When one constructor calls another, it's a good idea to take a break and reconsider the construct - perhaps the child class is not built properly.

How to find a way out of this situation? Simply put, you cannot substitute a Square class for a Rectangle. Thus, Square should not be a child of the Rectangle class. They must be separate classes.

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: interface sharing principle

The principle of separation of interfaces states that it is better to create many small interfaces than several large ones.

In this example, we are creating a single interface that includes several behaviors for the Mammal class, namely eat () and 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()
      }
}

Instead of creating a single interface for the Mammal class, you need to create
Separate interfaces for all behaviors:

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

We separate behaviors from the Mammal class. It turns out that instead of creating a single Mammal class through inheritance (more precisely, interfaces), we move on to design based on composition, similar to the strategy that we followed in the previous chapter.

In a few words, with this approach, we can create instances of the Mammal class using composition, rather than being forced to use behaviors that are embedded in a single Mammal class. For example, suppose a mammal is discovered that does not eat, but instead absorbs nutrients through the skin. If we inherit from the Mammal class containing the eat () behavior, this behavior will be redundant for the new mammal. Moreover, if all the behaviors are laid in separate single interfaces, it will turn out to build the class of each mammal exactly as intended.

»More details on the book can be found at publishing site

" Table of contents

" Excerpt

For Khabrozhiteley 25% discount on the coupon - OOP

Upon payment of the paper version of the book, an electronic book is sent by e-mail.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *