Гайд Введение в SOLID

Начинающий
Статус
Оффлайн
Регистрация
23 Дек 2024
Сообщения
71
Реакции[?]
1
Поинты[?]
1K

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

  • бесплатные читы для Майнкрафт — любое использование на свой страх и риск;
  • маркетплейс Майнкрафт — абсолютно любая коммерция, связанная с игрой, за исключением продажи читов (аккаунты, предоставления услуг, поиск кодеров читов и так далее);
  • приватные читы для Minecraft — в этом разделе только платные хаки для игры, покупайте группу "Продавец" и выставляйте на продажу свой софт;
  • обсуждения и гайды — всё тот же раздел с вопросами, но теперь модернизированный: поиск нужных хаков, пати с игроками-читерами и другая полезная информация.

Спасибо!

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

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

S - Single Responsibility (принцип единственной ответственности)
Каждый класс должен иметь только одну зону ответственности.

O - Open Closed (принцип открытости-закрытости)
Классы должны быть открыты для расширения, но закрыты для изменения.

L - Liskov Substitution (принцип подстановки Лисков)
Должна быть возможность вместо базового (родительского) типа (класса) подставить любой его подтип (класс-наследник), при этом работа программы не должна измениться.

I - Interface Segregation (принцип разделения интерфейсов)
Не нужно заставлять клиента (класс) реализовывать интерфейс, который не имеет к нему отношения.

D - Dependency Inversion (принцип инверсии зависимостей)
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракции. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Плавно перейдём от слов к делу.
Рассмотрим первый принцип - Single Responsibility

Допустим у нас есть класс PcShopService и в нем есть несколько методов: найти пк по номеру, заказать пк, распечатать заказ, получить информацию о пк, отправить сообщение.

Java:
public class PcShopService {

    public Pc findPc(String pcNum) {
        // находим пк по номеру
        return pc;
    }

    public Order orderPc(String pcNum, Client client) {
        // клиент заказывает пк
        return order;
    }

    public void printOrder(Order order) {
        // печать заказа
    }
    public void getPcInfo(String pcType) {
        if (pcType.equals("firstPc")) {
            // что-то делаем
        }
        if (pcType.equals("secondPc")) {
            // что-то делаем
        }
        if (pcType.equals("thirdPc")) {
            // что-то делаем
        }
    }
    public void sendMessage(String typeMessage, String message) {
        if (typeMessage.equals("email")) {
            // отправляем сообщение на почту
        }
    }
}

У данного класса есть несколько зон ответственности, что является нарушением первого принципа. Возьмем метод получения информации о пк. Теперь у нас есть только три типа пк firstPc, secondPc и thirdPc, но если человек захочет добавить еще несколько типов, тогда придется изменять и дописывать данный метод.

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

Одним словом, данный класс нарушает принцип единой ответственности, так как отвечает за разные действия.

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

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

Java:
public class PrinterService {
    public void printOrder(Order order) {
        // печать заказа
    }
}
Аналогично работа связанная с поиском информации о пк должна быть перенесена в класс PcInfoService.

Java:
public class PcInfoService {
    public void getPcInfo(String pcType) {
        if (pcType.equals("firstPc")) {
            // что-то делаем
        }
        if (pcType.equals("secondPc")) {
            // что-то делаем
        }
        if (pcType.equals("thirdPc")) {
            // что-то делаем
        }
    }
}
Метод по отправке сообщений перенести в класс NotificationService.

Java:
public class NotificationService {
    public void sendMessage(String typeMessage, String message) {
        if (typeMessage.equals("email")) {
            // отправляем сообщение на почту
        }
    }
}
А метод поиска пк в PcService.

Java:
public class PcService {
    public Pc findPc(String pcNum) {
        // находим пк по номеру
        return pc;
    }
}
И в классе PcShopService останется только один метод.


Java:
public class PcShopService {
    public Order orderPc(String pcNum, Client client) {
        // клиент заказывает пк
        return order;
    }
}
Теперь каждый класс несет ответственность только за одну зону и есть только одна причина для его изменения.


Принцип открытости-закрытости (Open Closed) рассмотрим на примере только что созданного класса по отправке сообщений.

Java:
public class NotificationService {
    public void sendMessage(String typeMessage, String message) {
        if (typeMessage.equals("email")) {
            // отправляем сообщение на почту
        }
    }
}
Допустим нам необходимо кроме отправки сообщения по электронной почте отправлять еще смс сообщения. И мы можем дописать метод sendMessage таким образом:

Java:
public class NotificationService {
    public void sendMessage(String typeMessage, String message) {
        if (typeMessage.equals("email")) {
            // отправляем сообщение на почту
        }
        if (typeMessage.equals("sms")) {
            // отправляем сообщение на телефон
        }

    }
}


Но в данном случае мы нарушим второй принцип, потому что класс должен быть закрыт для модификации, но открыт для расширения, а мы модифицируем (изменяем) метод.

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

Java:
public interface NotificationService {
    public void sendMessage(String message);
}
Далее создадим класс EmailNotification, который имплементит интерфейс NotificationService и реализует метод отправки сообщений по почте.

Java:
public class EmailNotification implements NotificationService {
    @Override
    public void sendMessage(String message) {
        // отправляем сообщение на почту
    }
}
Создадим аналогично класс MobileNotification, который будет отвечать за отправку сообщений на телефон.

Java:
public class MobileNotification implements NotificationService {
    @Override
    public void sendMessage(String message) {
        // отправляем сообщение на телефон
    }
}
Проектируя таким образом код мы не будем нарушать принцип открытости-закрытости, так как мы расширяем нашу функциональность, а не изменяем наш класс.


Третий принцип: Liskov Substitution

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

Java:
public class Account {
    public BigDecimal balance(String numberAccount){
        // логика
        return bigDecimal;
    };
    public void refill(String numberAccount, BigDecimal sum){
        // логика
    }
    public void payment(String numberAccount, BigDecimal sum){
        // логика
    }

}
Нам необходимо написать еще два класса: зарплатный счет и депозитный счет, при этом зарплатный счет должен поддерживать все операции, представленные в базовом классе, а депозитный счет - не должен поддерживать проведение оплаты.

Java:
public class SalaryAccount extends Account {
    @Override
    public BigDecimal balance(String numberAccount){
        // логика
        return bigDecimal;
    };
    @Override
    public void refill(String numberAccount, BigDecimal sum){
        // логика
    }
    @Override
    public void payment(String numberAccount, BigDecimal sum){
        // логика
    }
}
Java:
public class DepositAccount extends Account {
    @Override
    public BigDecimal balance(String numberAccount){
        // логика
        return bigDecimal;
    };
    @Override
    public void refill(String numberAccount, BigDecimal sum){
        // логика
    }
    @Override
    public void payment(String numberAccount, BigDecimal sum){
        throw new UnsupportedOperationException();
    }
}
Если сейчас в коде программы везде, где мы использовали класс Account заменить на его подтип SalaryAccount, то код продолжит нормально работать, так как в классе SalaryAccount доступны все операции, которые есть и в классе Account.

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

Java:
public class Account {
    public BigDecimal balance(String numberAccount){
        // логика
        return bigDecimal;
    };
    public void refill(String numberAccount, BigDecimal sum){
        // логика
    }
}
Мы сможем от него наследовать класс DepositAccount.

Java:
public class DepositAccount extends Account{
    @Override
    public BigDecimal balance(String numberAccount){
        // логика
        return bigDecimal;
    };
    @Override
    public void refill(String numberAccount, BigDecimal sum){
        // логика
    }
}
Создадим дополнительный класс PaymentAccount, который унаследуем от Account и его расширим методом проведения оплаты.

Java:
public class PaymentAccount extends Account{
    public void payment(String numberAccount, BigDecimal sum){
        // логика
    }
}
И наш класс SalaryAccount уже унаследуем от класса PaymentAccount.

Java:
public class SalaryAccount extends PaymentAccount{
    @Override
    public BigDecimal balance(String numberAccount){
        // логика
        return bigDecimal;
    };
    @Override
    public void refill(String numberAccount, BigDecimal sum){
        // логика
    }
    @Override
    public void payment(String numberAccount, BigDecimal sum){
        // логика
    }
}
Принцип подстановки Лисков заключается в правильном использовании отношения наследования. Мы должны создавать наследников какого-либо базового класса тогда, когда они собираются правильно реализовать его логику, не вызывая проблем при замене родителей на наследников.


Принцип разделения интерфейсов (Interface Segregation)

Допустим у нас есть интерфейс Payments и в нем есть три метода: оплата WebMoney, оплата банковской картой и оплата по номеру.

Java:
public interface Payments {
    void payWebMoney();
    void payCreditCard();
    void payPhoneNumber();
}
Далее нам надо реализовать два класса-сервиса, которые будут у себя реализовывать различные виды проведения оплат (класс InternetPaymentService и TerminalPaymentService). При этом TerminalPaymentService не будет поддерживать проведение оплат по номеру телефона. Но если мы оба класса имплементим от интерфейса Payments, то мы будем заставлять TerminalPaymentService реализовывать метод, который ему не нужен.

Java:
public class InternetPaymentService implements Payments {
    @Override
    public void payWebMoney() {
        // логика
    }
    @Override
    public void payCreditCard() {
        // логика
    }
    @Override
    public void payPhoneNumber() {
        // логика
    }
}
Java:
public class TerminalPaymentService implements Payments {
    @Override
    public void payWebMoney() {
        // логика
    }
    @Override
    public void payCreditCard() {
        // логика
    }
    @Override
    public void payPhoneNumber() {
        // логика
    }
}

Таким образом произойдет нарушение принципа разделения интерфейсов.

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


Java:
public interface WebMoneyPayment {
    void payWebMoney();
}
Java:
public interface CreditCardPayment {
    void payCreditCard();
}
Java:
public interface PhoneNumberPayment {
    void payPhoneNumber();
}
Java:
public class InternetPaymentService implements WebMoneyPayment, CreditCardPayment, PhoneNumberPayment {
    @Override
    public void payWebMoney() {
        // логика
    }
    @Override
    public void payCreditCard() {
        // логика
    }
    @Override
    public void payPhoneNumber() {
        // логика
    }
}
Java:
public class TerminalPaymentService implements WebMoneyPayment, CreditCardPayment {
    @Override
    public void payWebMoney() {
        // логика
    }
    @Override
    public void payCreditCard() {
        // логика
    }
}

Рассмотрим последний принцип: принцип инверсии зависимостей (Dependency Inversion)

Допустим мы пишем приложение для магазина и решаем вопросы с проведением оплат. Вначале это просто небольшой магазин, где оплата происходит только за наличные. Создаем класс Cash и класс Shop.

Java:
public class Cash {
    public void doTransaction(BigDecimal amount){
        // логика
    }
}
Java:
public class Shop {
    private Cash cash;
    public Shop(Cash cash) {
        this.cash = cash;
    }
    public void doPayment(Object order, BigDecimal amount){
        cash.doTransaction(amount);
    }
}
Вроде все хорошо, но мы уже нарушили принцип инверсии зависимостей, так как мы тесно связали оплату наличными к нашему магазину. И если в дальнейшем нам необходимо будет добавить оплату еще банковской картой и телефоном (100% понадобится), то нам придется переписывать и изменять много кода.

Поэтому создадим интерфейс Payments.

Java:
public interface Payments {
    void doTransaction(BigDecimal amount);
}
Теперь все наши классы по оплате будут имплементить данный интерфейс.

Java:
public class Cash implements Payments {
    @Override
    public void doTransaction(BigDecimal amount) {
        // логика
    }
}
Java:
public class BankCard implements Payments {
    @Override
    public void doTransaction(BigDecimal amount) {
         // логика
    }
}
Java:
public class PayByPhone implements Payments {
    @Override
    public void doTransaction(BigDecimal amount) {
        // логика
    }
}
Теперь надо перепроектировать наш магазин.

Java:
public class Shop {
    private Payments payments;

    public Shop(Payments payments) {
        this.payments = payments;
    }

    public void doPayment(Object order, BigDecimal amount){
        payments.doTransaction(amount);
    }
}
Сейчас наш магазин слабо связан с системой оплаты, то есть он зависит от абстракции и уже не важно каким способом оплаты будут пользоваться - все будет работать.


Мы рассмотрели на примерах псевдокода все принципы SOLID, надеюсь кому-то будет это полезно.
 
На самом деле я Zodiak
Read Only
Статус
Оффлайн
Регистрация
22 Дек 2020
Сообщения
1,104
Реакции[?]
200
Поинты[?]
98K
Так скажи какую нейросеть использовал, я ей лайк поставлю
 
Начинающий
Статус
Оффлайн
Регистрация
25 Янв 2024
Сообщения
471
Реакции[?]
1
Поинты[?]
3K
да прикольно, интересно но у меня код стайл чут чуть другой и я считаю то что создание таких интерфейсов - загрязнение базы. Это мое мнение
 
Начинающий
Статус
Оффлайн
Регистрация
23 Дек 2024
Сообщения
71
Реакции[?]
1
Поинты[?]
1K
да прикольно, интересно но у меня код стайл чут чуть другой и я считаю то что создание таких интерфейсов - загрязнение базы. Это мое мнение
для здешних экспенсив юзеров хватило бы для более менее приятного кода использовать 4 основных принципа: абстракцию, инкапсуляцию, наследование и полиморфизм. впринципе я думаю тут процентов 80 даже таких слов не знает
а насчет солида, ну да, может быть и так, что конкретно в случае майнкрафта он может быть для многих не юзабельным, но тут тоже не все так однозначно
 
Начинающий
Статус
Оффлайн
Регистрация
25 Янв 2024
Сообщения
471
Реакции[?]
1
Поинты[?]
3K
да, верно помню летом чекал сурсы 4.0 там +- чето на солид похожее
 
Начинающий
Статус
Оффлайн
Регистрация
16 Сен 2024
Сообщения
145
Реакции[?]
4
Поинты[?]
1K
Какой смысл от этой темы (тем более в разделе где тупо пастят) если уже было это где все понятно расписано?
 
MetalHead
EXCLUSIVE
Статус
Оффлайн
Регистрация
20 Фев 2025
Сообщения
29
Реакции[?]
3
Поинты[?]
3K
Спасибо, о Великий, что открыл нам величайшую тайну ООП, никто же не мог в гугле вбить - "SOLID/GRASP & other patterns java code examples with explanation". Не думал стать Arch engineer? А если серьёзно то, как и сказано выше статья - ненужный кал. У Вендо явно лучше и подробнее.
 
Сверху Снизу