ESPN DeportesSantos remonta a Toluca y firma una noche épica en el Nemesio DiezPunchKwara Poly expels 42 students for exam malpracticeBollywood HungamaEXCLUSIVE: Rajkummar Rao and Pratibha Ranta to headline Dharma Productions’ fantasy horror VishkanyaRTP DesportoFC Porto-Benfica. Protagonistas com visões diferentes sobre o clássicoESPN👀 NFL Week 2 overreactions: Would Steelers bench Rodgers?InquirerDA backs P210/kilo hog floor amid low farmgate ratesEgypt IndependentWhat to expect at the UN General Assembly, the biggest event in world diplomacyIl Fatto QuotidianoCaso mascherine Covid, chiesto il processo per Irene Pivetti e altre otto personeRMF24Przepustka do wolności słowa. Trzy redakcje pozywają TrumpaRTL BoulevardTaylor Swift dolblij met touchdown van echtgenoot Travis KelceThe South AfricanNew PowerBall rule has made five South Africans MEGA richFootball ItaliaNeto tipped for Juventus return as free agent after nine years
The Daily Newsstand · Free, Always
Monday, September 21, 2026

Базовые принципы ООП и SOLID

Translate

Данная статья служит двум целям, быстро освежить знания по ООП и SOLID, а также полезно и весело по‑проектировать роботов с элементами игры средствами языка Java. Итак, Объектно‑Ориентированное Программирование (ООП).

Рассмотрим сначала основные четыре аспекта ООП:

  1. Абстракция

  2. Наследование

  3. Инкапсуляция

  4. Полиморфизм

Итак начнем проектировать наших роботов с учетом ООП.

Пусть у нас есть карта размером 1000 на 1000 квадратов для примера. Один робот может занимать 1 квадрат полностью. Любое препятствие также занимает 1 квадрат или ячейку. Этакие «шахматы для роботов» вместо шахматных фигур.

Абстракция

Абстракция служит для выделения общих признаков для группы объектов. Определим общее для трех видов роботов: транспортный, ремонтный, военный. Общие данные для всех роботов будут координаты X и Y, имя и идентификатор уникальный, количество щитов, максимальное количество шагов за этап, а также порядковый номер выпуска, дата выпуска. Пусть имя общего абстрактного класса будет BaseRobot.

Код 1. Базовый класс робота.

public abstract class  BaseRobot {

    Integer xPosition;

    Integer yPosition;

    UUID uuid;

    Integer serialNumber;

    Date dataIssue;

    Integer power;

    Integer shield;

    public abstract Integer moveX(Integer xStep);

    public abstract Integer moveY(Integer yStep);
}

Абстрактный класс также будет содержать два абстрактных метода для передвижения по нашей «шахматной доске для роботов». И да, пусть роботы могут двигаться только по горизонтали и вертикали, при этом тратят одну единицу энергии за передвижение на одну клетку по вертикали и горизонтали. Каждый ход количество энергии восстанавливается.

Непосредственно реализация методов будет в классах потомках, здесь только обозначим данные методы.

Таким образом, абстракция — это выделение значимых общих признаков у группы объектов. Абстрактный класс нельзя создать, от него можно только наследоваться.

Итак, плавно переходим к наследованию и созданию классов роботов, на основании которых будем создавать объекты.

Наследование

Наследование — это реализация нового класса на основе уже существующего с частичной или полностью заимствованной функциональностью.

Создадим три класса роботов через наследование от абстрактного класса. Это будет транспортный робот, робот ремонтник и военный робот.

Созданные классы ниже, соответственно это: TransportRobot, UtilityRobot, MilitaryRobot.

Пусть транспортный робот может добывать и перевозить минералы до фабрики, робот сборщик может ремонтировать других юнитов и строить здания, а робот военный атаковать других роботов. Реализуем эти классы.

Первым реализуем класс военного робота, добавим поле сила атаки, и метод, который выполняет атаку. Метод принимает объект базового класса (атакуемого робота) и убирает количество щитов, в случае, если значение щитов менее 0, то объект считается уничтоженным. Пусть военный робот может атаковать только других роботов, но не может атаковать и разрушать здания. Код 2 показывает класс военного робота.

Вторым реализуем класс транспортного робота, который перевозит минералы от шахты до фабрики. Класс транспортного робота наследуется от базового класса и расширяется. Добавляется количество поинтов перевозимого груза и два метода, загрузка и разгрузка. На загрузку и разгрузку тратится полностью вся энергия на ход. Код 3 показывает данный класс.

Следующий класс — это ремонтный робот. Он имеет поле — значение на которое может ремонтировать других роботов за раз, а также метод начала ремонта. При этом ремонтируемый робот теряет поинты передвижения на текущем ходу. Код 4 показывает данный класс.

Код 2. Класс военного робота.

public class MilitaryRobot extends BaseRobot {

    Integer attack;

    public MilitaryRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer attack) {
        this.uuid = UUID.randomUUID();
        this.dataIssue = new Date();
        this.serialNumber = serialNumber;
        this.power = ConstantApp.getInstance().ROBOT_POWER_MILITARY;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_MILITARY;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.attack = attack;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    //В случае, если щиты объекта меньше 0, то объект разрушен и возвращается флаг true
    public boolean attack(BaseRobot item) {
        item.shield = item.shield - attack;
        return item.shield < 0;
    }
}

Код 3. Класс транспортного робота.

public class TransportRobot extends BaseRobot  {

    Integer cargoWeight;

    public TransportRobot(Integer xStart, Integer yStart, Integer serialNumber) {
        this.uuid = UUID.randomUUID();
        this.dataIssue = new Date();
        this.serialNumber = serialNumber;
        this.power = ConstantApp.getInstance().ROBOT_POWER_TRANSPORT;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.cargoWeight = 0;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    //Метод загрузки
    public boolean loadingCargo() {
        if (this.power == ConstantApp.getInstance().ROBOT_POWER_TRANSPORT) {
            this.power = 0;
            this.cargoWeight = ConstantApp.getInstance().ROBOT_TRANSPORT_WEIGHT_POINTS;
            return true;
        }
        return false;
    }

    //Метод разгрузки
    public boolean unloadingCargo() {
        if (this.power == ConstantApp.getInstance().ROBOT_POWER_TRANSPORT) {
            this.power = 0;
            this.cargoWeight = 0;
            return true;
        }
        return false;
    }
}

Код 4. Класс ремонтного робота.

public class UtilityRobot extends BaseRobot {

    Integer repairPointShield;

    public UtilityRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer repairPointShield) {
        this.uuid = UUID.randomUUID();
        this.dataIssue = new Date();
        this.serialNumber = serialNumber;
        this.power = ConstantApp.getInstance().ROBOT_POWER_UTILITY;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_UTILITY;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.repairPointShield = repairPointShield;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    public void repairUnit(BaseRobot item) {
        item.power = 0;
        item.shield += repairPointShield;
        if (item instanceof MilitaryRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_MILITARY)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_MILITARY;
        } else if (item instanceof UtilityRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_UTILITY)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_UTILITY;
        } else if (item instanceof TransportRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT;
        }
    }
}

Методы передвижения по карте у всех роботов на данный момент будут одинаковые, с одинаковой реализацией, эти методы пока не показываем.

Метод починки для ремонтного робота на данный момент поддерживает только три вида роботов. Реализация рабочая, на практике даже будет работать, но лучше это решить другим способом. Пока так и оставим.

Инкапсуляция

Инкапсуляция — это по сути принцип, в соответствии с которым внутреннее устройство сущностей необходимо скрывать от вмешательства из вне. Для классов это выражается в сокрытие внутренней реализации класса и предоставлять доступ к данным только через методы.

Если посмотреть на базовый класс и его наследников, то можно увидеть, что поля классов доступны для изменения из вне. Это недопустимо, так как поля объектов доступны бесконтрольно для изменений. Поэтому внесем изменения в базовый класс и классы наследники. Отметим все поля базового класса и наследников как protected. Данный модификатор доступа позволяет иметь доступ к полям класса у наследников/подклассах. Всего в Java существует четыре модификатора доступа:

  1. Public. Элемент (класс, метод, переменная или конструктор) доступен из любого места в программе, включая другие пакеты. Это самый открытый уровень доступа.

  2. Private. Элемент доступен только внутри класса, в котором он определён. Это наиболее ограничительный уровень доступа.

  3. Protected. Элемент доступен только для классов из того же пакета и всех подклассов. Это полезно, когда нужно предоставить доступ к определённым методам и переменным для наследников, но скрыть их от других классов.

  4. Default (пакетный доступ). Если модификатор доступа не указан явно, то используется уровень доступа по умолчанию, который также называется «пакетный доступ». Элементы с таким уровнем доступа доступны только для классов из того же пакета, что и определённый элемент.

Код 5, 6, 7, 8 показывает базовый класс и классы роботов с применённым принципами инкапсуляции.

Для базового класса существуют только методы get получения полей класса.

Методы set определяться в наследниках, если это будет необходимо.

Код 5. Базовый класс с принципами инкапсуляции.

public abstract class BaseRobot {

    protected Integer xPosition;

    protected Integer yPosition;

    protected UUID uuid;

    protected Integer serialNumber;

    protected Date dataIssue;

    protected Integer power;

    protected Integer shield;

    public abstract Integer moveX(Integer xStep);

    public abstract Integer moveY(Integer yStep);

    public Integer getxPosition() {
        return xPosition;
    }

    public Integer getyPosition() {
        return yPosition;
    }

    public UUID getUuid() {
        return uuid;
    }

    public Integer getSerialNumber() {
        return serialNumber;
    }

    public Date getDataIssue() {
        return dataIssue;
    }

    public Integer getPower() {
        return power;
    }

    public Integer getShield() {
        return shield;
    }
}

Код 6. Применение инкапсуляции для класса военного робота.

public class MilitaryRobot extends BaseRobot {
    
    protected Integer attack;

    public MilitaryRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer attack) {
        this.uuid = UUID.randomUUID();
        this.serialNumber = serialNumber;
        this.dataIssue = new Date();
        this.power = ConstantApp.getInstance().ROBOT_POWER_MILITARY;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_MILITARY;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.attack = attack;
    }

    public Integer getAttack() {
        return attack;
    }

    public void setAttack(Integer attack) {
        this.attack = attack;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    //В случае, если щиты объекта меньше 0, то объект разрушен и возвращается флаг true
    public boolean attack(BaseRobot item) {
        item.shield = item.shield - attack;
        return item.shield < 0;
    }
}

Код 7. Применение инкапсуляции для класса транспортного робота.

public class TransportRobot extends BaseRobot {
    
    protected Integer cargoWeight;

    public TransportRobot(Integer xStart, Integer yStart, Integer serialNumber) {
        this.uuid = UUID.randomUUID();
        this.dataIssue = new Date();
        this.power = ConstantApp.getInstance().ROBOT_POWER_TRANSPORT;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.cargoWeight = 0;
    }

    public Integer getCargoWeight() {
        return cargoWeight;
    }

    public void setCargoWeight(Integer cargoWeight) {
        this.cargoWeight = cargoWeight;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    //Метод загрузки
    public boolean loadingCargo() {
        if (this.power == ConstantApp.getInstance().ROBOT_POWER_TRANSPORT) {
            this.power = 0;
            this.cargoWeight = ConstantApp.getInstance().ROBOT_TRANSPORT_WEIGHT_POINTS;
            return true;
        }
        return false;
    }

    //Метод разгрузки
    public boolean unloadingCargo() {
        if (this.power == ConstantApp.getInstance().ROBOT_POWER_TRANSPORT) {
            this.power = 0;
            this.cargoWeight = 0;
            return true;
        }
        return false;
    }
}

Код 8. Применение инкапсуляции для класса ремонтного робота.

public class UtilityRobot extends BaseRobot {
    
    protected Integer repairPointShield;

    public UtilityRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer repairPointShield) {
        this.uuid = UUID.randomUUID();
        this.dataIssue = new Date();
        this.power = ConstantApp.getInstance().ROBOT_POWER_UTILITY;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_UTILITY;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.repairPointShield = repairPointShield;
    }

    public Integer getRepairPointShield() {
        return repairPointShield;
    }

    public void setRepairPointShield(Integer repairPointShield) {
        this.repairPointShield = repairPointShield;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    public void repairUnit(BaseRobot item) {
        item.power = 0;
        item.shield += repairPointShield;
        if (item instanceof MilitaryRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_MILITARY)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_MILITARY;
        } else if (item instanceof UtilityRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_UTILITY)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_UTILITY;
        } else if (item instanceof TransportRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT;
        }
    }
}

Таким образом, параметры базового класса доступны только для чтения для внешнего мира через методы get, а для подклассов и для методов подклассов доступны для чтения и изменения напрямую.

Полиморфизм

Полиморфизм — это множественность форм для одних и тех же объектов. С точки зрения ООП для Java — это возможность программы использовать объекты с одинаковым интерфейсом без информации о конкретном типе и внутренней структуре объекта.

Сложно, муторно, но на самом деле ничего сложного. Для использования полиморфизма для наших классов роботов создадим интерфейс с методом обновления состояния объектов после каждого хода. Код 9 показывает данный интерфейс.

Код 9. Интерфейс с методом обновления состояния.

public interface UpdateState {
    
    void updateItem();
    
}

Обновление состояния может быть доступно не только для наших роботов, но и для объектов типа здание и источники ресурсов. Пока этих элементов в игре нет, но это на развитие. Соответственно, для каждого объекта метод будет реализован по своему.

Данный интерфейс позволяет нам создать массив по объектам, реализующим данный интерфейс и в конце каждого хода проходить по списку и выполнять метод обновления состояния. Данный массив создается на уровне (в классе) управления игрой и при создании объекта, каждый объект, реализующий интерфейс обновления должен добавляться в данный массив.

При обновлении каждого состояния вызывается метод, который имеет свою реализацию для каждого класса:

  • для военного робота необходимо проводить проверку, что если щиты 5 поинтов и ниже, то энергия для передвижения не возобновляется на новом ходу.

  • для ремонтного робота, энергия для движения на каждом ходу восстанавливается вне зависимости от уровня щитов.

  • для транспортного робота, энергия для передвижения восстанавливается, если только уровень щита выше 1 поинта.

Код 10, 11, 12 ниже это показывает:

Код 10. Полиморфизм, военный робот.

public class MilitaryRobot extends BaseRobot implements UpdateState {
    
    protected Integer attack;

    public MilitaryRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer attack) {
        this.uuid = UUID.randomUUID();
        this.serialNumber = serialNumber;
        this.dataIssue = new Date();
        this.power = ConstantApp.getInstance().ROBOT_POWER_MILITARY;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_MILITARY;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.attack = attack;
    }

    public Integer getAttack() {
        return attack;
    }

    public void setAttack(Integer attack) {
        this.attack = attack;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    //В случае, если щиты объекта меньше 0, то объект разрушен и возвращается флаг true
    public boolean attack(BaseRobot item) {
        item.shield = item.shield - attack;
        return item.shield < 0;
    }

    @Override
    public void updateItem() {
        if (shield > 5) {
            this.power = ConstantApp.getInstance().ROBOT_POWER_MILITARY;
        } else {
            this.power = 0;
        }
    }
}

Код 11. Полиморфизм, ремонтный робот.

public class TransportRobot extends BaseRobot implements UpdateState {
    
    protected Integer cargoWeight;

    public TransportRobot(Integer xStart, Integer yStart, Integer serialNumber) {
        this.uuid = UUID.randomUUID();
        this.dataIssue = new Date();
        this.power = ConstantApp.getInstance().ROBOT_POWER_TRANSPORT;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.cargoWeight = 0;
    }

    public Integer getCargoWeight() {
        return cargoWeight;
    }

    public void setCargoWeight(Integer cargoWeight) {
        this.cargoWeight = cargoWeight;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    //Метод загрузки
    public boolean loadingCargo() {
        if (this.power == ConstantApp.getInstance().ROBOT_POWER_TRANSPORT) {
            this.power = 0;
            this.cargoWeight = ConstantApp.getInstance().ROBOT_TRANSPORT_WEIGHT_POINTS;
            return true;
        }
        return false;
    }

    //Метод разгрузки
    public boolean unloadingCargo() {
        if (this.power == ConstantApp.getInstance().ROBOT_POWER_TRANSPORT) {
            this.power = 0;
            this.cargoWeight = 0;
            return true;
        }
        return false;
    }

    @Override
    public void updateItem() {
        if (shield > 1) {
            this.power = ConstantApp.getInstance().ROBOT_POWER_TRANSPORT;
        } else {
            this.power = 0;
        }
    }
}

Код 12. Полиморфизм, транспортный робот.

public class UtilityRobot extends BaseRobot implements UpdateState {
    
    protected Integer repairPointShield;

    public UtilityRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer repairPointShield) {
        this.uuid = UUID.randomUUID();
        this.dataIssue = new Date();
        this.power = ConstantApp.getInstance().ROBOT_POWER_UTILITY;
        this.shield = ConstantApp.getInstance().ROBOT_SHIELD_UTILITY;
        this.xPosition = xStart;
        this.yPosition = yStart;
        this.repairPointShield = repairPointShield;
    }

    public Integer getRepairPointShield() {
        return repairPointShield;
    }

    public void setRepairPointShield(Integer repairPointShield) {
        this.repairPointShield = repairPointShield;
    }

    @Override
    public Integer moveX(Integer xStep) {
        //Обработка перемещения по оси X
        return 0;
    }

    @Override
    public Integer moveY(Integer yStep) {
        //Обработка перемещений по оси У
        return 0;
    }

    public void repairUnit(BaseRobot item) {
        item.power = 0;
        item.shield += repairPointShield;
        if (item instanceof MilitaryRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_MILITARY)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_MILITARY;
        } else if (item instanceof UtilityRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_UTILITY)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_UTILITY;
        } else if (item instanceof TransportRobot) {
            if(item.shield > ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT)
                item.shield = ConstantApp.getInstance().ROBOT_SHIELD_TRANSPORT;
        }
    }

    @Override
    public void updateItem() {
        
        this.power = ConstantApp.getInstance().ROBOT_POWER_UTILITY;
    }
}

Таким образом, полиморфизм позволяет группировать разные объекты по интерфейсу в массивы и обрабатывать их единым списком, в нашем случае, единый список игровых объектов, для актуализации параметров в игре по завершению хода.

Теперь, когда понятны общие принципы ООП, погружаемся в глубины ООП, а именно в SOLID. На днооо…..

SOLID

Теперь перейдем к принципам SOLID, демонстрируя их на классах наших роботов. Начнем с того, что перечислим принципы SOLID прямо по заглавным буквам.

  • S — Single Responsibility Principle — принцип единственной ответственности. Каждый класс должен иметь только одну причину для изменений.

  • O — Open‑Closed Principle (Принцип открытости/закрытости). Программные сущности, выраженные в классах, должны быть открыты для расширения и закрыты для изменения. 

  • L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков). Если существует базовый класс и подтипы базового класса, то любой подтип базового класса должен работать корректно, как и базовый класс.

  • I — Interface Segregation Principle (Принцип разделения интерфейса). Интерфейсы должны содержать только необходимые методы. Лучше создать несколько маленьких интерфейсов, чем один большой.

  • D — Dependency Inversion Principle (Принцип инверсии зависимостей). Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Теперь подробнее по каждому принципу.

SRP. Принцип единственной ответственности

У нас есть робот, и робот должен информировать игровую панель о своем состоянии, где он находится, какие у него координаты, состояние щита и энергии.

Решение в лоб, это добавить для каждого класса робота новый метод, прямо в класс, который будет отправлять интересующие игроков данные.

Код 13, 14 ниже показывает код с решением в лоб, которое нарушает принцип SRP. Код конечно условный, реальная реализация пока в разработке, как и сама игра.

Код 13. Добавляем метод отправки сообщения для ремонтного робота.

public class UtilityRobot extends BaseRobot implements UpdateState {

    // ...

    public void sendMessage() {
        String messagePosition = "X: " +xPosition + " , Y: " + yPosition;
        String messageData = "UUID: " + uuid + " , repair point: " + repairPointShield
                + " , power: " + power + " , shield: " + shield;
        System.out.println(messagePosition);
        System.out.println(messageData);
    }

    // ...

}

Код 14. Добавляем метод отправки сообщения для военного робота.

public class MilitaryRobot extends BaseRobot implements UpdateState {

    // ...
  
    public void sendMessage() {
        String messagePosition = "X: " +xPosition + " , Y: " + yPosition;
        String messageData = "UUID: " + uuid + " , attack point: " + attack
                + " , power: " + power + " , shield: " + shield;
        System.out.println(messagePosition);
        System.out.println(messageData);
    }

    // ... 
}

Для того, чтобы решить задачу отправки уведомлений, с использованием принципа SRP необходимо создать менеджер отправки сообщений. Специальный класс, в который вынести всю логику обработки и отправки сообщения.

Создадим класс NotificationManager (Код 15). При этом никаких изменений в классах роботов не потребуется.

Код 15 показывает пример работы менеджера уведомлений. Мы создаем экземпляр менеджера уведомлений и передаем ему на отправку объект. А дальше уже вся логика отправки и изменении этой логики происходит в менеджере уведомлений.

Код 15. Класс менеджер уведомлений.

public class NotificationManager {
    
    public NotificationManager() {}

    private void sendMessage(String... message) {
        System.out.println("Start message");
        for (int i = 0; i < message.length; i++) {
            System.out.println(message[i]);
        }
        System.out.println("End message");
    }

    public void prepareAndSendMessage(BaseRobot robot) {
        if(robot instanceof TransportRobot) {
            TransportRobot item = (TransportRobot) robot;
            String messagePosition = "X: " + item.getxPosition() + " , Y: " + item.getyPosition();
            String messageData = "UUID: " + item.getUuid() + " , weight point: " + item.getCargoWeight()
                    + " , power: " + item.getPower() + " , shield: " + item.getShield();
            sendMessage(messagePosition, messageData);
        } else if (robot instanceof MilitaryRobot) {
            MilitaryRobot item = (MilitaryRobot) robot;
            String messagePosition = "X: " + item.getxPosition() + " , Y: " + item.getyPosition();
            String messageData = "UUID: " + item.getUuid() + " , attack point: " + item.getAttack()
                    + " , power: " + item.getPower() + " , shield: " + item.getShield();
            sendMessage(messagePosition, messageData);
        } else if (robot instanceof UtilityRobot) {
            UtilityRobot item = (UtilityRobot) robot;
            String messagePosition = "X: " + item.getxPosition() + " , Y: " + item.getyPosition();
            String messageData = "UUID: " + item.getUuid() + " , repair point: " + item.getRepairPointShield()
                    + " , power: " + item.getPower() + " , shield: " + item.getShield();
            sendMessage(messagePosition, messageData);
        }
    }
}

Пример использования класса менеджера уведомлений в «игрушечном» классе управления игрой показан в коде 16. Мы создаем экземпляр класса NotificationManager и передаем в него объекты наших военных роботов m1 и m2.

Код 16. Пример использования кода с принципом SRP.

public class GameManagement {

    public static void main(String[] args) {
        NotificationManager notificationManager = new NotificationManager();
        MilitaryRobot m1 = new MilitaryRobot( 5, 5, 1,
                ConstantApp.getInstance().ROBOT_MILITARY_ATTACK_POINTS*2);
        MilitaryRobot m2 = new MilitaryRobot( 5, 5, 1,
                ConstantApp.getInstance().ROBOT_MILITARY_ATTACK_POINTS);

        notificationManager.prepareAndSendMessage(m1);
        notificationManager.prepareAndSendMessage(m2);
    }
}

Конечно, с помощью ООП можно разобрать метод «prepareAndSendMessage», создать для каждого случая свой класс обработки, а логику из цепочки if заменить на фабрику, но это уже паттерны проектирования, об этом в другой статье.

OCP. Принцип открытости/закрытости

При изучении литературы и интернет статей, я нашел несколько взглядов на реализацию механизма OCP, и при обобщении выделил два понимания для этого принципа:

  • первое понимание, разработанная изначально реализация класса не изменяется, а любые изменения производятся через создание нового класса.

  • второе понимание, проектировать классы таким образом, чтобы добавлять новую функциональность через интерфейсы или абстрактные классы.

Вроде кажется все банально и просто, но дьявол, как всегда, в деталях.

Если рассматривать «первый момент», то по факту мы приходим к обычному механизму наследования, в котором наследник расширяется новыми методами, но и можем переопределить наследуемый метод, изменить в нем логику.

Если рассматривать «второй момент», то он основан на выделении общего метода в интерфейс, и написание изначально кода таким образом, что данный интерфейс применяется. Например, у нас есть метод «sendMessage» в классе NotificationManager. И мы хотим, чтобы у нас был менеджер уведомлений Дашборда, менеджер уведомлений для логов и менеджер сохранения уведомлений в базу данных. Таким образом, мы должны изначально спроектировать наш менеджер уведомлений таким образом, чтобы выделить общий код в абстрактный класс, а метод «sendMessage» описать в интерфейсе или абстрактным методом в абстрактном классе. Получится громозко, но будет расширяемо без внесения изменений в реализованные классы. Но потребуется переписать первоначальный вариант менеджера уведомлений, что само по себе «противоречие» для «первого понимания» или считаем это рефакторингом.

Создадим класс BaseNotificationManager, вынесем в него метод prepareAndSendMessage и создадим абстрактный метод «sendMesssage», который будем переопределять в классах наследниках от абстрактного класса BaseNotificationManager.

Наша игра развивается, расширяется и вот нам нужен не просто отправлять сообщения в дашборд игровой, но еще отправлять данные в журнал логов о состоянии робота и записывать все это «робототехническое безобразие» в базу данных. Таким образом мы приходим к тому, что нужно создать три класса наследника от абстрактного класса: LogNotificationManager, DatabaseNotificationManager, DashboardNotificationManager.

Добавить эти классы в класс управления игрой и передавать в них объекты, данные от которых необходимо отправлять в виде сообщений.

Код 17. Абстрактный базовый класс менеджера уведомлений.

public abstract class BaseNotificationManager {

    protected abstract void sendMessage(String... message);

    public void prepareAndSendMessage(BaseRobot robot) {
        if(robot instanceof TransportRobot) {
            TransportRobot item = (TransportRobot) robot;
            String messagePosition = "X: " + item.getxPosition() + " , Y: " + item.getyPosition();
            String messageData = "UUID: " + item.getUuid() + " , weight point: " + item.getCargoWeight()
                    + " , power: " + item.getPower() + " , shield: " + item.getShield();
            sendMessage(messagePosition, messageData);
        } else if (robot instanceof MilitaryRobot) {
            MilitaryRobot item = (MilitaryRobot) robot;
            String messagePosition = "X: " + item.getxPosition() + " , Y: " + item.getyPosition();
            String messageData = "UUID: " + item.getUuid() + " , attack point: " + item.getAttack()
                    + " , power: " + item.getPower() + " , shield: " + item.getShield();
            sendMessage(messagePosition, messageData);
        } else if (robot instanceof UtilityRobot) {
            UtilityRobot item = (UtilityRobot) robot;
            String messagePosition = "X: " + item.getxPosition() + " , Y: " + item.getyPosition();
            String messageData = "UUID: " + item.getUuid() + " , repair point: " + item.getRepairPointShield()
                    + " , power: " + item.getPower() + " , shield: " + item.getShield();
            sendMessage(messagePosition, messageData);
        }
    }
}

Код 18. Реализация менеджера уведомлений для дашборда

public class DashboardNotificationManager  extends BaseNotificationManager {
    @Override
    protected void sendMessage(String... message) {
        System.out.println("Dashboard Notification processing ... ");
        System.out.println("Start message - dashboard");
        for (int i = 0; i < message.length; i++) {
            System.out.println(message[i]);
        }
        System.out.println("End message - dashboard");
    }
}

Код 19. Реализация менеджера уведомлений для БД

public class DatabaseNotificationManager extends BaseNotificationManager {
    @Override
    protected void sendMessage(String... message) {
        System.out.println("Database Notification processing ... ");
        System.out.println("Start message - database");
        for (int i = 0; i < message.length; i++) {
            System.out.println(message[i]);
        }
        System.out.println("End message - database");
    }
}

Код 20. Реализация менеджера уведомлений для логирования

public class LogNotificationManager  extends BaseNotificationManager {
    @Override
    protected void sendMessage(String... message) {
        System.out.println("Log Notification processing ... ");
        System.out.println("Start message - logs");
        for (int i = 0; i < message.length; i++) {
            System.out.println(message[i]);
        }
        System.out.println("End message - logs");
    }
}

Таким образом, мы создали систему, при которой, добавление каждого нового механизма уведомлений, не вносит изменений в существующие классы, а только расширяет класс управления игрой, просто добавляя в него новый элемент уведомлений.

Но как же быть с «первым пониманием» OCP. Его тоже надо учитывать, и руководствоваться им в том случае, если изначально система не была спроектирована с учетом принципа OCP.

LSP. Принцип подстановки Барбары Лисков

Название конечно шикарное, которое не говорит практически ни о чем. Кто такая Барбара Лисков, причем ее подстановки и вообще что это за «дичь» и с чем ее едят.

Итак, Барбара Лисков — это американский ученый в области информатики, исследователь проблемы абстракции данных, преподаватель в Массачусетском технологическом университете. Разработала в 1987 году принцип подстановки — концепцию определения подтипа.

Теперь, что такое принцип подстановки. Официальное определение — «Если в программе используется базовый класс, то любой его подкласс должен работать так же корректно, как и родительский класс». Вроде понятно, но как мы знаем дьявол в деталях, но об этом позже. Главный вопрос — что значит корректно? Корректно — это соблюдение контрактов, наследуемой логики, или что? Обратимся к Википедии на английском языке (https://en.wikipedia.org/wiki/Liskov_substitution_principle) и увидим интересный текст, а именно — «A principle in object‑oriented programming stating that an object (such as a class) may be replaced by a sub‑object (such as a class that extends the first class) without breaking the program. It is a semantic rather than merely syntactic relation, because it intends to guarantee semantic interoperability of types in a hierarchy, object types in particular.». Что означает, что изначальный класс не должен изменяться, наследники должны только расширять логику новыми методами. Данный принцип ничего не говорит о том, можем ли мы или не можем изменять логику в переопределенных методах базового класса в наследниках. Только лишь то, что родительский класс, может быть заменен классом наследником, при этом должна соблюдаться семантическая совместимость, и такая подстановка не должна приводить к «крашу» или прекращению работы программы.

Теперь немного кода и проектирования.

Наша игра развивается и растет, появляются новые юниты с новыми возможностями. Теперь нам нужен ремонтный робот, который будет не только ремонтировать роботов, но и ремонтировать шахты, строить/разрушать шахты, строить/разрушать другие объекты в игре, связанные с абстрактным классом BaseBuilding. Для этого создадим данный абстрактный класс и создадим интерфейс, который мы должны реализовать, чтобы позволить строить и демонтировать (разрушать) объекты наследники абстрактного класса BaseBuilding.

Код 21. Абстрактный класс строения.

public abstract class BaseBuilding {
  
    protected String nameOfBuilding;
  
    protected String typeOfBuilding;

    public BaseBuilding(String name, String typeOfBuilding) {
      
        this.nameOfBuilding = name;
      
        this.typeOfBuilding = typeOfBuilding;
      
    }

    public String getNameOfBuilding() {
        return nameOfBuilding;
    }

    public String getTypeOfBuilding() {
        return typeOfBuilding;
    }
}

Код 22. Интерфейс для новой функциональности наследников.

public interface OperationBuilding {
    
    void repairBuilding(BaseBuilding baseBuilding);
    
    void buildBuilding(BaseBuilding baseBuilding);
    
    void destroyBuilding(BaseBuilding baseBuilding);
    
}

Итак мы создаем нового ремонтного робота, более продвинутого, который может строить, разрушать и ремонтировать строения. Строения строятся и разрушаются постепенно, ход за ходом, на определенное количество ConstructPoint за ход. У каждого типа строения свое количество ConstructPoint. Реализацию этого мы оставим на будущее, для примера это не важно. Класс нового ремонтного робота называется ConstructUtilityRobot, он не только может оперировать со зданиями, но имеет лучшую функцию починки, которая восстанавливает юниты в два раза быстрее. Для этого в конструкторе класса мы увеличиваем параметр repairPointShield в два раза (в приложении у нас есть константа базового значения поинтов для починки роботов), сам метод для починки роботов не изменяется.

Кодк 23. Класс продвинутого ремонтного робота.

public class ConstructUtilityRobot extends UtilityRobot implements OperationBuilding {
    
    public ConstructUtilityRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer repairPointShield) {
        super(xStart, yStart, serialNumber, repairPointShield*2);
    }

    @Override
    public void repairBuilding(BaseBuilding baseBuilding) {
        System.out.println("Repair building ...");
    }

    @Override
    public void buildBuilding(BaseBuilding baseBuilding) {
        System.out.println("Build building ...");
    }

    @Override
    public void destroyBuilding(BaseBuilding baseBuilding) {
        System.out.println("Destroy building ...");
    }
    
}

Структура класса родительского UtilityRobot и ремонтного робота следующего поколения ConstructUtilityRobot такая, что функция по ремонту роботов не изменялась и не переопределялась, логика осталась такая же, хотя скорость ремонта роботов увеличилась в два раза. Теперь не важно, какой робот будет ремонтировать, родительский или потомок, логика ремонта не нарушена. Принцип подстановки Барбары Лисков соблюден.

ISP. Принцип разделения интерфейса

Наш проект растет и развивается, у нас появились новые объекты — это здания, а также новый вид ремонтных роботов, который реализуя интерфейс OperationBuilding могут строить, разрушать и чинить здания.

Раз есть роботы, которые могут строить, значит должны быть и те, которые могут разрушать вражеские здания, так давайте создадим новый вид военного робота с возможностью разрушать здания, тем более, что у нас уже есть готовый интерфейс. Итак, решено, берем наш интерфейс созданный на предыдущем шаге, создаем класс наследник от военного робота, называем его DevastatorMilitaryRobot и вроде все отлично… Но, кажется, что‑то пошло не так. Смотрим на код ниже.

Код 24. Код класса DevastatorMilitaryRobot.

public class DevastatorMilitaryRobot extends MilitaryRobot implements OperationBuilding {
    
    public DevastatorMilitaryRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer attack) {
        super(xStart, yStart, serialNumber, attack);
    }

    @Override
    public void repairBuilding(BaseBuilding baseBuilding) {
        //Данный метод не делает ничего
    }

    @Override
    public void buildBuilding(BaseBuilding baseBuilding) {
        //Данный метод не делает ничего
    }

    @Override
    public void destroyBuilding(BaseBuilding baseBuilding) {
        System.out.println("Destroy enemy buildings... huu-raa ");
    }
}

Наш робот опустошитель теперь может разрушать здания, но также содержит два пустых метода по строительству и починке зданий. А это совсем не правильно.

И здесь приходит на помощь принцип разделения интерфейса. Необходимо один большой интерфейс разделить на два по смыслу интерфейса, для строительства/починки зданий и для разрушения заданий. Таким образом вместо одного интерфейса большого, будет два маленьких, разделенных по смыслу и наш военный робот будет иметь ровно те возможности, которые планировалось, без лишнего пустого кода. Код ниже.

Код 25. Реализация интерфейса для строительства и починки зданий.

public interface OperationBuildBuilding {
    
    void repairBuilding(BaseBuilding baseBuilding);
    
    void buildBuilding(BaseBuilding baseBuilding);
    
}

Код 26. Реализация интерфейса для уничтожения зданий.

public interface OperationDestroyBuilding {
    
    void destroyBuilding(BaseBuilding baseBuilding);
    
}

Класс робота опустошителя теперь выглядит красиво и просто.

Код 27. Класс робота опустошителя.

public class DevastatorMilitaryRobot extends MilitaryRobot implements OperationDestroyBuilding {

    public DevastatorMilitaryRobot(Integer xStart, Integer yStart, Integer serialNumber, Integer attack) {
        super(xStart, yStart, serialNumber, attack);
    }

    @Override
    public void destroyBuilding(BaseBuilding baseBuilding) {
        System.out.println("Destroy enemy buildings... huu-raa ");
    }
}

DIP. Принцип инверсии зависимостей

Данный принцип требует от нас, чтобы модули высокого уровня зависели от абстракций, а не от модулей низкого уровня. Модули высокого уровня — это бизнес‑логика, а модули низкого уровня — это работы с конкретной реализацией аппаратной части, либо реализация API, работа с файловой системой и так далее

Представим, что наша игра может быть как локальной однопользовательской, так сетевой многопользовательской и нам нужно сохранять состояние игры, когда мы ее ставим на паузу. Допустим при локальной игре, мы сохраняем файл с состоянием игры на локальный жесткий диск, а в случае сетевой игры — сохраняем на жесткий диск нашего сервера игры. Но при этом метод называться для сохранения состояния будет один и тот же в обоих случаях.

Как решить это — просто, на самом деле. Нужно создать интерфейс, реализовывать который будут два класса обработки состояния, и в зависимости от типа игры будет подставляться тот или иной объект в классе управления игрой. Ниже показан код реализации такого решения.

Код 28. Код интерфейса.

public interface StateSaveGameRepository {
    
    void saveStateGame();
    
}

Код 29. Код реализации сохранения локально на компьютере

public class LocalStateSaveGameRepository  implements StateSaveGameRepository {
    
    @Override
    public void saveStateGame() {
        System.out.println("Save on local computer... ");
    }
    
}

Код 30. Код сохранения на сервере.

public class NetworkStateSaveGameRepository implements StateSaveGameRepository {
    
    @Override
    public void saveStateGame() {
        System.out.println("Save use network on the game server ");
    }
}

Таким образом, данный принцип позволяет нам создавать взаимозаменяемые программные модули без внесения изменений в классы логики. И конечно, подходы чистой архитектуры прочно стоят на данном принципе.

Код 31. Код применения данного принципа, условно конечно.

public class GameManagement {

    public static void main(String[] args) {
        StateSaveGameRepository repository;
        Boolean network = true;
        if (network) {
            repository = new NetworkStateSaveGameRepository();
        } else {
            repository = new LocalStateSaveGameRepository();
        }
        repository.saveStateGame();
    }

}

Исходный код примеров для данной статьи можно скачать вот здесь по ссылке

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.