Проблема
При разработке программного обеспечения часто возникает необходимость создания кода, который может обрабатывать различные типы данных и объектов. Однако, если код не спроектирован с учетом масштабируемости, он может стать громоздким и трудным для поддержки
Одним из примеров такой проблемы является обработка различных типов фигур в геометрических расчетах
Создадим два класса, представляющие геометрические фигуры:
public class Rectangle {
    private int sideA;
    private int sideB;
    public Rectangle(int sideA, int sideB) {
        this.sideA = sideA;
        this.sideB = sideB;
    }
    public double getArea() {
        System.out.println("get Rectangle area");
        return sideA * sideB;
    }
}
public class Square {
    private int side;
    public Square(int side) {
        this.side = side;
    }
    public double getArea() {
        System.out.println("get square area");
        return side * side;
    }
}
и метод, который будет выполнять геометрические расчеты
public class Example1 {
    public static void main(String[] args) {
        List < Square > squares = List.of(
            new Square(2),
            new Square(3)
        );
        List < Rectangle > rectangles = List.of(
            new Rectangle(1, 2),
            new Rectangle(1, 3)
        );
        System.out.println(getTotalArea(squares, rectangles)); //=> 18
    }
    public static double getTotalArea(List<Square> squares, List<Rectangle> rectangles) {
        double result = 0;
        for (Square square : squares) {
            result += square.getArea();
        }
        for (Rectangle rectangle : rectangles) {
            result += rectangle.getArea();
        }
        return result;
    }
}
Проблема этого кода заключается в том, что его сложно масштабировать. Если нам нужно добавить новый тип фигуры, например, круг, то нам придется изменить метод getTotalArea()
Сделаем это, добавим еще одну фигуру — круг
public class Circle {
    private int r;
    public Circle(int r) {
        this.r = r;
    }
    public double getArea() {
        return Math.PI * r * r;
    }
}
Расширим метод так, чтобы он мог работать еще и с кругами
public class Example1 {
    public static void main(String[] args) {
        List < Square > squares = List.of(
            new Square(2),
            new Square(3)
        );
        List < Rectangle > rectangles = List.of(
            new Rectangle(1, 2),
            new Rectangle(1, 3)
        );
        List < Rectangle > rectangles = List.of(
            new Circle(1)
        );
        System.out.println(getTotalArea(squares, rectangles, circles)); //=> 21.1415...
    }
    public static double getTotalArea(
        List<Square> squares,
        List<Rectangle> rectangles,
        List<Circles> circles
    ) {
        double result = 0;
        for (Square square : squares) {
            result += square.getArea();
        }
        for (Rectangle rectangle : rectangles) {
            result += rectangle.getArea();
        }
        for (Circle circle : circles) {
            result += circle.getArea();
        }
        return result;
    }
}
Если мы добавим еще 10 типов фигур, то метод getTotalArea() станет очень большим и сложным, и нам придется изменять его каждый раз, когда мы добавляем новый тип фигуры
Решение
Сделаем интерфейс, который будут реализовывать геометрические фигуры
public interface Shape {
    double getArea();
}
Откорректируем классы, чтобы они реализовывали этот интерфейс
public class Rectangle implements Shape {
    private int sideA;
    private int sideB;
    public Rectangle(int sideA, int sideB) {
        this.sideA = sideA;
        this.sideB = sideB;
    }
    public double getArea() {
        System.out.println("get Rectangle area");
        return sideA * sideB;
    }
}
public class Square implements Shape {
    private int side;
    public Square(int side) {
        this.side = side;
    }
    public double getArea() {
        System.out.println("get square area");
        return side * side;
    }
}
public class Circle implements Shape  {
    private int r;
    public Circle(int r) {
        this.r = r;
    }
    public double getArea() {
        System.out.println("get Circle area");
        return Math.PI * r * r;
    }
}
Изменим метод для подсчета площади
public class Example1 {
    public static void main(String[] args) {
        List <Shape> squares = List.of(
            new Square(2),
            new Square(3),
            new Rectangle(1, 2),
            new Rectangle(1, 3),
            new Circle(1)
        );
        System.out.println(getTotalArea(shapes)); //=> 21.1415...
    }
    public static double getTotalAreal(List<Shape> shapes) {
        double result = 0;
        for (Shape shape: shapes) {
            result += shape.getArea();
        }
        return result;
    }
}
Теперь метод getTotalArea() может обрабатывать любые фигуры, реализующие интерфейс Shape. В результате наших изменений мы получили более гибкий и масштабируемый код, который позволяет легко добавлять новые типы фигур без изменения метода getTotalArea()
Игры разума
public class Calc {
    public static String[] generateQuestion() {
        return new String[0];
    }
    public static String getDescription() {
        return "Введите результат выражения";
    }
}
public class IsEven {
    public static String[] generateQuestion() {
        return new String[0];
    }
    public static String getDescription() {
        return "Является ли число четным";
    }
}
public class IsPrime {
    public static String[] generateQuestion() {
        return new String[0];
    }
    public static String getDescription() {
        return "Является ли число простым";
    }
}
class Example2 {
    public final static String IS_EVEN_NAME = "IS_EVEN";
    public final static String CALC_NAME = "CALC";
    public final static String IS_PRINE_NANE = "IS_PRIME";
    public static void main(String[] args) {
        System.out.println(getGameDescription(IS_EVEN_NAME));
    }
    public static String getGameDescription(String name) {
        return switch (name) {
            case IS_EVEN_NAME -> IsEven.getDescription();
            case CALC_NAME -> Calc.getDescription();
            case IS_PRIME_NAME -> IsPrime.getDescription();
            default -> throw new RuntimeException();
        };
    }
    public static String[] generateQuestion(String name) {
        return switch (name) {
            case IS_EVEN_NAME -> IsEven.generateQuestion();
            case CALC_NAME -> Calc.generateQuestion();
            case IS_PRIME_NAME -> IsPrime.generateQuestion();
            default -> throw new RuntimeException();
        };
    }
}
Проблема этого кода заключается в том, что для добавления новой игры необходимо каждый раз изменять класс Example2 и добавлять новый случай в методах getGameDescription() и generateQuestion(). Это может привести к дублированию кода и усложнению класса, что затрудняет его поддержку и расширение.
Чтобы решить эту проблему, можно создать интерфейс для игр, который будет иметь методы getDescription() и generateQuestion(). Тогда каждый класс игры будет реализовывать этот интерфейс
Создадим интерфейс:
public interface Game {
    String getDescription();
    String[] generateQuestion();
}
Доработаем классы игр, чтобы они реализовывали интерфейс
public class Calc implements Game {
    public String[] generateQuestion() {
        return new String[0];
    }
    public String getDescription() {
        return "Введите результат выражения";
    }
}
public class IsEven implements Game {
    public String[] generateQuestion() {
        return new String[0];
    }
    public String getDescription() {
        return "Является ли число четным";
    }
}
public class IsPrime implements Game {
    public String[] generateQuestion() {
        return new String[0];
    }
    public String getDescription() {
        return "Является ли число простым";
    }
}
Теперь мы можем упростить код в классе Example2
class Example2 {
    public final static String IS_EVEN_NAME = "IS_EVEN";
    public final static String CALC_NAME = "CALC";
    public final static String IS_PRINE_NANE = "IS_PRIME";
    public static Map<String, Game> games = Map.of(
        IS_EVEN_NAME, new IsEven(),
        CALC_NAME, new Calc(),
        IS_PRINE_NANE, new IsPrime()
    );
    public static void main(String[] args) {
        System.out.println(getGameDescription(IS_EVEN_NAME));
    }
    public static String getGameDescription(String name) {
        return games.get(name).getDescription();
    }
    public static String[] generateQuestion(String name) {
        return games.get(name).generateQuestion();
    }
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.